Merge m-c to b2g-inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 15 Oct 2014 15:58:42 -0400
changeset 210568 8881162414762625c8ae81f91a041e1896d5aef8
parent 210567 3ec8efa8d0509c70091d9ffe6664d9681c3c4f93 (current diff)
parent 210557 a280a03c9f3cc6207cd17a7c76081e4e7ffd4eea (diff)
child 210569 5976d156f577282f4f7ed0eedef36962d4270eec
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmerge
milestone36.0a1
Merge m-c to b2g-inbound. a=merge CLOSED TREE
build/autoconf/ccache.m4
js/xpconnect/tests/unit/test_bug805807.js
layout/xul/nsBoxObject.cpp
layout/xul/nsBoxObject.h
layout/xul/nsContainerBoxObject.cpp
layout/xul/nsIEditorBoxObject.idl
layout/xul/nsIIFrameBoxObject.idl
layout/xul/nsIPopupBoxObject.idl
layout/xul/nsListBoxObject.cpp
layout/xul/nsMenuBoxObject.cpp
layout/xul/nsPopupBoxObject.cpp
layout/xul/nsScrollBoxObject.cpp
layout/xul/tree/nsTreeBoxObject.cpp
layout/xul/tree/nsTreeBoxObject.h
tools/performance/layout/40-url-dup.txt
tools/performance/layout/40-url.txt
tools/performance/layout/Averagetable2.pl
tools/performance/layout/Footer.pl
tools/performance/layout/Header.pl
tools/performance/layout/collect.pl
tools/performance/layout/genfromlogs.pl
tools/performance/layout/history.pl
tools/performance/layout/history.txt
tools/performance/layout/perf-doc.html
tools/performance/layout/perf.pl
tools/performance/layout/property.inc
tools/performance/layout/property.t
tools/performance/layout/readme.txt
tools/performance/layout/tables/013100.html
tools/performance/layout/tables/021300.html
tools/performance/layout/tables/Daily-0317-TrendTable.html
tools/performance/layout/tables/Daily-0317.html
tools/performance/layout/tables/Daily_021700-TrendTable.html
tools/performance/layout/tables/Daily_021700-mozilla.html
tools/performance/layout/tables/Daily_021700.html
tools/performance/layout/tables/Daily_022500.html
tools/performance/layout/tables/Daily_0302.html
tools/performance/layout/tables/Daily_0308-TrendTable.html
tools/performance/layout/tables/Daily_0308.html
tools/performance/layout/tables/Daily_0324.html
tools/performance/layout/tables/M13.html
tools/performance/layout/uncombine.pl
tools/reorder/Makefile
tools/reorder/addrs2text.cpp
tools/reorder/cygprof.c
tools/reorder/elf_symbol_table.cpp
tools/reorder/elf_symbol_table.h
tools/reorder/elf_utils.cpp
tools/reorder/elf_utils.h
tools/reorder/func-by-addr.sh
tools/reorder/garope.cpp
tools/reorder/grope.cpp
tools/reorder/histogram.cpp
tools/reorder/interval_map.h
tools/reorder/mapaddrs.cpp
tools/reorder/mcount.c
tools/reorder/missing-syms.pl
tools/reorder/mult.c
tools/reorder/rseed.c
tools/reorder/syms-by-addr.sh
tools/reorder/test.cpp
tools/tests/ctor-dtor.pl
tools/tests/isa-module.pl
tools/uuiddeps/uuidgrep.bash
--- a/accessible/base/nsCoreUtils.cpp
+++ b/accessible/base/nsCoreUtils.cpp
@@ -52,17 +52,17 @@ nsCoreUtils::HasClickListener(nsIContent
     (listenerManager->HasListenersFor(nsGkAtoms::onclick) ||
      listenerManager->HasListenersFor(nsGkAtoms::onmousedown) ||
      listenerManager->HasListenersFor(nsGkAtoms::onmouseup));
 }
 
 void
 nsCoreUtils::DispatchClickEvent(nsITreeBoxObject *aTreeBoxObj,
                                 int32_t aRowIndex, nsITreeColumn *aColumn,
-                                const nsCString& aPseudoElt)
+                                const nsAString& aPseudoElt)
 {
   nsCOMPtr<nsIDOMElement> tcElm;
   aTreeBoxObj->GetTreeBody(getter_AddRefs(tcElm));
   if (!tcElm)
     return;
 
   nsCOMPtr<nsIContent> tcContent(do_QueryInterface(tcElm));
   nsIDocument *document = tcContent->GetCurrentDoc();
--- a/accessible/base/nsCoreUtils.h
+++ b/accessible/base/nsCoreUtils.h
@@ -39,17 +39,17 @@ public:
    * @param  aTreeBoxObj  [in] tree box object
    * @param  aRowIndex    [in] row index
    * @param  aColumn      [in] column object
    * @param  aPseudoElm   [in] pseudo elemenet inside the cell, see
    *                       nsITreeBoxObject for available values
    */
   static void DispatchClickEvent(nsITreeBoxObject *aTreeBoxObj,
                                  int32_t aRowIndex, nsITreeColumn *aColumn,
-                                 const nsCString& aPseudoElt = EmptyCString());
+                                 const nsAString& aPseudoElt = EmptyString());
 
   /**
    * Send mouse event to the given element.
    *
    * @param aEventType   [in] an event type (see BasicEvents.h for constants)
    * @param aX           [in] x coordinate in dev pixels
    * @param aY           [in] y coordinate in dev pixels
    * @param aContent     [in] the element
@@ -309,9 +309,8 @@ public:
   static bool IsWhitespace(char16_t aChar)
   {
     return aChar == ' ' || aChar == '\n' ||
       aChar == '\r' || aChar == '\t' || aChar == 0xa0;
   }
 };
 
 #endif
-
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -15,16 +15,18 @@ this.EXPORTED_SYMBOLS = ['AccessFu']; //
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
 
 const ACCESSFU_DISABLE = 0; // jshint ignore:line
 const ACCESSFU_ENABLE = 1;
 const ACCESSFU_AUTO = 2;
 
 const SCREENREADER_SETTING = 'accessibility.screenreader';
+const QUICKNAV_MODES_PREF = 'accessibility.accessfu.quicknav_modes';
+const QUICKNAV_INDEX_PREF = 'accessibility.accessfu.quicknav_index';
 
 this.AccessFu = { // jshint ignore:line
   /**
    * Initialize chrome-layer accessibility functionality.
    * If accessibility is enabled on the platform, then a special accessibility
    * mode is started.
    */
   attach: function attach(aWindow) {
@@ -98,21 +100,28 @@ this.AccessFu = { // jshint ignore:line
     let stylesheet = Utils.win.document.createProcessingInstruction(
       'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
     Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild);
     this.stylesheet = Cu.getWeakReference(stylesheet);
 
 
     // Populate quicknav modes
     this._quicknavModesPref =
-      new PrefCache(
-        'accessibility.accessfu.quicknav_modes',
-        (aName, aValue) => {
-          this.Input.quickNavMode.updateModes(aValue);
-        }, true);
+      new PrefCache(QUICKNAV_MODES_PREF, (aName, aValue, aFirstRun) => {
+        this.Input.quickNavMode.updateModes(aValue);
+        if (!aFirstRun) {
+          // If the modes change, reset the current mode index to 0.
+          Services.prefs.setIntPref(QUICKNAV_INDEX_PREF, 0);
+        }
+      }, true);
+
+    this._quicknavCurrentModePref =
+      new PrefCache(QUICKNAV_INDEX_PREF, (aName, aValue) => {
+        this.Input.quickNavMode.updateCurrentMode(Number(aValue));
+      }, true);
 
     // Check for output notification
     this._notifyOutputPref =
       new PrefCache('accessibility.accessfu.notify_output');
 
 
     this.Input.start();
     Output.start();
@@ -662,27 +671,31 @@ var Input = {
           aGesture.touches[0].y);
         break;
       case 'doubletap1':
         this.activateCurrent();
         break;
       case 'taphold1':
         this.sendContextMenuMessage();
         break;
+      case 'doubletaphold1':
+        Utils.dispatchChromeEvent('accessibility-control', 'quicknav-menu');
+        break;
       case 'swiperight1':
         this.moveCursor('moveNext', 'Simple', 'gestures');
         break;
       case 'swipeleft1':
         this.moveCursor('movePrevious', 'Simple', 'gesture');
         break;
       case 'swipeup1':
-        this.contextAction('backward');
+        this.moveCursor(
+          'movePrevious', this.quickNavMode.current, 'gesture', true);
         break;
       case 'swipedown1':
-        this.contextAction('forward');
+        this.moveCursor('moveNext', this.quickNavMode.current, 'gesture', true);
         break;
       case 'exploreend1':
       case 'dwellend1':
         this.activateCurrent(null, true);
         break;
       case 'swiperight2':
         if (aGesture.edge) {
           Utils.dispatchChromeEvent('accessibility-control',
@@ -824,27 +837,22 @@ var Input = {
         {rule: aRule, x: aX, y: aY, origin: 'top'});
     } else {
       let win = Utils.win;
       Utils.winUtils.sendMouseEvent('mousemove',
         aX - win.mozInnerScreenX, aY - win.mozInnerScreenY, 0, 0, 0);
     }
   },
 
-  moveCursor: function moveCursor(aAction, aRule, aInputType) {
+  moveCursor: function moveCursor(aAction, aRule, aInputType, aAdjustRange) {
     let mm = Utils.getMessageManager(Utils.CurrentBrowser);
     mm.sendAsyncMessage('AccessFu:MoveCursor',
-                        {action: aAction, rule: aRule,
-                         origin: 'top', inputType: aInputType});
-  },
-
-  contextAction: function contextAction(aDirection) {
-    // XXX: For now, the only supported context action is adjusting a range.
-    let mm = Utils.getMessageManager(Utils.CurrentBrowser);
-    mm.sendAsyncMessage('AccessFu:AdjustRange', {direction: aDirection});
+                        { action: aAction, rule: aRule,
+                          origin: 'top', inputType: aInputType,
+                          adjustRange: aAdjustRange });
   },
 
   moveByGranularity: function moveByGranularity(aDetails) {
     const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
 
     if (!this.editState.editing) {
       if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) {
         this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture');
@@ -952,31 +960,34 @@ var Input = {
   },
 
   quickNavMode: {
     get current() {
       return this.modes[this._currentIndex];
     },
 
     previous: function quickNavMode_previous() {
-      if (--this._currentIndex < 0) {
-        this._currentIndex = this.modes.length - 1;
-      }
+      Services.prefs.setIntPref(QUICKNAV_INDEX_PREF,
+        this._currentIndex > 0 ?
+          this._currentIndex - 1 : this.modes.length - 1);
     },
 
     next: function quickNavMode_next() {
-      if (++this._currentIndex >= this.modes.length) {
-        this._currentIndex = 0;
-      }
+      Services.prefs.setIntPref(QUICKNAV_INDEX_PREF,
+        this._currentIndex + 1 >= this.modes.length ?
+          0 : this._currentIndex + 1);
     },
 
     updateModes: function updateModes(aModes) {
       if (aModes) {
         this.modes = aModes.split(',');
       } else {
         this.modes = [];
       }
     },
 
-    _currentIndex: -1
+    updateCurrentMode: function updateCurrentMode(aModeIndex) {
+      Logger.debug('Quicknav mode:', this.modes[aModeIndex]);
+      this._currentIndex = aModeIndex;
+    }
   }
 };
 AccessFu.Input = Input;
--- a/accessible/jsat/ContentControl.jsm
+++ b/accessible/jsat/ContentControl.jsm
@@ -89,23 +89,28 @@ this.ContentControl.prototype = {
       Logger.logException(
         x, 'Error handling message: ' + JSON.stringify(aMessage.json));
     }
   },
 
   handleMoveCursor: function cc_handleMoveCursor(aMessage) {
     let origin = aMessage.json.origin;
     let action = aMessage.json.action;
+    let adjustRange = aMessage.json.adjustRange;
     let vc = this.vc;
 
     if (origin != 'child' && this.sendToChild(vc, aMessage)) {
       // Forwarded succesfully to child cursor.
       return;
     }
 
+    if (adjustRange && this.adjustRange(vc.position, action === 'moveNext')) {
+      return;
+    }
+
     let moved = vc[action](TraversalRules[aMessage.json.rule]);
 
     if (moved) {
       if (origin === 'child') {
         // We just stepped out of a child, clear child cursor.
         Utils.getMessageManager(aMessage.target).sendAsyncMessage(
           'AccessFu:ClearCursor', {});
       } else {
@@ -117,20 +122,24 @@ this.ContentControl.prototype = {
         } else if (action === 'movePrevious') {
           childAction = 'moveLast';
         }
 
         // Attempt to forward move to a potential child cursor in our
         // new position.
         this.sendToChild(vc, aMessage, { action: childAction }, true);
       }
-    } else if (!this._childMessageSenders.has(aMessage.target)) {
-      // We failed to move, and the message is not from a child, so forward
-      // to parent.
+    } else if (!this._childMessageSenders.has(aMessage.target) &&
+               origin !== 'top') {
+      // We failed to move, and the message is not from a parent, so forward
+      // to it.
       this.sendToParent(aMessage);
+    } else {
+      this._contentScope.get().sendAsyncMessage('AccessFu:Present',
+        Presentation.noMove(action));
     }
   },
 
   handleEvent: function cc_handleEvent(aEvent) {
     if (aEvent.type === 'mousemove') {
       this.handleMoveToPoint(
         { json: { x: aEvent.screenX, y: aEvent.screenY, rule: 'Simple' } });
     }
@@ -164,17 +173,17 @@ this.ContentControl.prototype = {
 
   handleActivate: function cc_handleActivate(aMessage) {
     let activateAccessible = (aAccessible) => {
       Logger.debug(() => {
         return ['activateAccessible', Logger.accessibleToString(aAccessible)];
       });
       try {
         if (aMessage.json.activateIfKey &&
-          aAccessible.role != Roles.KEY) {
+          !Utils.isActivatableOnFingerUp(aAccessible)) {
           // Only activate keys, don't do anything on other objects.
           return;
         }
       } catch (e) {
         // accessible is invalid. Silently fail.
         return;
       }
 
@@ -205,17 +214,17 @@ this.ContentControl.prototype = {
         for (let eventType of ['mousedown', 'mouseup']) {
           let evt = this.document.createEvent('MouseEvents');
           evt.initMouseEvent(eventType, true, true, this.window,
             x, y, 0, 0, 0, false, false, false, false, 0, null);
           node.dispatchEvent(evt);
         }
       }
 
-      if (aAccessible.role !== Roles.KEY) {
+      if (!Utils.isActivatableOnFingerUp(aAccessible)) {
         // Keys will typically have a sound of their own.
         this._contentScope.get().sendAsyncMessage('AccessFu:Present',
           Presentation.actionInvoked(aAccessible, 'click'));
       }
     };
 
     let focusedAcc = Utils.AccRetrieval.getAccessibleFor(
       this.document.activeElement);
@@ -252,16 +261,47 @@ this.ContentControl.prototype = {
 
     let vc = this.vc;
     if (!this.sendToChild(vc, aMessage, null, true)) {
       let position = vc.position;
       activateAccessible(getActivatableDescendant(position) || position);
     }
   },
 
+  adjustRange: function cc_adjustRange(aAccessible, aStepUp) {
+    let acc = Utils.getEmbeddedControl(aAccessible) || aAccessible;
+    try {
+      acc.QueryInterface(Ci.nsIAccessibleValue);
+    } catch (x) {
+      // This is not an adjustable, return false.
+      return false;
+    }
+
+    let elem = acc.DOMNode;
+    if (!elem) {
+      return false;
+    }
+
+    if (elem.tagName === 'INPUT' && elem.type === 'range') {
+      elem[aStepUp ? 'stepDown' : 'stepUp']();
+      let evt = this.document.createEvent('UIEvent');
+      evt.initEvent('change', true, true);
+      elem.dispatchEvent(evt);
+    } else {
+      let evt = this.document.createEvent('KeyboardEvent');
+      let keycode = aStepUp ? content.KeyEvent.DOM_VK_DOWN :
+        content.KeyEvent.DOM_VK_UP;
+      evt.initKeyEvent(
+        "keypress", false, true, null, false, false, false, false, keycode, 0);
+      elem.dispatchEvent(evt);
+    }
+
+    return true;
+  },
+
   handleMoveByGranularity: function cc_handleMoveByGranularity(aMessage) {
     // XXX: Add sendToChild. Right now this is only used in Android, so no need.
     let direction = aMessage.json.direction;
     let granularity;
 
     switch(aMessage.json.granularity) {
       case MOVEMENT_GRANULARITY_CHARACTER:
         granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY;
--- a/accessible/jsat/Presentation.jsm
+++ b/accessible/jsat/Presentation.jsm
@@ -112,16 +112,21 @@ Presenter.prototype = {
   editingModeChanged: function editingModeChanged(aIsEditing) {}, // jshint ignore:line
 
   /**
    * Announce something. Typically an app state change.
    */
   announce: function announce(aAnnouncement) {}, // jshint ignore:line
 
 
+  /**
+   * User tried to move cursor forward or backward with no success.
+   * @param {string} aMoveMethod move method that was used (eg. 'moveNext').
+   */
+  noMove: function noMove(aMoveMethod) {},
 
   /**
    * Announce a live region.
    * @param  {PivotContext} aContext context object for an accessible.
    * @param  {boolean} aIsPolite A politeness level for a live region.
    * @param  {boolean} aIsHide An indicator of hide/remove event.
    * @param  {string} aModifiedText Optional modified text.
    */
@@ -477,17 +482,17 @@ B2GPresenter.prototype.pivotChanged =
 
     return {
       type: this.type,
       details: {
         eventType: 'vc-change',
         data: UtteranceGenerator.genForContext(aContext),
         options: {
           pattern: this.PIVOT_CHANGE_HAPTIC_PATTERN,
-          isKey: aContext.accessible.role === Roles.KEY,
+          isKey: Utils.isActivatableOnFingerUp(aContext.accessible),
           reason: this.pivotChangedReasons[aReason],
           isUserInput: aIsUserInput
         }
       }
     };
   };
 
 B2GPresenter.prototype.valueChanged =
@@ -531,16 +536,27 @@ B2GPresenter.prototype.announce =
       type: this.type,
       details: {
         eventType: 'announcement',
         data: aAnnouncement
       }
     };
   };
 
+B2GPresenter.prototype.noMove =
+  function B2GPresenter_noMove(aMoveMethod) {
+    return {
+      type: this.type,
+      details: {
+        eventType: 'no-move',
+        data: aMoveMethod
+      }
+    };
+  };
+
 /**
  * A braille presenter
  */
 function BraillePresenter() {}
 
 BraillePresenter.prototype = Object.create(Presenter.prototype);
 
 BraillePresenter.prototype.type = 'Braille';
@@ -631,16 +647,20 @@ this.Presentation = { // jshint ignore:l
 
   announce: function Presentation_announce(aAnnouncement) {
     // XXX: Typically each presenter uses the UtteranceGenerator,
     // but there really isn't a point here.
     return [p.announce(UtteranceGenerator.genForAnnouncement(aAnnouncement)) // jshint ignore:line
       for each (p in this.presenters)]; // jshint ignore:line
   },
 
+  noMove: function Presentation_noMove(aMoveMethod) {
+    return [p.noMove(aMoveMethod) for each (p in this.presenters)]; // jshint ignore:line
+  },
+
   liveRegion: function Presentation_liveRegion(aAccessible, aIsPolite, aIsHide,
     aModifiedText) {
     let context;
     if (!aModifiedText) {
       context = new PivotContext(aAccessible, null, -1, -1, true,
         aIsHide ? true : false);
     }
     return [p.liveRegion(context, aIsPolite, aIsHide, aModifiedText) // jshint ignore:line
--- a/accessible/jsat/TraversalRules.jsm
+++ b/accessible/jsat/TraversalRules.jsm
@@ -29,16 +29,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 let gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images');
 
 function BaseTraversalRule(aRoles, aMatchFunc, aPreFilter) {
   this._explicitMatchRoles = new Set(aRoles);
   this._matchRoles = aRoles;
   if (aRoles.indexOf(Roles.LABEL) < 0) {
     this._matchRoles.push(Roles.LABEL);
   }
+  if (aRoles.indexOf(Roles.INTERNAL_FRAME) < 0) {
+    // Used for traversing in to child OOP frames.
+    this._matchRoles.push(Roles.INTERNAL_FRAME);
+  }
   this._matchFunc = aMatchFunc || function() { return Filters.MATCH; };
   this.preFilter = aPreFilter || gSimplePreFilter;
 }
 
 BaseTraversalRule.prototype = {
     getMatchRoles: function BaseTraversalRule_getmatchRoles(aRules) {
       aRules.value = this._matchRoles;
       return aRules.value.length;
@@ -93,19 +97,17 @@ var gSimpleTraversalRoles =
    Roles.HEADER,
    Roles.HEADING,
    Roles.SLIDER,
    Roles.SPINBUTTON,
    Roles.OPTION,
    Roles.LISTITEM,
    Roles.GRID_CELL,
    Roles.COLUMNHEADER,
-   Roles.ROWHEADER,
-   // Used for traversing in to child OOP frames.
-   Roles.INTERNAL_FRAME];
+   Roles.ROWHEADER];
 
 var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) {
   // An object is simple, if it either has a single child lineage,
   // or has a flat subtree.
   function isSingleLineage(acc) {
     for (let child = acc; child; child = child.firstChild) {
       if (Utils.visibleChildCount(child) > 1) {
         return false;
--- a/accessible/jsat/Utils.jsm
+++ b/accessible/jsat/Utils.jsm
@@ -465,16 +465,24 @@ this.Utils = { // jshint ignore:line
       // emulator add-on.
       window.dispatchEvent(new window.CustomEvent(aType, {
         bubbles: true,
         cancelable: true,
         detail: details
       }));
     }
 
+  },
+
+  isActivatableOnFingerUp: function isActivatableOnFingerUp(aAccessible) {
+    if (aAccessible.role === Roles.KEY) {
+      return true;
+    }
+    let quick_activate = this.getAttributes(aAccessible)['moz-quick-activate'];
+    return quick_activate && JSON.parse(quick_activate);
   }
 };
 
 /**
  * State object used internally to process accessible's states.
  * @param {Number} aBase     Base state.
  * @param {Number} aExtended Extended state.
  */
@@ -944,17 +952,17 @@ this.PrefCache = function PrefCache(aNam
   this.name = aName;
   this.callback = aCallback;
 
   let branch = Services.prefs;
   this.value = this._getValue(branch);
 
   if (this.callback && aRunCallbackNow) {
     try {
-      this.callback(this.name, this.value);
+      this.callback(this.name, this.value, true);
     } catch (x) {
       Logger.logException(x);
     }
   }
 
   branch.addObserver(aName, this, true);
 };
 
@@ -977,19 +985,20 @@ PrefCache.prototype = {
     } catch (x) {
       // Pref does not exist.
       return null;
     }
   },
 
   observe: function observe(aSubject) {
     this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch));
+    Logger.info('pref changed', this.name, this.value);
     if (this.callback) {
       try {
-        this.callback(this.name, this.value);
+        this.callback(this.name, this.value, false);
       } catch (x) {
         Logger.logException(x);
       }
     }
   },
 
   QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
                                           Ci.nsISupportsWeakReference])
--- a/accessible/jsat/content-script.js
+++ b/accessible/jsat/content-script.js
@@ -87,56 +87,29 @@ function scroll(aMessage) {
   if (!forwardToChild(aMessage, scroll, position)) {
     sendAsyncMessage('AccessFu:DoScroll',
                      { bounds: Utils.getBounds(position, true),
                        page: aMessage.json.page,
                        horizontal: aMessage.json.horizontal });
   }
 }
 
-function adjustRange(aMessage) {
-  function sendUpDownKey(aAccessible) {
-    let acc = Utils.getEmbeddedControl(aAccessible) || aAccessible;
-    let elem = acc.DOMNode;
-    if (elem) {
-      if (elem.tagName === 'INPUT' && elem.type === 'range') {
-        elem[aMessage.json.direction === 'forward' ? 'stepDown' : 'stepUp']();
-        let changeEvent = content.document.createEvent('UIEvent');
-        changeEvent.initEvent('change', true, true);
-        elem.dispatchEvent(changeEvent);
-      } else {
-        let evt = content.document.createEvent('KeyboardEvent');
-        let keycode = aMessage.json.direction == 'forward' ?
-              content.KeyEvent.DOM_VK_DOWN : content.KeyEvent.DOM_VK_UP;
-        evt.initKeyEvent(
-          "keypress", false, true, null, false, false, false, false, keycode, 0);
-        elem.dispatchEvent(evt);
-      }
-    }
-  }
-
-  let position = Utils.getVirtualCursor(content.document).position;
-  if (!forwardToChild(aMessage, adjustRange, position)) {
-    sendUpDownKey(position);
-  }
-}
 addMessageListener(
   'AccessFu:Start',
   function(m) {
     if (m.json.logLevel) {
       Logger.logLevel = Logger[m.json.logLevel];
     }
 
     Logger.debug('AccessFu:Start');
     if (m.json.buildApp)
       Utils.MozBuildApp = m.json.buildApp;
 
     addMessageListener('AccessFu:ContextMenu', activateContextMenu);
     addMessageListener('AccessFu:Scroll', scroll);
-    addMessageListener('AccessFu:AdjustRange', adjustRange);
 
     if (!contentControl) {
       contentControl = new ContentControl(this);
     }
     contentControl.start();
 
     if (!eventManager) {
       eventManager = new EventManager(this, contentControl);
--- a/accessible/tests/mochitest/actions/test_treegrid.xul
+++ b/accessible/tests/mochitest/actions/test_treegrid.xul
@@ -150,18 +150,17 @@
     {
       var treeNode = getNode("tabletree");
       waitForEvent(EVENT_REORDER, treeNode, doTestActions);
       treeNode.view = new nsTreeTreeView();
     }
 
     function test1()
     {
-      var boxObj = getNode("tabletree").treeBoxObject;
-      boxObj.view.setCellValue(0, boxObj.columns.firstColumn, "false");
+      getNode("tabletree").view.setCellValue(0, boxObj.columns.firstColumn, "false");
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTest);
   ]]>
   </script>
 
   <hbox flex="1" style="overflow: auto;">
@@ -191,9 +190,8 @@
       </tree>
 
       <vbox id="debug"/>
       <button oncommand="test1();" label="uncheck"/>
     </vbox>
   </hbox>
 
 </window>
-
--- a/accessible/tests/mochitest/hittest/test_zoom_tree.xul
+++ b/accessible/tests/mochitest/hittest/test_zoom_tree.xul
@@ -35,26 +35,24 @@
       var treecol1 = tabDocument.getElementById("treecol1");
 
       // tree columns
       hitTest(tree, treecols, treecol1);
 
       // tree rows and cells
       var treeBoxObject = tree.treeBoxObject;
       var treeBodyBoxObj = tree.treeBoxObject.treeBody.boxObject;
-      var xObj = {}, yObj = {}, widthObj = {}, heightObj = {};
-      treeBoxObject.getCoordsForCellItem(1, tree.columns[0], "cell",
-                                         xObj, yObj, widthObj, heightObj);
+      var rect = treeBoxObject.getCoordsForCellItem(1, tree.columns[0], "cell");
 
       var treeAcc = getAccessible(tree, [nsIAccessibleTable]);
       var cellAcc = treeAcc.getCellAt(1, 0);
       var rowAcc = cellAcc.parent;
 
-      var cssX = xObj.value + treeBodyBoxObj.x;
-      var cssY = yObj.value + treeBodyBoxObj.y;
+      var cssX = rect.x + treeBodyBoxObj.x;
+      var cssY = rect.y + treeBodyBoxObj.y;
       var [x, y] = CSSToDevicePixels(tabWindow, cssX, cssY);
 
       testChildAtPoint(treeAcc, x, y, rowAcc, cellAcc);
       testChildAtPoint(rowAcc, x, y, cellAcc, cellAcc);
 
       // do zoom
       zoomDocument(tabDocument, 1.5);
 
@@ -95,9 +93,8 @@
       <div id="content" style="display: none">
       </div>
       <pre id="test">
       </pre>
     </body>
   </hbox>
 
 </window>
-
--- a/accessible/tests/mochitest/jsat/a11y.ini
+++ b/accessible/tests/mochitest/jsat/a11y.ini
@@ -13,11 +13,12 @@ support-files =
 skip-if = buildapp == 'mulet'
 [test_content_text.html]
 skip-if = buildapp == 'mulet'
 [test_explicit_names.html]
 [test_gesture_tracker.html]
 [test_landmarks.html]
 [test_live_regions.html]
 [test_output.html]
+[test_quicknav_modes.html]
 [test_tables.html]
 [test_pointer_relay.html]
 [test_traversal.html]
--- a/accessible/tests/mochitest/jsat/jsatcommon.js
+++ b/accessible/tests/mochitest/jsat/jsatcommon.js
@@ -130,17 +130,17 @@ var AccessFuTest = {
     } catch (ex) {
       // StopIteration exception.
       this.finish();
       return;
     }
     testFunc();
   },
 
-  runTests: function AccessFuTest_runTests() {
+  runTests: function AccessFuTest_runTests(aAdditionalPrefs) {
     if (gTestFuncs.length === 0) {
       ok(false, "No tests specified!");
       SimpleTest.finish();
       return;
     }
 
     // Create an Iterator for gTestFuncs array.
     gIterator = Iterator(gTestFuncs); // jshint ignore:line
@@ -151,20 +151,21 @@ var AccessFuTest = {
     AccessFu.attach(getMainChromeWindow(window));
 
     AccessFu.readyCallback = function readyCallback() {
       // Enable logging to the console service.
       Logger.test = true;
       Logger.logLevel = Logger.DEBUG;
     };
 
-    SpecialPowers.pushPrefEnv({
-      'set': [['accessibility.accessfu.notify_output', 1],
-              ['dom.mozSettings.enabled', true]]
-    }, function () {
+    var prefs = [['accessibility.accessfu.notify_output', 1],
+      ['dom.mozSettings.enabled', true]];
+    prefs.push.apply(prefs, aAdditionalPrefs);
+
+    SpecialPowers.pushPrefEnv({ 'set': prefs }, function () {
       if (AccessFuTest._waitForExplicitFinish) {
         // Run all test functions asynchronously.
         AccessFuTest.nextTest();
       } else {
         // Run all test functions synchronously.
         [testFunc() for (testFunc of gTestFuncs)]; // jshint ignore:line
         AccessFuTest.finish();
       }
@@ -359,29 +360,39 @@ var ContentMessages = {
 
   clearCursor: {
     name: 'AccessFu:ClearCursor',
     json: {
       origin: 'top'
     }
   },
 
-  adjustRangeUp: {
-    name: 'AccessFu:AdjustRange',
-    json: {
-      origin: 'top',
-      direction: 'backward'
+  moveOrAdjustUp: function moveOrAdjustUp(aRule) {
+    return {
+      name: 'AccessFu:MoveCursor',
+      json: {
+        origin: 'top',
+        action: 'movePrevious',
+        inputType: 'gesture',
+        rule: (aRule || 'Simple'),
+        adjustRange: true
+      }
     }
   },
 
-  adjustRangeDown: {
-    name: 'AccessFu:AdjustRange',
-    json: {
-      origin: 'top',
-      direction: 'forward'
+  moveOrAdjustDown: function moveOrAdjustUp(aRule) {
+    return {
+      name: 'AccessFu:MoveCursor',
+      json: {
+        origin: 'top',
+        action: 'moveNext',
+        inputType: 'gesture',
+        rule: (aRule || 'Simple'),
+        adjustRange: true
+      }
     }
   },
 
   focusSelector: function focusSelector(aSelector, aBlur) {
     return {
       name: 'AccessFuTest:Focus',
       json: {
         selector: aSelector,
@@ -649,16 +660,22 @@ function ExpectedAnnouncement(aAnnouncem
     eventType: AndroidEvent.ANNOUNCEMENT,
     text: [ aAnnouncement],
     addedCount: aAnnouncement.length
   }], aOptions);
 }
 
 ExpectedAnnouncement.prototype = Object.create(ExpectedPresent.prototype);
 
+function ExpectedNoMove(aOptions) {
+  ExpectedPresent.call(this, {eventType: 'no-move' }, null, aOptions);
+}
+
+ExpectedNoMove.prototype = Object.create(ExpectedPresent.prototype);
+
 var AndroidEvent = {
   VIEW_CLICKED: 0x01,
   VIEW_LONG_CLICKED: 0x02,
   VIEW_SELECTED: 0x04,
   VIEW_FOCUSED: 0x08,
   VIEW_TEXT_CHANGED: 0x10,
   WINDOW_STATE_CHANGED: 0x20,
   VIEW_HOVER_ENTER: 0x80,
--- a/accessible/tests/mochitest/jsat/test_content_integration.html
+++ b/accessible/tests/mochitest/jsat/test_content_integration.html
@@ -29,16 +29,17 @@
       iframe.addEventListener('mozbrowserloadend', function () {
       var contentTest = new AccessFuContentTest(
         [
           // Simple traversal forward
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(
             ['Phone status bar', 'Traversal Rule test document'],
             { focused: 'body' })],
+          [ContentMessages.simpleMovePrevious, new ExpectedNoMove()],
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
           [ContentMessages.simpleMoveNext, new ExpectedCursorChange(
             ['wow', {'string': 'headingLevel', 'args': [1]} ,'such app'],
             { focused: 'iframe' })],
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(['many option', {'string': 'stateNotChecked'},
             {'string': 'checkbutton'}, {'string': 'listStart'},
@@ -47,24 +48,24 @@
           // check checkbox
           [ContentMessages.activateCurrent(),
            new ExpectedClickAction({ no_android: true }),
            new ExpectedCheckAction(true, { android_todo: true })],
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(['much range', {'string': 'label'}])],
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(['much range', '5', {'string': 'slider'}])],
-          [ContentMessages.adjustRangeUp, new ExpectedValueChange('6')],
+          [ContentMessages.moveOrAdjustUp(), new ExpectedValueChange('6')],
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
 
           // Simple traversal backward
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['much range', '6', {'string': 'slider'}, 'such app'])],
-          [ContentMessages.adjustRangeDown, new ExpectedValueChange('5')],
+          [ContentMessages.moveOrAdjustDown(), new ExpectedValueChange('5')],
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['much range', {'string': 'label'}])],
           [ContentMessages.simpleMovePrevious,
            new ExpectedCursorChange(['many option', {'string': 'stateChecked'},
             {'string': 'checkbutton'}, {'string': 'listStart'},
             {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}])],
           // uncheck checkbox
           [ContentMessages.activateCurrent(),
@@ -86,16 +87,42 @@
             ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
           // Move from an inner frame to the last element in the parent doc
           [ContentMessages.simpleMoveLast,
             new ExpectedCursorChange(
               ['Home', {'string': 'pushbutton'}], { b2g_todo: true })],
 
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [ContentMessages.moveOrAdjustDown('FormElement'),
+           new ExpectedCursorChange(['Back', {"string": "pushbutton"}])],
+          [ContentMessages.moveOrAdjustDown('FormElement'),
+           new ExpectedCursorChange(['many option', {'string': 'stateNotChecked'},
+            {'string': 'checkbutton'}, {'string': 'listStart'},
+            {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}, 'such app'])],
+          [ContentMessages.moveOrAdjustDown('FormElement'),
+           new ExpectedCursorChange(['much range', '5', {'string': 'slider'}])],
+          // Calling AdjustOrMove should adjust the range.
+          [ContentMessages.moveOrAdjustDown('FormElement'),
+           new ExpectedValueChange('4')],
+          [ContentMessages.moveOrAdjustUp('FormElement'),
+           new ExpectedValueChange('5')],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(['much range', {'string': 'label'}])],
+          [ContentMessages.moveOrAdjustUp('FormElement'),
+           new ExpectedCursorChange(['many option', {'string': 'stateNotChecked'},
+            {'string': 'checkbutton'}, {'string': 'listStart'},
+            {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}])],
+          [ContentMessages.moveOrAdjustUp('FormElement'),
+           new ExpectedCursorChange(['Back', {"string": "pushbutton"}])],
+
+          [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
+
           // Moving to the absolute first item from an embedded document
           // fails. Bug 972035.
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
           [ContentMessages.simpleMoveNext,
            new ExpectedCursorChange(['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
@@ -220,17 +247,18 @@
           [doc.defaultView.showAlert,
            new ExpectedCursorChange(['This is an alert!',
             {'string': 'headingLevel', 'args': [1]}, {'string': 'dialog'}])],
 
           [function hideAlertAndFocusHomeButton() {
             doc.defaultView.hideAlert();
             doc.querySelector('button#home').focus();
           }, new ExpectedCursorChange(['Home', {'string': 'pushbutton'},
-            'Traversal Rule test document'])]
+            'Traversal Rule test document'])],
+          [ContentMessages.simpleMoveNext, new ExpectedNoMove()]
         ]);
 
         addA11yLoadEvent(function() {
           contentTest.start(function () {
             closeBrowserWindow();
             SimpleTest.finish();
           });
         }, doc.defaultView)
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_quicknav_modes.html
@@ -0,0 +1,103 @@
+<html>
+
+<head>
+  <title>AccessFu test for enabling</title>
+
+  <link rel="stylesheet" type="text/css"
+        href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript"
+          src="../common.js"></script>
+  <script type="application/javascript"
+          src="./jsatcommon.js"></script>
+  <script type="application/javascript">
+
+    function prefStart() {
+      // Start AccessFu via pref.
+      SpecialPowers.setIntPref("accessibility.accessfu.activate", 1);
+      AccessFuTest.once_log("EventManager.start", AccessFuTest.nextTest);
+    }
+
+    function nextMode(aCurrentMode, aNextMode) {
+      return function() {
+        is(AccessFu.Input.quickNavMode.current, aCurrentMode,
+          'initial current mode is correct');
+        AccessFu.Input.quickNavMode.next();
+        _expectMode(aNextMode, AccessFuTest.nextTest);
+      }
+    }
+
+    function prevMode(aCurrentMode, aNextMode) {
+      return function() {
+        is(AccessFu.Input.quickNavMode.current, aCurrentMode,
+          'initial current mode is correct');
+        AccessFu.Input.quickNavMode.previous();
+        _expectMode(aNextMode, AccessFuTest.nextTest);
+      }
+    }
+
+    function setMode(aModeIndex, aExpectedMode) {
+      return function() {
+        SpecialPowers.setIntPref(
+          'accessibility.accessfu.quicknav_index', aModeIndex);
+        _expectMode(aExpectedMode, AccessFuTest.nextTest);
+      }
+    }
+
+    function reconfigureModes() {
+      SpecialPowers.setCharPref('accessibility.accessfu.quicknav_modes',
+        'Landmark,Button,Entry,Graphic');
+      // When the modes are reconfigured, the current mode should
+      // be set to the first in the new list.
+      _expectMode('Landmark', AccessFuTest.nextTest);
+    }
+
+    function _expectMode(aExpectedMode, aCallback) {
+      if (AccessFu.Input.quickNavMode.current === aExpectedMode) {
+        ok(true, 'correct mode');
+        aCallback();
+      } else {
+        AccessFuTest.once_log('Quicknav mode: ' + aExpectedMode, function() {
+          ok(true, 'correct mode');
+          aCallback();
+        });
+      }
+    }
+
+    // Listen for initial 'EventManager.start' and disable AccessFu.
+    function prefStop() {
+      ok(AccessFu._enabled, "AccessFu was started via preference.");
+      AccessFuTest.once_log("EventManager.stop", AccessFuTest.finish);
+      SpecialPowers.setIntPref("accessibility.accessfu.activate", 0);
+    }
+
+    function doTest() {
+      AccessFuTest.addFunc(prefStart);
+      AccessFuTest.addFunc(nextMode('Link', 'Heading'));
+      AccessFuTest.addFunc(nextMode('Heading', 'FormElement'));
+      AccessFuTest.addFunc(nextMode('FormElement', 'Link'));
+      AccessFuTest.addFunc(nextMode('Link', 'Heading'));
+      AccessFuTest.addFunc(prevMode('Heading', 'Link'));
+      AccessFuTest.addFunc(prevMode('Link', 'FormElement'));
+      AccessFuTest.addFunc(setMode(1, 'Heading'));
+      AccessFuTest.addFunc(reconfigureModes);
+      AccessFuTest.addFunc(prefStop);
+      AccessFuTest.waitForExplicitFinish();
+      AccessFuTest.runTests([   // Will call SimpleTest.finish();
+        ['accessibility.accessfu.quicknav_modes', 'Link,Heading,FormElement']]);
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTest);
+  </script>
+
+</head>
+<body>
+  <a target="_blank"
+     href="https://bugzilla.mozilla.org/show_bug.cgi?id=811307"
+     title="[AccessFu] Add mochitest for enabling">
+    Mozilla Bug 811307
+  </a>
+</body>
+</html>
\ No newline at end of file
--- a/accessible/tests/mochitest/name/test_tree.xul
+++ b/accessible/tests/mochitest/name/test_tree.xul
@@ -27,17 +27,17 @@
   <script type="application/javascript">
   <![CDATA[
     function treeTester(aID)
     {
       this.DOMNode = getNode(aID);
 
       this.invoke = function treeTester_invoke()
       {
-        this.DOMNode.treeBoxObject.view = new nsTreeTreeView();
+        this.DOMNode.view = new nsTreeTreeView();
       }
 
       this.check = function treeTester_check(aEvent)
       {
         var tree = {
           role: ROLE_OUTLINE,
           children: [
             {
@@ -85,17 +85,17 @@
     }
 
     function tableTester(aID, aIsTable, aCol1ID, aCol2ID)
     {
       this.DOMNode = getNode(aID);
 
       this.invoke = function tableTester_invoke()
       {
-        this.DOMNode.treeBoxObject.view = new nsTableTreeView(2);
+        this.DOMNode.view = new nsTableTreeView(2);
       }
 
       this.check = function tableTester_check(aEvent)
       {
         var tree = {
           role: aIsTable ? ROLE_TABLE : ROLE_TREE_TABLE,
           children: [
             {
@@ -204,9 +204,8 @@
       <treecol id="tt_col2" flex="1" label="column 2"/>
     </treecols>
     <treechildren/>
   </tree>
 
   </vbox> <!-- close tests area -->
   </hbox> <!-- close main area -->
 </window>
-
--- a/accessible/tests/mochitest/selectable/test_tree.xul
+++ b/accessible/tests/mochitest/selectable/test_tree.xul
@@ -37,17 +37,17 @@
      * accessible.
      */
     function statesChecker(aTreeID, aView)
     {
       this.DOMNode = getNode(aTreeID);
       
       this.invoke = function invoke()
       {
-        this.DOMNode.treeBoxObject.view = aView;
+        this.DOMNode.view = aView;
       }
       this.check = function check()
       {
         var tree = getAccessible(this.DOMNode);
 
         var isTreeMultiSelectable = false;
         var seltype = this.DOMNode.getAttribute("seltype");
         if (seltype != "single" && seltype != "cell" && seltype != "text")
@@ -181,9 +181,8 @@
         <treechildren/>
       </tree>
 
       <vbox id="debug"/>
     </vbox>
   </hbox>
 
 </window>
-
--- a/accessible/tests/mochitest/states/test_tree.xul
+++ b/accessible/tests/mochitest/states/test_tree.xul
@@ -33,17 +33,17 @@
      * accessible.
      */
     function statesChecker(aTreeID, aView)
     {
       this.DOMNode = getNode(aTreeID);
 
       this.invoke = function statesChecker_invoke()
       {
-        this.DOMNode.treeBoxObject.view = aView;
+        this.DOMNode.view = aView;
       }
 
       this.check = function statesChecker_check()
       {
         var tree = getAccessible(this.DOMNode);
 
         // tree states
         testStates(tree, STATE_READONLY);
@@ -145,9 +145,8 @@
         <treechildren/>
       </tree>
 
       <vbox id="debug"/>
     </vbox>
   </hbox>
 
 </window>
-
--- a/accessible/tests/mochitest/tree/test_tree.xul
+++ b/accessible/tests/mochitest/tree/test_tree.xul
@@ -67,17 +67,17 @@
       var accTreeForTree = {
         role: aRole,
         children: [
           accTreeForColumns
         ]
       };
 
       var treeBoxObject = aTree.treeBoxObject;
-      var view = treeBoxObject.view;
+      var view = aTree.view;
       var columnCount = treeBoxObject.columns.count;
 
       for (var idx = 0; idx < columnCount; idx++)
         accTreeForColumns.children.push({ COLUMNHEADER: [ ] });
       if (!aTree.hasAttribute("hidecolumnpicker"))
         accTreeForColumns.children.push({ PUSHBUTTON: [ { MENUPOPUP: [] } ] });
 
       for (var idx = 0; idx < view.rowCount; idx++)
@@ -90,17 +90,17 @@
      * Event queue invoker object to test accessible tree for XUL tree element.
      */
     function treeChecker(aID, aView, aRole)
     {
       this.DOMNode = getNode(aID);
 
       this.invoke = function invoke()
       {
-        this.DOMNode.treeBoxObject.view = aView;
+        this.DOMNode.view = aView;
       }
       this.check = function check(aEvent)
       {
         testAccessibleTreeFor(this.DOMNode, aRole);
       }
       this.getID = function getID()
       {
         return "Tree testing of " + aID;
@@ -175,9 +175,8 @@
         <treechildren/>
       </tree>
 
       <vbox id="debug"/>
     </vbox>
   </hbox>
 
 </window>
-
--- a/accessible/tests/mochitest/treeview.js
+++ b/accessible/tests/mochitest/treeview.js
@@ -12,17 +12,17 @@ function loadXULTreeAndDoTest(aDoTestFun
     this.treeNode = getNode(aTreeID);
 
     this.eventSeq = [
       new invokerChecker(EVENT_REORDER, this.treeNode)
     ];
 
     this.invoke = function loadXULTree_invoke()
     {
-      this.treeNode.treeBoxObject.view = aTreeView;
+      this.treeNode.view = aTreeView;
     }
 
     this.getID = function loadXULTree_getID()
     {
       return "Load XUL tree " + prettyName(aTreeID);
     }
   }
 
--- a/accessible/xul/XULTreeAccessible.cpp
+++ b/accessible/xul/XULTreeAccessible.cpp
@@ -194,17 +194,17 @@ XULTreeAccessible::ChildAtPoint(int32_t 
 
   nsIntRect rootRect = rootFrame->GetScreenRect();
 
   int32_t clientX = presContext->DevPixelsToIntCSSPixels(aX) - rootRect.x;
   int32_t clientY = presContext->DevPixelsToIntCSSPixels(aY) - rootRect.y;
 
   int32_t row = -1;
   nsCOMPtr<nsITreeColumn> column;
-  nsAutoCString childEltUnused;
+  nsAutoString childEltUnused;
   mTree->GetCellAt(clientX, clientY, &row, getter_AddRefs(column),
                    childEltUnused);
 
   // If we failed to find tree cell for the given point then it might be
   // tree columns.
   if (row == -1 || !column)
     return AccessibleWrap::ChildAtPoint(aX, aY, aWhichChild);
 
@@ -741,17 +741,17 @@ XULTreeItemAccessibleBase::Bounds() cons
 
   nsCOMPtr<nsIBoxObject> boxObj = nsCoreUtils::GetTreeBodyBoxObject(mTree);
   if (!boxObj)
     return nsIntRect();
 
   nsCOMPtr<nsITreeColumn> column = nsCoreUtils::GetFirstSensibleColumn(mTree);
 
   int32_t x = 0, y = 0, width = 0, height = 0;
-  nsresult rv = mTree->GetCoordsForCellItem(mRow, column, EmptyCString(),
+  nsresult rv = mTree->GetCoordsForCellItem(mRow, column, EmptyString(),
                                             &x, &y, &width, &height);
   if (NS_FAILED(rv))
     return nsIntRect();
 
   boxObj->GetWidth(&width);
 
   int32_t tcX = 0, tcY = 0;
   boxObj->GetScreenX(&tcX);
@@ -991,25 +991,25 @@ XULTreeItemAccessibleBase::DispatchClick
 
   nsCOMPtr<nsITreeColumns> columns;
   mTree->GetColumns(getter_AddRefs(columns));
   if (!columns)
     return;
 
   // Get column and pseudo element.
   nsCOMPtr<nsITreeColumn> column;
-  nsAutoCString pseudoElm;
+  nsAutoString pseudoElm;
 
   if (aActionIndex == eAction_Click) {
     // Key column is visible and clickable.
     columns->GetKeyColumn(getter_AddRefs(column));
   } else {
     // Primary column contains a twisty we should click on.
     columns->GetPrimaryColumn(getter_AddRefs(column));
-    pseudoElm = NS_LITERAL_CSTRING("twisty");
+    pseudoElm = NS_LITERAL_STRING("twisty");
   }
 
   if (column)
     nsCoreUtils::DispatchClickEvent(mTree, mRow, column, pseudoElm);
 }
 
 Accessible*
 XULTreeItemAccessibleBase::GetSiblingAtOffset(int32_t aOffset,
@@ -1191,9 +1191,8 @@ XULTreeColumAccessible::GetSiblingAtOffs
         if (treeAcc)
           return treeAcc->GetTreeItemAccessible(aOffset - 1);
       }
     }
   }
 
   return nullptr;
 }
-
--- a/accessible/xul/XULTreeGridAccessible.cpp
+++ b/accessible/xul/XULTreeGridAccessible.cpp
@@ -336,17 +336,17 @@ XULTreeGridRowAccessible::ChildAtPoint(i
 
   nsIntRect rootRect = rootFrame->GetScreenRect();
 
   int32_t clientX = presContext->DevPixelsToIntCSSPixels(aX) - rootRect.x;
   int32_t clientY = presContext->DevPixelsToIntCSSPixels(aY) - rootRect.y;
 
   int32_t row = -1;
   nsCOMPtr<nsITreeColumn> column;
-  nsAutoCString childEltUnused;
+  nsAutoString childEltUnused;
   mTree->GetCellAt(clientX, clientY, &row, getter_AddRefs(column),
                    childEltUnused);
 
   // Return if we failed to find tree cell in the row for the given point.
   if (row != mRow || !column)
     return nullptr;
 
   return GetCellAccessible(column);
@@ -517,17 +517,17 @@ XULTreeGridCellAccessible::Bounds() cons
   // Get bounds for tree cell and add x and y of treechildren element to
   // x and y of the cell.
   nsCOMPtr<nsIBoxObject> boxObj = nsCoreUtils::GetTreeBodyBoxObject(mTree);
   if (!boxObj)
     return nsIntRect();
 
   int32_t x = 0, y = 0, width = 0, height = 0;
   nsresult rv = mTree->GetCoordsForCellItem(mRow, mColumn,
-                                            NS_LITERAL_CSTRING("cell"),
+                                            NS_LITERAL_STRING("cell"),
                                             &x, &y, &width, &height);
   if (NS_FAILED(rv))
     return nsIntRect();
 
   int32_t tcX = 0, tcY = 0;
   boxObj->GetScreenX(&tcX);
   boxObj->GetScreenY(&tcY);
   x += tcX;
--- a/aclocal.m4
+++ b/aclocal.m4
@@ -3,17 +3,16 @@ dnl Local autoconf macros used with mozi
 dnl The contents of this file are under the Public Domain.
 dnl
 
 builtin(include, build/autoconf/hotfixes.m4)dnl
 builtin(include, build/autoconf/acwinpaths.m4)dnl
 builtin(include, build/autoconf/hooks.m4)dnl
 builtin(include, build/autoconf/config.status.m4)dnl
 builtin(include, build/autoconf/toolchain.m4)dnl
-builtin(include, build/autoconf/ccache.m4)dnl
 builtin(include, build/autoconf/wrapper.m4)dnl
 builtin(include, build/autoconf/nspr.m4)dnl
 builtin(include, build/autoconf/nspr-build.m4)dnl
 builtin(include, build/autoconf/nss.m4)dnl
 builtin(include, build/autoconf/pkg.m4)dnl
 builtin(include, build/autoconf/codeset.m4)dnl
 builtin(include, build/autoconf/altoptions.m4)dnl
 builtin(include, build/autoconf/mozprog.m4)dnl
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -789,16 +789,18 @@ pref("dom.ipc.systemMessageCPULockTimeou
 pref("dom.disable_window_open_dialog_feature", true);
 
 // Enable before keyboard events and after keyboard events.
 pref("dom.beforeAfterKeyboardEvent.enabled", true);
 
 // Screen reader support
 pref("accessibility.accessfu.activate", 2);
 pref("accessibility.accessfu.quicknav_modes", "Link,Heading,FormElement,Landmark,ListItem");
+// Active quicknav mode, index value of list from quicknav_modes
+pref("accessibility.accessfu.quicknav_index", 0);
 // Setting for an utterance order (0 - description first, 1 - description last).
 pref("accessibility.accessfu.utterance", 1);
 // Whether to skip images with empty alt text
 pref("accessibility.accessfu.skip_empty_images", true);
 
 // Enable hit-target fluffing
 pref("ui.touch.radius.enabled", true);
 pref("ui.touch.radius.leftmm", 3);
--- a/b2g/chrome/content/settings.js
+++ b/b2g/chrome/content/settings.js
@@ -452,16 +452,26 @@ SettingsListener.observe("theme.selected
     Services.prefs.setCharPref('dom.mozApps.selected_theme', newTheme);
     Services.prefs.savePrefFile(null);
     Services.obs.notifyObservers(null, 'app-theme-changed', newTheme);
   }
 });
 
 // =================== Various simple mapping  ======================
 let settingsToObserve = {
+  'accessibility.screenreader_quicknav_modes': {
+    prefName: 'accessibility.accessfu.quicknav_modes',
+    resetToPref: true,
+    defaultValue: ''
+  },
+  'accessibility.screenreader_quicknav_index': {
+    prefName: 'accessibility.accessfu.quicknav_index',
+    resetToPref: true,
+    defaultValue: 0
+  },
   'app.update.channel': {
     resetToPref: true
   },
   'app.update.interval': 86400,
   'app.update.url': {
     resetToPref: true
   },
   'apz.force-enable': {
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -703,23 +703,22 @@ var BookmarksEventHandler = {
 
   fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) {
     var node;
     var cropped = false;
     var targetURI;
 
     if (aDocument.tooltipNode.localName == "treechildren") {
       var tree = aDocument.tooltipNode.parentNode;
-      var row = {}, column = {};
       var tbo = tree.treeBoxObject;
-      tbo.getCellAt(aEvent.clientX, aEvent.clientY, row, column, {});
-      if (row.value == -1)
+      var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
+      if (cell.row == -1)
         return false;
-      node = tree.view.nodeForTreeIndex(row.value);
-      cropped = tbo.isCellCropped(row.value, column.value);
+      node = tree.view.nodeForTreeIndex(cell.row);
+      cropped = tbo.isCellCropped(cell.row, cell.col);
     }
     else {
       // Check whether the tooltipNode is a Places node.
       // In such a case use it, otherwise check for targetURI attribute.
       var tooltipNode = aDocument.tooltipNode;
       if (tooltipNode._placesNode)
         node = tooltipNode._placesNode;
       else {
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -246,23 +246,29 @@ var gPluginHandler = {
       pluginData = new Map();
     }
 
     for (var pluginInfo of plugins) {
       if (pluginData.has(pluginInfo.permissionString)) {
         continue;
       }
 
-      let url;
-      // TODO: allow the blocklist to specify a better link, bug 873093
+      // If a block contains an infoURL, we should always prefer that to the default
+      // URL that we construct in-product, even for other blocklist types.
+      let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag);
+
       if (pluginInfo.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) {
-        url = Services.urlFormatter.formatURLPref("plugins.update.url");
+        if (!url) {
+          url = Services.urlFormatter.formatURLPref("plugins.update.url");
+        }
       }
       else if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
-        url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
+        if (!url) {
+          url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
+        }
       }
       else {
         url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "clicktoplay";
       }
       pluginInfo.detailsLink = url;
 
       pluginData.set(pluginInfo.permissionString, pluginInfo);
     }
--- a/browser/base/content/pageinfo/pageInfo.js
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -503,17 +503,17 @@ function makeGeneralTab()
     metaGroup.collapsed = true;
   else {
     var metaTagsCaption = document.getElementById("metaTagsCaption");
     if (length == 1)
       metaTagsCaption.label = gBundle.getString("generalMetaTag");
     else
       metaTagsCaption.label = gBundle.getFormattedString("generalMetaTags", [length]);
     var metaTree = document.getElementById("metatree");
-    metaTree.treeBoxObject.view = gMetaView;
+    metaTree.view = gMetaView;
 
     for (var i = 0; i < length; i++)
       gMetaView.addRow([metaNodes[i].name || metaNodes[i].httpEquiv, metaNodes[i].content]);
 
     metaGroup.collapsed = false;
   }
 
   // get the date of last modification
--- a/browser/base/content/sync/quota.js
+++ b/browser/base/content/sync/quota.js
@@ -156,20 +156,19 @@ let gUsageTreeView = {
 
   /*
    * Handle click events on the tree.
    */
   onTreeClick: function onTreeClick(event) {
     if (event.button == 2)
       return;
 
-    let row = {}, col = {};
-    this.treeBox.getCellAt(event.clientX, event.clientY, row, col, {});
-    if (col.value && col.value.id == "enabled")
-      this.toggle(row.value);
+    let cell = this.treeBox.getCellAt(event.clientX, event.clientY);
+    if (cell.col && cell.col.id == "enabled")
+      this.toggle(cell.row);
   },
 
   /*
    * Toggle enabled state of an engine.
    */
   toggle: function toggle(row) {
     // Update the tree
     let collection = this._collections[row];
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1175,41 +1175,43 @@
 
               oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
               if (this.isFindBarInitialized(oldTab)) {
                 let findBar = this.getFindBar(oldTab);
                 oldTab._findBarFocused = (!findBar.hidden &&
                   findBar._findField.getAttribute("focused") == "true");
               }
 
+              // If focus is in the tab bar, retain it there.
+              if (document.activeElement == oldTab) {
+                // We need to explicitly focus the new tab, because
+                // tabbox.xml does this only in some cases.
+                this.mCurrentTab.focus();
+              }
+
               if (!gMultiProcessBrowser)
-                this._adjustFocusAfterTabSwitch(this.mCurrentTab, oldTab);
+                this._adjustFocusAfterTabSwitch(this.mCurrentTab);
             }
 
             this.tabContainer._setPositionalAttributes();
 
             if (!aForceUpdate)
               TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
           ]]>
         </body>
       </method>
 
       <method name="_adjustFocusAfterTabSwitch">
         <parameter name="newTab"/>
-        <parameter name="oldTab"/>
         <body><![CDATA[
         let newBrowser = this.getBrowserForTab(newTab);
 
-        // When focus is in the tab bar, retain it there.
-        if (document.activeElement == oldTab) {
-          // We need to explicitly focus the new tab, because
-          // tabbox.xml does this only in some cases.
-          this.mCurrentTab.focus();
+        // Don't steal focus from the tab bar.
+        if (document.activeElement == newTab)
           return;
-        }
 
         // If there's a tabmodal prompt showing, focus it.
         if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
           let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
           let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
           let prompt = prompts[prompts.length - 1];
           prompt.Dialog.setDefaultFocus();
           return;
@@ -3314,17 +3316,17 @@
           }
         ]]></body>
       </method>
 
       <method name="_finalizeTabSwitch">
         <parameter name="toTab"/>
         <parameter name="fromTab"/>
         <body><![CDATA[
-          this._adjustFocusAfterTabSwitch(toTab, fromTab);
+          this._adjustFocusAfterTabSwitch(toTab);
           this._deactivateContent(fromTab);
 
           let toBrowser = this.getBrowserForTab(toTab);
           toBrowser.setAttribute("type", "content-primary");
 
           let fromBrowser = this.getBrowserForTab(fromTab);
           // It's possible that the tab we're switching from closed
           // before we were able to finalize, in which case, fromBrowser
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/plugins/blockPluginInfoURL.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1336406310000">
+  <emItems>
+  </emItems>
+  <pluginItems>
+    <pluginItem blockID="p9999">
+      <match name="filename" exp="libnptest\.so|nptest\.dll|Test\.plugin" />
+      <versionRange severity="2"></versionRange>
+      <infoURL>http://test.url.com/</infoURL>
+    </pluginItem>
+  </pluginItems>
+</blocklist>
--- a/browser/base/content/test/plugins/browser.ini
+++ b/browser/base/content/test/plugins/browser.ini
@@ -4,16 +4,17 @@
 # * Bug 921916 - no plugin events
 # * Bug XXXXX - no plugins in content processes ("Error: You cannot use the AddonManager in child processes!")
 # * Bug 866413 - PageInfo doesn't work in e10s [browser_pageInfo_plugins.js]
 # * Bug 921957 - remote webprogress doesn't supply originalURI attribute on the request object [browser_clearplugindata.js]
 skip-if = buildapp == "mulet" || e10s
 support-files =
   blockNoPlugins.xml
   blockPluginHard.xml
+  blockPluginInfoURL.xml
   blockPluginVulnerableNoUpdate.xml
   blockPluginVulnerableUpdatable.xml
   browser_clearplugindata.html
   browser_clearplugindata_noage.html
   head.js
   plugin_add_dynamically.html
   plugin_alternate_content.html
   plugin_big.html
--- a/browser/base/content/test/plugins/browser_pluginnotification.js
+++ b/browser/base/content/test/plugins/browser_pluginnotification.js
@@ -56,16 +56,19 @@ TabOpenListener.prototype = {
 
 function test() {
   waitForExplicitFinish();
   SimpleTest.requestCompleteLog();
   requestLongerTimeout(2);
   registerCleanupFunction(function() {
     clearAllPluginPermissions();
     Services.prefs.clearUserPref("extensions.blocklist.suppressUI");
+    return new Promise(resolve => {
+      setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", resolve);
+    });
   });
   Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true);
 
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY);
   setTestPluginEnabledState(Ci.nsIPluginTag.STATE_CLICKTOPLAY, "Second Test Plug-in");
 
   var newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
@@ -816,10 +819,51 @@ function test24d() {
   }, "Test 24d, plugin should be activated");
 }
 
 function test25() {
   let notification = PopupNotifications.getNotification("click-to-play-plugins");
   ok(notification, "Test 25: There should be a plugin notification even if the plugin was immediately removed");
   ok(notification.dismissed, "Test 25: The notification should be dismissed by default");
 
+  prepareTest26();
+}
+
+function prepareTest26() {
+  info("prepareTest26");
+  let plugin = getTestPlugin();
+  plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+  setAndUpdateBlocklist(gHttpTestRoot + "blockPluginInfoURL.xml",
+    function() {
+      info("prepareTest26 callback");
+      prepareTest(runAfterPluginBindingAttached(test26), gTestRoot + "plugin_test.html");
+  });
+}
+
+// Tests a page with a blocked plugin in it and make sure the
+// infoURL property from the blocklist file gets used.
+function test26() {
+  info("test26 - Test infoURL");
+  let notification = PopupNotifications.getNotification("click-to-play-plugins");
+
+  // Since the plugin notification is dismissed by default, reshow it.
+  notification.reshow();
+
+  let pluginNode = gTestBrowser.contentDocument.getElementById("test");
+  ok(pluginNode, "Test 26, Found plugin in page");
+  let objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent);
+  is(objLoadingContent.pluginFallbackType,
+     Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED,
+     "Test 26, plugin fallback type should be PLUGIN_BLOCKLISTED");
+
+  const testUrl = "http://test.url.com/";
+
+  let doc = gTestBrowser.contentDocument;
+  let firstPanelChild = PopupNotifications.panel.firstChild;
+
+  let infoLink = doc.getAnonymousElementByAttribute(
+    firstPanelChild, "anonid", "click-to-play-plugins-notification-link");
+
+  is(infoLink.href, testUrl,
+    "Test 26, the notification URL needs to match the infoURL from the blocklist file.");
+
   finishTest();
 }
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -1900,18 +1900,24 @@ CustomizeMode.prototype = {
     if (!aEvent.dataTransfer.mozTypesAt(0)) {
       return;
     }
 
     let draggedItemId =
       aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
 
     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
-    draggedWrapper.hidden = false;
-    draggedWrapper.removeAttribute("mousedown");
+
+    // DraggedWrapper might no longer available if a widget node is
+    // destroyed after starting (but before stopping) a drag.
+    if (draggedWrapper) {
+      draggedWrapper.hidden = false;
+      draggedWrapper.removeAttribute("mousedown");
+    }
+
     if (this._dragOverItem) {
       this._cancelDragActive(this._dragOverItem);
       this._dragOverItem = null;
     }
     this._updateToolbarCustomizationOutline(this.window);
     this._showPanelCustomizationPlaceholders();
     DragPositionManager.stop();
   },
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -62,16 +62,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
                                   "resource:///modules/DownloadsLogger.jsm");
 
 const nsIDM = Ci.nsIDownloadManager;
 
 const kDownloadsStringBundleUrl =
   "chrome://browser/locale/downloads/downloads.properties";
 
@@ -138,16 +140,23 @@ PrefObserver.register({
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsCommon
 
 /**
  * This object is exposed directly to the consumers of this JavaScript module,
  * and provides shared methods for all the instances of the user interface.
  */
 this.DownloadsCommon = {
+  /**
+   * Constants with the different types of unblock messages.
+   */
+  BLOCK_VERDICT_MALWARE: "Malware",
+  BLOCK_VERDICT_POTENTIALLY_UNWANTED: "PotentiallyUnwanted",
+  BLOCK_VERDICT_UNCOMMON: "Uncommon",
+
   log: function DC_log(...aMessageArgs) {
     delete this.log;
     this.log = function DC_log(...aMessageArgs) {
       if (!PrefObserver.debug) {
         return;
       }
       DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
     }
@@ -506,17 +515,79 @@ this.DownloadsCommon = {
           // If launch also fails (probably because it's not implemented), let
           // the OS handler try to open the parent.
           Cc["@mozilla.org/uriloader/external-protocol-service;1"]
             .getService(Ci.nsIExternalProtocolService)
             .loadUrl(NetUtil.newURI(parent));
         }
       }
     }
-  }
+  },
+
+  /**
+   * Displays an alert message box which asks the user if they want to
+   * unblock the downloaded file or not.
+   *
+   * @param aType
+   *        The type of malware the downloaded file contains.
+   * @param aOwnerWindow
+   *        The window with which this action is associated.
+   *
+   * @return True to unblock the file, false to keep the user safe and
+   *         cancel the operation.
+   */
+  confirmUnblockDownload: Task.async(function* DP_confirmUnblockDownload(aType, aOwnerWindow) {
+    let s = DownloadsCommon.strings;
+    let title = s.unblockHeader;
+    let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+                      (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
+    let type = "";
+    let message = s.unblockTip;
+    let okButton = s.unblockButtonContinue;
+    let cancelButton = s.unblockButtonCancel;
+
+    switch (aType) {
+      case this.BLOCK_VERDICT_MALWARE:
+        type = s.unblockTypeMalware;
+        break;
+      case this.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
+        type = s.unblockTypePotentiallyUnwanted;
+        break;
+      case this.BLOCK_VERDICT_UNCOMMON:
+        type = s.unblockTypeUncommon;
+        break;
+    }
+
+    if (type) {
+      message = type + "\n\n" + message;
+    }
+
+    Services.ww.registerNotification(function onOpen(subj, topic) {
+      if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+        // Make sure to listen for "DOMContentLoaded" because it is fired
+        // before the "load" event.
+        subj.addEventListener("DOMContentLoaded", function onLoad() {
+          subj.removeEventListener("DOMContentLoaded", onLoad);
+          if (subj.document.documentURI ==
+              "chrome://global/content/commonDialog.xul") {
+            Services.ww.unregisterNotification(onOpen);
+            let dialog = subj.document.getElementById("commonDialog");
+            if (dialog) {
+              // Change the dialog to use a warning icon.
+              dialog.classList.add("alert-dialog");
+            }
+          }
+        });
+      }
+    });
+
+    let rv = Services.prompt.confirmEx(aOwnerWindow, title, message, buttonFlags,
+                                       cancelButton, okButton, null, null, {});
+    return (rv == 1);
+  }),
 };
 
 /**
  * Returns true if we are executing on Windows Vista or a later version.
  */
 XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () {
   let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
   if (os != "WINNT") {
--- a/browser/components/downloads/test/browser/browser.ini
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -2,8 +2,9 @@
 support-files = head.js
 
 [browser_basic_functionality.js]
 skip-if = buildapp == "mulet" || e10s
 [browser_first_download_panel.js]
 skip-if = os == "linux" # Bug 949434
 [browser_overflow_anchor.js]
 skip-if = os == "linux" # Bug 952422
+[browser_confirm_unblock_download.js]
--- a/browser/components/downloads/test/browser/browser_basic_functionality.js
+++ b/browser/components/downloads/test/browser/browser_basic_functionality.js
@@ -1,57 +1,55 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+registerCleanupFunction(function*() {
+  yield task_resetState();
+});
+
 /**
  * Make sure the downloads panel can display items in the right order and
  * contains the expected data.
  */
-function test_task()
-{
+add_task(function* test_basic_functionality() {
   // Display one of each download state.
   const DownloadData = [
     { state: nsIDM.DOWNLOAD_NOTSTARTED },
     { state: nsIDM.DOWNLOAD_PAUSED },
     { state: nsIDM.DOWNLOAD_FINISHED },
     { state: nsIDM.DOWNLOAD_FAILED },
     { state: nsIDM.DOWNLOAD_CANCELED },
   ];
 
-  try {
-    // Wait for focus first
-    yield promiseFocus();
+  // Wait for focus first
+  yield promiseFocus();
 
-    // Ensure that state is reset in case previous tests didn't finish.
-    yield task_resetState();
+  // Ensure that state is reset in case previous tests didn't finish.
+  yield task_resetState();
 
-    // For testing purposes, show all the download items at once.
-    var originalCountLimit = DownloadsView.kItemCountLimit;
-    DownloadsView.kItemCountLimit = DownloadData.length;
-    registerCleanupFunction(function () {
-      DownloadsView.kItemCountLimit = originalCountLimit;
-    });
+  // For testing purposes, show all the download items at once.
+  var originalCountLimit = DownloadsView.kItemCountLimit;
+  DownloadsView.kItemCountLimit = DownloadData.length;
+  registerCleanupFunction(function () {
+    DownloadsView.kItemCountLimit = originalCountLimit;
+  });
 
-    // Populate the downloads database with the data required by this test.
-    yield task_addDownloads(DownloadData);
+  // Populate the downloads database with the data required by this test.
+  yield task_addDownloads(DownloadData);
 
-    // Open the user interface and wait for data to be fully loaded.
-    yield task_openPanel();
+  // Open the user interface and wait for data to be fully loaded.
+  yield task_openPanel();
 
-    // Test item data and count.  This also tests the ordering of the display.
-    let richlistbox = document.getElementById("downloadsListBox");
-/* disabled for failing intermittently (bug 767828)
+  // Test item data and count.  This also tests the ordering of the display.
+  let richlistbox = document.getElementById("downloadsListBox");
+  /* disabled for failing intermittently (bug 767828)
     is(richlistbox.children.length, DownloadData.length,
        "There is the correct number of richlistitems");
-*/
-    let itemCount = richlistbox.children.length;
-    for (let i = 0; i < itemCount; i++) {
-      let element = richlistbox.children[itemCount - i - 1];
-      let dataItem = new DownloadsViewItemController(element).dataItem;
-      is(dataItem.state, DownloadData[i].state, "Download states match up");
-    }
-  } finally {
-    // Clean up when the test finishes.
-    yield task_resetState();
+  */
+  let itemCount = richlistbox.children.length;
+  for (let i = 0; i < itemCount; i++) {
+    let element = richlistbox.children[itemCount - i - 1];
+    let dataItem = new DownloadsViewItemController(element).dataItem;
+    is(dataItem.state, DownloadData[i].state, "Download states match up");
   }
-}
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_confirm_unblock_download.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the dialog which allows the user to unblock a downloaded file.
+
+registerCleanupFunction(() => {});
+
+function addDialogOpenObserver(buttonAction) {
+  Services.ww.registerNotification(function onOpen(subj, topic, data) {
+    if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+      // The test listens for the "load" event which guarantees that the alert
+      // class has already been added (it is added when "DOMContentLoaded" is
+      // fired).
+      subj.addEventListener("load", function onLoad() {
+        subj.removeEventListener("load", onLoad);
+        if (subj.document.documentURI ==
+            "chrome://global/content/commonDialog.xul") {
+          Services.ww.unregisterNotification(onOpen);
+
+          let dialog = subj.document.getElementById("commonDialog");
+          ok(dialog.classList.contains("alert-dialog"),
+             "The dialog element should contain an alert class.");
+
+          let doc = subj.document.documentElement;
+          doc.getButton(buttonAction).click();
+        }
+      });
+    }
+  });
+}
+
+add_task(function* test_confirm_unblock_dialog_unblock() {
+  addDialogOpenObserver("cancel");
+  let result = yield DownloadsCommon.confirmUnblockDownload(DownloadsCommon.UNBLOCK_MALWARE,
+                                                            window);
+  ok(result, "Should return true when the user clicks on `Unblock` button.");
+});
+
+add_task(function* test_confirm_unblock_dialog_keep_safe() {
+  addDialogOpenObserver("accept");
+  let result = yield DownloadsCommon.confirmUnblockDownload(DownloadsCommon.UNBLOCK_MALWARE,
+                                                            window);
+  ok(!result, "Should return false when the user clicks on `Keep me safe` button.");
+});
--- a/browser/components/downloads/test/browser/browser_first_download_panel.js
+++ b/browser/components/downloads/test/browser/browser_first_download_panel.js
@@ -3,63 +3,55 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Make sure the downloads panel only opens automatically on the first
  * download it notices. All subsequent downloads, even across sessions, should
  * not open the panel automatically.
  */
-function test_task()
-{
+add_task(function* test_first_download_panel() {
   // Clear the download panel has shown preference first as this test is used to
   // verify this preference's behaviour.
-  let oldPrefValue = true;
-  try {
-    oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
-  } catch(ex) { }
+  let oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
   Services.prefs.setBoolPref("browser.download.panel.shown", false);
 
-  try {
-    // Ensure that state is reset in case previous tests didn't finish.
+  registerCleanupFunction(function*() {
+    // Clean up when the test finishes.
     yield task_resetState();
 
-    // With this set to false, we should automatically open the panel the first
-    // time a download is started.
-    DownloadsCommon.getData(window).panelHasShownBefore = false;
-
-    let promise = promisePanelOpened();
-    DownloadsCommon.getData(window)._notifyDownloadEvent("start");
-    yield promise;
-
-    // If we got here, that means the panel opened.
-    DownloadsPanel.hidePanel();
-
-    ok(DownloadsCommon.getData(window).panelHasShownBefore,
-       "Should have recorded that the panel was opened on a download.")
-
-    // Next, make sure that if we start another download, we don't open the
-    // panel automatically.
-    let originalOnPopupShown = DownloadsPanel.onPopupShown;
-    DownloadsPanel.onPopupShown = function () {
-      originalOnPopupShown.apply(this, arguments);
-      ok(false, "Should not have opened the downloads panel.");
-    };
-
-    try {
-      DownloadsCommon.getData(window)._notifyDownloadEvent("start");
-
-      // Wait 2 seconds to ensure that the panel does not open.
-      let deferTimeout = Promise.defer();
-      setTimeout(deferTimeout.resolve, 2000);
-      yield deferTimeout.promise;
-    } finally {
-      DownloadsPanel.onPopupShown = originalOnPopupShown;
-    }
-  } finally {
-    // Clean up when the test finishes.
-    yield task_resetState();
     // Set the preference instead of clearing it afterwards to ensure the
     // right value is used no matter what the default was. This ensures the
     // panel doesn't appear and affect other tests.
     Services.prefs.setBoolPref("browser.download.panel.shown", oldPrefValue);
-  }
-}
+  });
+
+  // Ensure that state is reset in case previous tests didn't finish.
+  yield task_resetState();
+
+  // With this set to false, we should automatically open the panel the first
+  // time a download is started.
+  DownloadsCommon.getData(window).panelHasShownBefore = false;
+
+  let promise = promisePanelOpened();
+  DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+  yield promise;
+
+  // If we got here, that means the panel opened.
+  DownloadsPanel.hidePanel();
+
+  ok(DownloadsCommon.getData(window).panelHasShownBefore,
+     "Should have recorded that the panel was opened on a download.")
+
+  // Next, make sure that if we start another download, we don't open the
+  // panel automatically.
+  let originalOnPopupShown = DownloadsPanel.onPopupShown;
+  DownloadsPanel.onPopupShown = function () {
+    originalOnPopupShown.apply(this, arguments);
+    ok(false, "Should not have opened the downloads panel.");
+  };
+
+  DownloadsCommon.getData(window)._notifyDownloadEvent("start");
+
+  // Wait 2 seconds to ensure that the panel does not open.
+  yield new Promise(resolve => setTimeout(resolve, 2000));
+  DownloadsPanel.onPopupShown = originalOnPopupShown;
+});
--- a/browser/components/downloads/test/browser/browser_overflow_anchor.js
+++ b/browser/components/downloads/test/browser/browser_overflow_anchor.js
@@ -1,74 +1,74 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+registerCleanupFunction(function*() {
+  // Clean up when the test finishes.
+  yield task_resetState();
+});
+
 /**
  * Make sure the downloads button and indicator overflows into the nav-bar
  * chevron properly, and then when those buttons are clicked in the overflow
  * panel that the downloads panel anchors to the chevron.
  */
-function test_task() {
-  try {
-    // Ensure that state is reset in case previous tests didn't finish.
-    yield task_resetState();
+add_task(function* test_overflow_anchor() {
+  // Ensure that state is reset in case previous tests didn't finish.
+  yield task_resetState();
 
-    // Record the original width of the window so we can put it back when
-    // this test finishes.
-    let oldWidth = window.outerWidth;
+  // Record the original width of the window so we can put it back when
+  // this test finishes.
+  let oldWidth = window.outerWidth;
 
-    // The downloads button should not be overflowed to begin with.
-    let button = CustomizableUI.getWidget("downloads-button")
-                               .forWindow(window);
-    ok(!button.overflowed, "Downloads button should not be overflowed.");
+  // The downloads button should not be overflowed to begin with.
+  let button = CustomizableUI.getWidget("downloads-button")
+                             .forWindow(window);
+  ok(!button.overflowed, "Downloads button should not be overflowed.");
 
-    // Hack - we lock the size of the default flex-y items in the nav-bar,
-    // namely, the URL and search inputs. That way we can resize the
-    // window without worrying about them flexing.
-    const kFlexyItems = ["urlbar-container", "search-container"];
-    registerCleanupFunction(() => unlockWidth(kFlexyItems));
-    lockWidth(kFlexyItems);
+  // Hack - we lock the size of the default flex-y items in the nav-bar,
+  // namely, the URL and search inputs. That way we can resize the
+  // window without worrying about them flexing.
+  const kFlexyItems = ["urlbar-container", "search-container"];
+  registerCleanupFunction(() => unlockWidth(kFlexyItems));
+  lockWidth(kFlexyItems);
 
-    // Resize the window to half of its original size. That should
-    // be enough to overflow the downloads button.
-    window.resizeTo(oldWidth / 2, window.outerHeight);
-    yield waitForOverflowed(button, true);
+  // Resize the window to half of its original size. That should
+  // be enough to overflow the downloads button.
+  window.resizeTo(oldWidth / 2, window.outerHeight);
+  yield waitForOverflowed(button, true);
 
-    let promise = promisePanelOpened();
-    button.node.doCommand();
-    yield promise;
-
-    let panel = DownloadsPanel.panel;
-    let chevron = document.getElementById("nav-bar-overflow-button");
-    is(panel.anchorNode, chevron, "Panel should be anchored to the chevron.");
+  let promise = promisePanelOpened();
+  button.node.doCommand();
+  yield promise;
 
-    DownloadsPanel.hidePanel();
+  let panel = DownloadsPanel.panel;
+  let chevron = document.getElementById("nav-bar-overflow-button");
+  is(panel.anchorNode, chevron, "Panel should be anchored to the chevron.");
 
-    // Unlock the widths on the flex-y items.
-    unlockWidth(kFlexyItems);
+  DownloadsPanel.hidePanel();
 
-    // Put the window back to its original dimensions.
-    window.resizeTo(oldWidth, window.outerHeight);
+  // Unlock the widths on the flex-y items.
+  unlockWidth(kFlexyItems);
 
-    // The downloads button should eventually be un-overflowed.
-    yield waitForOverflowed(button, false);
+  // Put the window back to its original dimensions.
+  window.resizeTo(oldWidth, window.outerHeight);
 
-    // Now try opening the panel again.
-    promise = promisePanelOpened();
-    button.node.doCommand();
-    yield promise;
+  // The downloads button should eventually be un-overflowed.
+  yield waitForOverflowed(button, false);
 
-    is(panel.anchorNode.id, "downloads-indicator-anchor");
+  // Now try opening the panel again.
+  promise = promisePanelOpened();
+  button.node.doCommand();
+  yield promise;
 
-    DownloadsPanel.hidePanel();
-  } finally {
-    // Clean up when the test finishes.
-    yield task_resetState();
-  }
-}
+  is(panel.anchorNode.id, "downloads-indicator-anchor");
+
+  DownloadsPanel.hidePanel();
+});
 
 /**
  * For some node IDs, finds the nodes and sets their min-width's to their
  * current width, preventing them from flex-shrinking.
  *
  * @param aItemIDs an array of item IDs to set min-width on.
  */
 function lockWidth(aItemIDs) {
--- a/browser/components/downloads/test/browser/head.js
+++ b/browser/components/downloads/test/browser/head.js
@@ -24,25 +24,16 @@ const nsIDM = Ci.nsIDownloadManager;
 
 let gTestTargetFile = FileUtils.getFile("TmpD", ["dm-ui-test.file"]);
 gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
 registerCleanupFunction(function () {
   gTestTargetFile.remove(false);
 });
 
 ////////////////////////////////////////////////////////////////////////////////
-//// Infrastructure
-
-function test()
-{
-  waitForExplicitFinish();
-  Task.spawn(test_task).then(null, ex => ok(false, ex)).then(finish);
-}
-
-////////////////////////////////////////////////////////////////////////////////
 //// Asynchronous support subroutines
 
 function promiseFocus()
 {
   let deferred = Promise.defer();
   waitForFocus(deferred.resolve);
   return deferred.promise;
 }
--- a/browser/components/feeds/FeedWriter.js
+++ b/browser/components/feeds/FeedWriter.js
@@ -853,17 +853,16 @@ FeedWriter.prototype = {
            * Application" item is being selected with the keyboard. We do this
            * by ignoring command events while the dropdown is closed (user
            * arrowing through the combobox), but handling them while the
            * combobox dropdown is open (user pressed enter when an item was
            * selected). If we don't show the filepicker here, it will be shown
            * when clicking "Subscribe Now".
            */
           var popupbox = this._handlersMenuList.firstChild.boxObject;
-          popupbox.QueryInterface(Components.interfaces.nsIPopupBoxObject);
           if (popupbox.popupState == "hiding") {
             this._chooseClientApp(function(aResult) {
               if (!aResult) {
                 // Select the (per-prefs) selected handler if no application
                 // was selected
                 this._setSelectedHandler(this._getFeedType());
               }
             }.bind(this));
--- a/browser/components/loop/GoogleImporter.jsm
+++ b/browser/components/loop/GoogleImporter.jsm
@@ -446,17 +446,17 @@ this.GoogleImporter.prototype = {
     // Process telephone numbers.
     let phoneNodes = entry.getElementsByTagNameNS(kNS_GD, "phoneNumber");
     if (phoneNodes.length) {
       contact.tel = [];
       for (let [,phoneNode] of Iterator(phoneNodes)) {
         contact.tel.push({
           pref: (phoneNode.getAttribute("primary") == "true"),
           type: [getFieldType(phoneNode)],
-          value: phoneNode.firstChild.nodeValue
+          value: phoneNode.getAttribute("uri").replace("tel:", "")
         });
       }
     }
 
     let orgNodes = entry.getElementsByTagNameNS(kNS_GD, "organization");
     if (orgNodes.length) {
       contact.org = [];
       contact.jobTitle = [];
@@ -496,17 +496,17 @@ this.GoogleImporter.prototype = {
           try {
             email = getPreferred(contact);
           } catch (ex) {}
           if (email) {
             contact.name = [email.value];
           } else {
             let tel;
             try {
-              tel = getPreferred(contact, "phone");
+              tel = getPreferred(contact, "tel");
             } catch (ex) {}
             if (tel) {
               contact.name = [tel.value];
             }
           }
         }
       }
     }
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -5,18 +5,21 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/loop/MozLoopService.jsm");
-Cu.import("resource:///modules/loop/LoopContacts.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
+                                        "resource:///modules/loop/LoopContacts.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
+                                        "resource:///modules/loop/LoopStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
                                         "resource://gre/modules/MozSocialAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                         "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
   return Cc["@mozilla.org/xre/app-info;1"]
            .getService(Ci.nsIXULAppInfo)
            .QueryInterface(Ci.nsIXULRuntime);
@@ -226,16 +229,22 @@ function injectLoopAPI(targetWindow) {
      * @returns {Object} The contacts API object
      */
     contacts: {
       enumerable: true,
       get: function() {
         if (contactsAPI) {
           return contactsAPI;
         }
+
+        // Make a database switch when a userProfile is active already.
+        let profile = MozLoopService.userProfile;
+        if (profile) {
+          LoopStorage.switchDatabase(profile.uid);
+        }
         return contactsAPI = injectObjectAPI(LoopContacts, targetWindow);
       }
     },
 
     /**
      * Import a list of (new) contacts from an external data source.
      *
      * @param {Object}   options  Property bag of options for the importer
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -30,15 +30,17 @@
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
+    <script type="text/javascript" src="loop/shared/js/localRoomStore.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
+    <script type="text/javascript" src="loop/js/roomViews.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
   </body>
 </html>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -9,18 +9,21 @@
 
 var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
+  var sharedActions = loop.shared.actions;
+
   var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
   var CallIdentifierView = loop.conversationViews.CallIdentifierView;
+  var EmptyRoomView = loop.roomViews.EmptyRoomView;
 
   var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
     mixins: [sharedMixins.DropdownMenuMixin],
 
     propTypes: {
       model: React.PropTypes.object.isRequired,
       video: React.PropTypes.bool.isRequired
     },
@@ -475,40 +478,52 @@ loop.conversation = (function(mozL10n) {
       console.error("Failed initiating the call session.");
     },
   });
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
-  var ConversationControllerView = React.createClass({displayName: 'ConversationControllerView',
+  var AppControllerView = React.createClass({displayName: 'AppControllerView',
     propTypes: {
       // XXX Old types required for incoming call view.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for OutgoingConversationView
       store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+
+      // if not passed, this is not a room view
+      localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
     },
 
     getInitialState: function() {
       return this.props.store.attributes;
     },
 
     componentWillMount: function() {
       this.props.store.on("change:outgoing", function() {
         this.setState(this.props.store.attributes);
       }, this);
     },
 
     render: function() {
+      if (this.props.localRoomStore) {
+        return (
+          EmptyRoomView({
+            mozLoop: navigator.mozLoop, 
+            localRoomStore: this.props.localRoomStore}
+          )
+        );
+      }
+
       // Don't display anything, until we know what type of call we are.
       if (this.state.outgoing === undefined) {
         return null;
       }
 
       if (this.state.outgoing) {
         return (OutgoingConversationView({
           store: this.props.store, 
@@ -564,53 +579,72 @@ loop.conversation = (function(mozL10n) {
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the callId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationHash();
     var callId;
     var outgoing;
+    var localRoomStore;
 
-    var hash = locationHash.match(/\#incoming\/(.*)/);
+    // XXX removeMe, along with noisy comment at the beginning of
+    // conversation_test.js "when locationHash begins with #room".
+    if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
+      locationHash = "#room/32";
+    }
+
+    var hash = locationHash.match(/#incoming\/(.*)/);
     if (hash) {
       callId = hash[1];
       outgoing = false;
+    } else if (hash = locationHash.match(/#room\/(.*)/)) {
+      localRoomStore = new loop.store.LocalRoomStore({
+        dispatcher: dispatcher,
+        mozLoop: navigator.mozLoop
+      });
     } else {
-      hash = locationHash.match(/\#outgoing\/(.*)/);
+      hash = locationHash.match(/#outgoing\/(.*)/);
       if (hash) {
         callId = hash[1];
         outgoing = true;
       }
     }
 
     conversation.set({callId: callId});
 
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
       navigator.mozLoop.releaseCallData(callId);
     });
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
-    React.renderComponent(ConversationControllerView({
+    React.renderComponent(AppControllerView({
+      localRoomStore: localRoomStore, 
       store: conversationStore, 
       client: client, 
       conversation: conversation, 
       dispatcher: dispatcher, 
       sdk: window.OT}
     ), document.querySelector('#main'));
 
+   if (localRoomStore) {
+      dispatcher.dispatch(
+        new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
+      return;
+    }
+
     dispatcher.dispatch(new loop.shared.actions.GatherCallData({
       callId: callId,
       outgoing: outgoing
     }));
   }
 
   return {
-    ConversationControllerView: ConversationControllerView,
+    AppControllerView: AppControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
     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
@@ -9,18 +9,21 @@
 
 var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
+  var sharedActions = loop.shared.actions;
+
   var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
   var CallIdentifierView = loop.conversationViews.CallIdentifierView;
+  var EmptyRoomView = loop.roomViews.EmptyRoomView;
 
   var IncomingCallView = React.createClass({
     mixins: [sharedMixins.DropdownMenuMixin],
 
     propTypes: {
       model: React.PropTypes.object.isRequired,
       video: React.PropTypes.bool.isRequired
     },
@@ -475,40 +478,52 @@ loop.conversation = (function(mozL10n) {
       console.error("Failed initiating the call session.");
     },
   });
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
-  var ConversationControllerView = React.createClass({
+  var AppControllerView = React.createClass({
     propTypes: {
       // XXX Old types required for incoming call view.
       client: React.PropTypes.instanceOf(loop.Client).isRequired,
       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
                          .isRequired,
       sdk: React.PropTypes.object.isRequired,
 
       // XXX New types for OutgoingConversationView
       store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
-      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+
+      // if not passed, this is not a room view
+      localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
     },
 
     getInitialState: function() {
       return this.props.store.attributes;
     },
 
     componentWillMount: function() {
       this.props.store.on("change:outgoing", function() {
         this.setState(this.props.store.attributes);
       }, this);
     },
 
     render: function() {
+      if (this.props.localRoomStore) {
+        return (
+          <EmptyRoomView
+            mozLoop={navigator.mozLoop}
+            localRoomStore={this.props.localRoomStore}
+          />
+        );
+      }
+
       // Don't display anything, until we know what type of call we are.
       if (this.state.outgoing === undefined) {
         return null;
       }
 
       if (this.state.outgoing) {
         return (<OutgoingConversationView
           store={this.props.store}
@@ -564,53 +579,72 @@ loop.conversation = (function(mozL10n) {
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the callId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationHash();
     var callId;
     var outgoing;
+    var localRoomStore;
 
-    var hash = locationHash.match(/\#incoming\/(.*)/);
+    // XXX removeMe, along with noisy comment at the beginning of
+    // conversation_test.js "when locationHash begins with #room".
+    if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
+      locationHash = "#room/32";
+    }
+
+    var hash = locationHash.match(/#incoming\/(.*)/);
     if (hash) {
       callId = hash[1];
       outgoing = false;
+    } else if (hash = locationHash.match(/#room\/(.*)/)) {
+      localRoomStore = new loop.store.LocalRoomStore({
+        dispatcher: dispatcher,
+        mozLoop: navigator.mozLoop
+      });
     } else {
-      hash = locationHash.match(/\#outgoing\/(.*)/);
+      hash = locationHash.match(/#outgoing\/(.*)/);
       if (hash) {
         callId = hash[1];
         outgoing = true;
       }
     }
 
     conversation.set({callId: callId});
 
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
       navigator.mozLoop.releaseCallData(callId);
     });
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
-    React.renderComponent(<ConversationControllerView
+    React.renderComponent(<AppControllerView
+      localRoomStore={localRoomStore}
       store={conversationStore}
       client={client}
       conversation={conversation}
       dispatcher={dispatcher}
       sdk={window.OT}
     />, document.querySelector('#main'));
 
+   if (localRoomStore) {
+      dispatcher.dispatch(
+        new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
+      return;
+    }
+
     dispatcher.dispatch(new loop.shared.actions.GatherCallData({
       callId: callId,
       outgoing: outgoing
     }));
   }
 
   return {
-    ConversationControllerView: ConversationControllerView,
+    AppControllerView: AppControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
     init: init
   };
 })(document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -279,19 +279,21 @@ loop.conversationViews = (function(mozL1
       // height set to 100%" to fix video layout on Google Chrome
       // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
       return {
         insertMode: "append",
         width: "100%",
         height: "100%",
         publishVideo: this.props.video.enabled,
         style: {
+          audioLevelDisplayMode: "off",
           bugDisplayMode: "off",
           buttonDisplayMode: "off",
-          nameDisplayMode: "off"
+          nameDisplayMode: "off",
+          videoDisabledDisplayMode: "off"
         }
       }
     },
 
     /**
      * Used to update the video container whenever the orientation or size of the
      * display area changes.
      */
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -279,19 +279,21 @@ loop.conversationViews = (function(mozL1
       // height set to 100%" to fix video layout on Google Chrome
       // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
       return {
         insertMode: "append",
         width: "100%",
         height: "100%",
         publishVideo: this.props.video.enabled,
         style: {
+          audioLevelDisplayMode: "off",
           bugDisplayMode: "off",
           buttonDisplayMode: "off",
-          nameDisplayMode: "off"
+          nameDisplayMode: "off",
+          videoDisabledDisplayMode: "off"
         }
       }
     },
 
     /**
      * Used to update the video container whenever the orientation or size of the
      * display area changes.
      */
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/roomViews.js
@@ -0,0 +1,109 @@
+/** @jsx React.DOM */
+
+/* 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/. */
+
+/* global loop:true, React */
+
+var loop = loop || {};
+loop.roomViews = (function(mozL10n) {
+  "use strict";
+
+  /**
+   * Root object, by default set to window.
+   * @type {DOMWindow|Object}
+   */
+  var rootObject = window;
+
+  /**
+   * Sets a new root object. This is useful for testing native DOM events so we
+   * can fake them.
+   *
+   * @param {Object}
+   */
+  function setRootObject(obj) {
+    rootObject = obj;
+  }
+
+  var EmptyRoomView = React.createClass({displayName: 'EmptyRoomView',
+    mixins: [Backbone.Events],
+
+    propTypes: {
+      mozLoop:
+        React.PropTypes.object.isRequired,
+      localRoomStore:
+        React.PropTypes.instanceOf(loop.store.LocalRoomStore).isRequired,
+    },
+
+    getInitialState: function() {
+      return this.props.localRoomStore.getStoreState();
+    },
+
+    componentWillMount: function() {
+      this.listenTo(this.props.localRoomStore, "change",
+        this._onLocalRoomStoreChanged);
+    },
+
+    componentDidMount: function() {
+      // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+      // once the addCallback stuff lands
+      if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.addCallback) {
+        this.props.mozLoop.rooms.addCallback(
+          this.state.localRoomId,
+          "RoomCreationError", this.onCreationError);
+      }
+    },
+
+    /**
+     * Attached to the "RoomCreationError" with mozLoop.rooms.addCallback,
+     * which is fired mozLoop.rooms.createRoom from the panel encounters an
+     * error while attempting to create the room for this view.
+     *
+     * @param {Error} err - JS Error object with info about the problem
+     */
+    onCreationError: function(err) {
+      // XXX put up a user friendly error instead of this
+      rootObject.console.error("EmptyRoomView creation error: ", err);
+    },
+
+    /**
+     * Handles a "change" event on the localRoomStore, and updates this.state
+     * to match the store.
+     *
+     * @private
+     */
+    _onLocalRoomStoreChanged: function() {
+      this.setState(this.props.localRoomStore.getStoreState());
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.localRoomStore);
+
+      // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+      // once the addCallback stuff lands
+      if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.removeCallback) {
+        this.props.mozLoop.rooms.removeCallback(
+          this.state.localRoomId,
+          "RoomCreationError", this.onCreationError);
+      }
+    },
+
+    render: function() {
+      // XXX switch this to use the document title mixin once bug 1081079 lands
+      if (this.state.serverData && this.state.serverData.roomName) {
+        rootObject.document.title = this.state.serverData.roomName;
+      }
+
+      return (
+        React.DOM.div({className: "goat"})
+      );
+    }
+  });
+
+  return {
+    setRootObject: setRootObject,
+    EmptyRoomView: EmptyRoomView
+  };
+
+})(document.mozL10n || navigator.mozL10n);;
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -0,0 +1,109 @@
+/** @jsx React.DOM */
+
+/* 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/. */
+
+/* global loop:true, React */
+
+var loop = loop || {};
+loop.roomViews = (function(mozL10n) {
+  "use strict";
+
+  /**
+   * Root object, by default set to window.
+   * @type {DOMWindow|Object}
+   */
+  var rootObject = window;
+
+  /**
+   * Sets a new root object. This is useful for testing native DOM events so we
+   * can fake them.
+   *
+   * @param {Object}
+   */
+  function setRootObject(obj) {
+    rootObject = obj;
+  }
+
+  var EmptyRoomView = React.createClass({
+    mixins: [Backbone.Events],
+
+    propTypes: {
+      mozLoop:
+        React.PropTypes.object.isRequired,
+      localRoomStore:
+        React.PropTypes.instanceOf(loop.store.LocalRoomStore).isRequired,
+    },
+
+    getInitialState: function() {
+      return this.props.localRoomStore.getStoreState();
+    },
+
+    componentWillMount: function() {
+      this.listenTo(this.props.localRoomStore, "change",
+        this._onLocalRoomStoreChanged);
+    },
+
+    componentDidMount: function() {
+      // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+      // once the addCallback stuff lands
+      if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.addCallback) {
+        this.props.mozLoop.rooms.addCallback(
+          this.state.localRoomId,
+          "RoomCreationError", this.onCreationError);
+      }
+    },
+
+    /**
+     * Attached to the "RoomCreationError" with mozLoop.rooms.addCallback,
+     * which is fired mozLoop.rooms.createRoom from the panel encounters an
+     * error while attempting to create the room for this view.
+     *
+     * @param {Error} err - JS Error object with info about the problem
+     */
+    onCreationError: function(err) {
+      // XXX put up a user friendly error instead of this
+      rootObject.console.error("EmptyRoomView creation error: ", err);
+    },
+
+    /**
+     * Handles a "change" event on the localRoomStore, and updates this.state
+     * to match the store.
+     *
+     * @private
+     */
+    _onLocalRoomStoreChanged: function() {
+      this.setState(this.props.localRoomStore.getStoreState());
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.localRoomStore);
+
+      // XXXremoveMe (just the conditional itself) in patch 2 for bug 1074686,
+      // once the addCallback stuff lands
+      if (this.props.mozLoop.rooms && this.props.mozLoop.rooms.removeCallback) {
+        this.props.mozLoop.rooms.removeCallback(
+          this.state.localRoomId,
+          "RoomCreationError", this.onCreationError);
+      }
+    },
+
+    render: function() {
+      // XXX switch this to use the document title mixin once bug 1081079 lands
+      if (this.state.serverData && this.state.serverData.roomName) {
+        rootObject.document.title = this.state.serverData.roomName;
+      }
+
+      return (
+        <div className="goat"/>
+      );
+    }
+  });
+
+  return {
+    setRootObject: setRootObject,
+    EmptyRoomView: EmptyRoomView
+  };
+
+})(document.mozL10n || navigator.mozL10n);;
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -120,11 +120,21 @@ loop.shared.actions = (function() {
       enabled: Boolean
     }),
 
     /**
      * Retrieves room list.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     GetAllRooms: Action.define("getAllRooms", {
-    })
+    }),
+
+    /**
+     * Primes localRoomStore with roomLocalId, which triggers the EmptyRoomView
+     * to do any necessary setup.
+     *
+     * XXX should move to localRoomActions module
+     */
+    SetupEmptyRoom: Action.define("setupEmptyRoom", {
+      localRoomId: String
+    }),
   };
 })();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/localRoomStore.js
@@ -0,0 +1,113 @@
+/* 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/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.store = loop.store || {};
+loop.store.LocalRoomStore = (function() {
+  "use strict";
+
+  var sharedActions = loop.shared.actions;
+
+  /**
+   * Store for things that are local to this instance (in this profile, on
+   * this machine) of this roomRoom store, in addition to a mirror of some
+   * remote-state.
+   *
+   * @extends {Backbone.Events}
+   *
+   * @param {Object}          options - Options object
+   * @param {loop.Dispatcher} options.dispatch - The dispatcher for dispatching
+   *                            actions and registering to consume them.
+   * @param {MozLoop}         options.mozLoop - MozLoop API provider object
+   */
+  function LocalRoomStore(options) {
+    options = options || {};
+
+    if (!options.dispatcher) {
+      throw new Error("Missing option dispatcher");
+    }
+    this.dispatcher = options.dispatcher;
+
+    if (!options.mozLoop) {
+      throw new Error("Missing option mozLoop");
+    }
+    this.mozLoop = options.mozLoop;
+
+    this.dispatcher.register(this, ["setupEmptyRoom"]);
+  }
+
+  LocalRoomStore.prototype = _.extend({
+
+    /**
+     * Stored data reflecting the local state of a given room, used to drive
+     * the room's views.
+     *
+     * @property {Object} serverData - local cache of the data returned by
+     *                                 MozLoop.getRoomData for this room.
+     * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
+     *
+     * @property {Error=} error - if the room is an error state, this will be
+     *                            set to an Error object reflecting the problem;
+     *                            otherwise it will be unset.
+     *
+     * @property {String} localRoomId - profile-local identifier used with
+     *                                  the MozLoop API.
+     */
+    _storeState: {
+    },
+
+    getStoreState: function() {
+      return this._storeState;
+    },
+
+    setStoreState: function(state) {
+      this._storeState = state;
+      this.trigger("change");
+    },
+
+    /**
+     * Proxy to mozLoop.rooms.getRoomData for setupEmptyRoom action.
+     *
+     * XXXremoveMe Can probably be removed when bug 1074664 lands.
+     *
+     * @param {sharedActions.setupEmptyRoom} actionData
+     * @param {Function} cb Callback(error, roomData)
+     */
+    _fetchRoomData: function(actionData, cb) {
+      if (this.mozLoop.rooms && this.mozLoop.rooms.getRoomData) {
+        this.mozLoop.rooms.getRoomData(actionData.localRoomId, cb);
+      } else {
+        cb(null, {roomName: "Donkeys"});
+      }
+    },
+
+    /**
+     * Execute setupEmptyRoom event action from the dispatcher.  This primes
+     * the store with the localRoomId, and calls MozLoop.getRoomData on that
+     * ID.  This will return either a reflection of state on the server, or,
+     * if the createRoom call hasn't yet returned, it will have at least the
+     * roomName as specified to the createRoom method.
+     *
+     * When the room name gets set, that will trigger the view to display
+     * that name.
+     *
+     * @param {sharedActions.setupEmptyRoom} actionData
+     */
+    setupEmptyRoom: function(actionData) {
+      this._fetchRoomData(actionData, function(error, roomData) {
+        this.setStoreState({
+          error: error,
+          localRoomId: actionData.localRoomId,
+          serverData: roomData
+        });
+      }.bind(this));
+    }
+
+  }, Backbone.Events);
+
+  return LocalRoomStore;
+
+})();
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -146,19 +146,21 @@ loop.shared.views = (function(_, OT, l10
 
     // height set to 100%" to fix video layout on Google Chrome
     // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
     publisherConfig: {
       insertMode: "append",
       width: "100%",
       height: "100%",
       style: {
+        audioLevelDisplayMode: "off",
         bugDisplayMode: "off",
         buttonDisplayMode: "off",
-        nameDisplayMode: "off"
+        nameDisplayMode: "off",
+        videoDisabledDisplayMode: "off"
       }
     },
 
     getDefaultProps: function() {
       return {
         initiate: true,
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -146,19 +146,21 @@ loop.shared.views = (function(_, OT, l10
 
     // height set to 100%" to fix video layout on Google Chrome
     // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
     publisherConfig: {
       insertMode: "append",
       width: "100%",
       height: "100%",
       style: {
+        audioLevelDisplayMode: "off",
         bugDisplayMode: "off",
         buttonDisplayMode: "off",
-        nameDisplayMode: "off"
+        nameDisplayMode: "off",
+        videoDisabledDisplayMode: "off"
       }
     },
 
     getDefaultProps: function() {
       return {
         initiate: true,
         video: {enabled: true, visible: true},
         audio: {enabled: true, visible: true}
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -12,16 +12,17 @@ browser.jar:
 
   # Desktop script
   content/browser/loop/js/client.js                 (content/js/client.js)
   content/browser/loop/js/conversation.js           (content/js/conversation.js)
   content/browser/loop/js/otconfig.js               (content/js/otconfig.js)
   content/browser/loop/js/panel.js                  (content/js/panel.js)
   content/browser/loop/js/contacts.js               (content/js/contacts.js)
   content/browser/loop/js/conversationViews.js      (content/js/conversationViews.js)
+  content/browser/loop/js/roomViews.js              (content/js/roomViews.js)
 
   # Shared styles
   content/browser/loop/shared/css/reset.css         (content/shared/css/reset.css)
   content/browser/loop/shared/css/common.css        (content/shared/css/common.css)
   content/browser/loop/shared/css/panel.css         (content/shared/css/panel.css)
   content/browser/loop/shared/css/conversation.css  (content/shared/css/conversation.css)
   content/browser/loop/shared/css/contacts.css      (content/shared/css/contacts.css)
 
@@ -52,16 +53,17 @@ browser.jar:
   content/browser/loop/shared/img/icons-10x10.svg               (content/shared/img/icons-10x10.svg)
   content/browser/loop/shared/img/icons-14x14.svg               (content/shared/img/icons-14x14.svg)
   content/browser/loop/shared/img/icons-16x16.svg               (content/shared/img/icons-16x16.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js           (content/shared/js/actions.js)
   content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
   content/browser/loop/shared/js/roomListStore.js     (content/shared/js/roomListStore.js)
+  content/browser/loop/shared/js/localRoomStore.js    (content/shared/js/localRoomStore.js)
   content/browser/loop/shared/js/dispatcher.js        (content/shared/js/dispatcher.js)
   content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
   content/browser/loop/shared/js/models.js            (content/shared/js/models.js)
   content/browser/loop/shared/js/mixins.js            (content/shared/js/mixins.js)
   content/browser/loop/shared/js/otSdkDriver.js       (content/shared/js/otSdkDriver.js)
   content/browser/loop/shared/js/views.js             (content/shared/js/views.js)
   content/browser/loop/shared/js/utils.js             (content/shared/js/utils.js)
   content/browser/loop/shared/js/validate.js          (content/shared/js/validate.js)
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -1,14 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var expect = chai.expect;
 
 describe("loop.conversationViews", function () {
+  "use strict";
+
   var sandbox, oldTitle, view, dispatcher, contact;
 
   var CALL_STATES = loop.store.CALL_STATES;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
     oldTitle = document.title;
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -85,35 +85,72 @@ describe("loop.conversation", function()
         overrideGuidStorage: sinon.stub()
       };
     });
 
     afterEach(function() {
       delete window.OT;
     });
 
-    it("should initalize L10n", function() {
+    it("should initialize L10n", function() {
       loop.conversation.init();
 
       sinon.assert.calledOnce(document.mozL10n.initialize);
       sinon.assert.calledWithExactly(document.mozL10n.initialize,
         navigator.mozLoop);
     });
 
-    it("should create the ConversationControllerView", function() {
+    it("should create the AppControllerView", function() {
       loop.conversation.init();
 
       sinon.assert.calledOnce(React.renderComponent);
       sinon.assert.calledWith(React.renderComponent,
         sinon.match(function(value) {
           return TestUtils.isDescriptorOfType(value,
-            loop.conversation.ConversationControllerView);
+            loop.conversation.AppControllerView);
       }));
     });
 
+    describe("when locationHash begins with #room", function () {
+      // XXX must stay in sync with "test.alwaysUseRooms" pref check
+      // in conversation.jsx:init until we remove that code, which should
+      // happen in the second patch in bug 1074686, at which time this comment
+      // can go away as well.
+      var fakeRoomID = "32";
+
+      beforeEach(function() {
+        loop.shared.utils.Helper.prototype.locationHash
+          .returns("#room/" + fakeRoomID);
+
+        sandbox.stub(loop.store, "LocalRoomStore");
+      });
+
+      it("should create a localRoomStore", function() {
+        loop.conversation.init();
+
+        sinon.assert.calledOnce(loop.store.LocalRoomStore);
+        sinon.assert.calledWithNew(loop.store.LocalRoomStore);
+        sinon.assert.calledWithExactly(loop.store.LocalRoomStore,
+          sinon.match({
+            dispatcher: sinon.match.instanceOf(loop.Dispatcher),
+            mozLoop: sinon.match.same(navigator.mozLoop)
+          }));
+      });
+
+      it("should dispatch SetupEmptyRoom with localRoomId from locationHash",
+        function() {
+
+          loop.conversation.init();
+
+          sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
+          sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
+            new loop.shared.actions.SetupEmptyRoom({localRoomId: fakeRoomID}));
+        });
+    });
+
     it("should trigger a gatherCallData action", function() {
       loop.conversation.init();
 
       sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
       sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
         new loop.shared.actions.GatherCallData({
           callId: "42",
           outgoing: false
@@ -133,21 +170,22 @@ describe("loop.conversation", function()
             outgoing: true
           }));
       });
   });
 
   describe("ConversationControllerView", function() {
     var store, conversation, client, ccView, oldTitle, dispatcher;
 
-    function mountTestComponent() {
+    function mountTestComponent(localRoomStore) {
       return TestUtils.renderIntoDocument(
-        loop.conversation.ConversationControllerView({
+        loop.conversation.AppControllerView({
           client: client,
           conversation: conversation,
+          localRoomStore: localRoomStore,
           sdk: {},
           store: store
         }));
     }
 
     beforeEach(function() {
       oldTitle = document.title;
       client = new loop.Client();
@@ -188,16 +226,32 @@ describe("loop.conversation", function()
     it("should display the IncomingConversationView for incoming calls", function() {
       store.set({outgoing: false});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.conversation.IncomingConversationView);
     });
+
+    it("should display the EmptyRoomView for rooms", function() {
+      navigator.mozLoop.rooms = {
+        addCallback: function() {},
+        removeCallback: function() {}
+      };
+      var localRoomStore = new loop.store.LocalRoomStore({
+        mozLoop: navigator.mozLoop,
+        dispatcher: dispatcher
+      });
+
+      ccView = mountTestComponent(localRoomStore);
+
+      TestUtils.findRenderedComponentWithType(ccView,
+        loop.roomViews.EmptyRoomView);
+    });
   });
 
   describe("IncomingConversationView", function() {
     var conversation, client, icView, oldTitle;
 
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         loop.conversation.IncomingConversationView({
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -40,25 +40,28 @@
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
   <script src="../../content/shared/js/roomListStore.js"></script>
   <script src="../../content/js/client.js"></script>
+  <script src="../../content/shared/js/localRoomStore.js"></script>
+  <script src="../../content/js/roomViews.js"></script>
   <script src="../../content/js/conversationViews.js"></script>
   <script src="../../content/js/conversation.js"></script>
   <script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
   <script src="../../content/js/panel.js"></script>
 
   <!-- Test scripts -->
   <script src="client_test.js"></script>
   <script src="conversation_test.js"></script>
   <script src="panel_test.js"></script>
+  <script src="roomViews_test.js"></script>
   <script src="conversationViews_test.js"></script>
   <script>
     // Stop the default init functions running to avoid conflicts in tests
     document.removeEventListener('DOMContentLoaded', loop.panel.init);
     document.removeEventListener('DOMContentLoaded', loop.conversation.init);
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -0,0 +1,94 @@
+var expect = chai.expect;
+
+describe("loop.roomViews", function () {
+  "use strict";
+
+  var store, fakeWindow, sandbox, fakeAddCallback, fakeMozLoop,
+    fakeRemoveCallback, fakeRoomId, fakeWindow;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+
+    fakeRoomId = "fakeRoomId";
+    fakeAddCallback =
+      sandbox.stub().withArgs(fakeRoomId, "RoomCreationError");
+    fakeRemoveCallback =
+      sandbox.stub().withArgs(fakeRoomId, "RoomCreationError");
+    fakeMozLoop = { rooms: { addCallback: fakeAddCallback,
+                             removeCallback: fakeRemoveCallback } };
+
+    fakeWindow = { document: {} };
+    loop.roomViews.setRootObject(fakeWindow);
+
+    store = new loop.store.LocalRoomStore({
+      dispatcher: { register: function() {} },
+      mozLoop: fakeMozLoop
+    });
+    store.setStoreState({localRoomId: fakeRoomId});
+  });
+
+  afterEach(function() {
+    sinon.sandbox.restore();
+    loop.roomViews.setRootObject(window);
+  });
+
+  describe("EmptyRoomView", function() {
+    function mountTestComponent() {
+      return TestUtils.renderIntoDocument(
+        new loop.roomViews.EmptyRoomView({
+          mozLoop: fakeMozLoop,
+          localRoomStore: store
+        }));
+    }
+
+    describe("#componentDidMount", function() {
+       it("should add #onCreationError using mozLoop.rooms.addCallback",
+         function() {
+
+           var testComponent = mountTestComponent();
+
+           sinon.assert.calledOnce(fakeMozLoop.rooms.addCallback);
+           sinon.assert.calledWithExactly(fakeMozLoop.rooms.addCallback,
+             fakeRoomId, "RoomCreationError", testComponent.onCreationError);
+         });
+    });
+
+    describe("#componentWillUnmount", function () {
+      it("should remove #onCreationError using mozLoop.rooms.addCallback",
+        function () {
+          var testComponent = mountTestComponent();
+
+          testComponent.componentWillUnmount();
+
+          sinon.assert.calledOnce(fakeMozLoop.rooms.removeCallback);
+          sinon.assert.calledWithExactly(fakeMozLoop.rooms.removeCallback,
+            fakeRoomId, "RoomCreationError", testComponent.onCreationError);
+        });
+      });
+
+    describe("#onCreationError", function() {
+      it("should log an error using console.error", function() {
+        fakeWindow.console = { error: sandbox.stub() };
+        var testComponent = mountTestComponent();
+
+        testComponent.onCreationError(new Error("fake error"));
+
+        sinon.assert.calledOnce(fakeWindow.console.error);
+      });
+    });
+
+    describe("#render", function() {
+      it("should set document.title to store.serverData.roomName",
+        function() {
+          var fakeRoomName = "Monkey";
+          store.setStoreState({serverData: {roomName: fakeRoomName},
+                               localRoomId: fakeRoomId});
+
+          mountTestComponent();
+
+          expect(fakeWindow.document.title).to.equal(fakeRoomName);
+        })
+    });
+
+  });
+});
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -1,14 +1,14 @@
 from marionette_test import MarionetteTestCase
 from by import By
 import urlparse
-from marionette.errors import NoSuchElementException, StaleElementException
+from errors import NoSuchElementException, StaleElementException
 # noinspection PyUnresolvedReferences
-from marionette.wait import Wait
+from wait import Wait
 from time import sleep
 
 import os
 import sys
 sys.path.insert(1, os.path.dirname(os.path.abspath(__file__)))
 
 from serversetup import LoopTestServers
 from config import *
--- a/browser/components/loop/test/mochitest/browser_GoogleImporter.js
+++ b/browser/components/loop/test/mochitest/browser_GoogleImporter.js
@@ -18,60 +18,68 @@ function promiseImport() {
 }
 
 add_task(function* test_GoogleImport() {
   let stats;
   // An error may throw and the test will fail when that happens.
   stats = yield promiseImport();
 
   // Assert the world.
-  Assert.equal(stats.total, 5, "Five contacts should get processed");
-  Assert.equal(stats.success, 5, "Five contacts should be imported");
+  Assert.equal(stats.total, 6, "Five contacts should get processed");
+  Assert.equal(stats.success, 6, "Five contacts should be imported");
 
   yield promiseImport();
-  Assert.equal(Object.keys(mockDb._store).length, 5, "Database should contain only five contact after reimport");
+  Assert.equal(Object.keys(mockDb._store).length, 6, "Database should contain only five contact after reimport");
 
-  let c = mockDb._store[mockDb._next_guid - 5];
+  let c = mockDb._store[mockDb._next_guid - 6];
   Assert.equal(c.name[0], "John Smith", "Full name should match");
   Assert.equal(c.givenName[0], "John", "Given name should match");
   Assert.equal(c.familyName[0], "Smith", "Family name should match");
   Assert.equal(c.email[0].type, "other", "Email type should match");
   Assert.equal(c.email[0].value, "john.smith@example.com", "Email should match");
   Assert.equal(c.email[0].pref, true, "Pref should match");
   Assert.equal(c.category[0], "google", "Category should match");
   Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/0", "UID should match and be scoped to provider");
 
-  c = mockDb._store[mockDb._next_guid - 4];
+  c = mockDb._store[mockDb._next_guid - 5];
   Assert.equal(c.name[0], "Jane Smith", "Full name should match");
   Assert.equal(c.givenName[0], "Jane", "Given name should match");
   Assert.equal(c.familyName[0], "Smith", "Family name should match");
   Assert.equal(c.email[0].type, "other", "Email type should match");
   Assert.equal(c.email[0].value, "jane.smith@example.com", "Email should match");
   Assert.equal(c.email[0].pref, true, "Pref should match");
   Assert.equal(c.category[0], "google", "Category should match");
   Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1", "UID should match and be scoped to provider");
 
-  c = mockDb._store[mockDb._next_guid - 3];
+  c = mockDb._store[mockDb._next_guid - 4];
   Assert.equal(c.name[0], "Davy Randall Jones", "Full name should match");
   Assert.equal(c.givenName[0], "Davy Randall", "Given name should match");
   Assert.equal(c.familyName[0], "Jones", "Family name should match");
   Assert.equal(c.email[0].type, "other", "Email type should match");
   Assert.equal(c.email[0].value, "davy.jones@example.com", "Email should match");
   Assert.equal(c.email[0].pref, true, "Pref should match");
   Assert.equal(c.category[0], "google", "Category should match");
   Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2", "UID should match and be scoped to provider");
 
-  c = mockDb._store[mockDb._next_guid - 2];
+  c = mockDb._store[mockDb._next_guid - 3];
   Assert.equal(c.name[0], "noname@example.com", "Full name should match");
   Assert.equal(c.email[0].type, "other", "Email type should match");
   Assert.equal(c.email[0].value, "noname@example.com", "Email should match");
   Assert.equal(c.email[0].pref, true, "Pref should match");
   Assert.equal(c.category[0], "google", "Category should match");
   Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3", "UID should match and be scoped to provider");
 
-  c = mockDb._store[mockDb._next_guid - 1];
+  c = mockDb._store[mockDb._next_guid - 2];
   Assert.equal(c.name[0], "lycnix", "Full name should match");
   Assert.equal(c.email[0].type, "other", "Email type should match");
   Assert.equal(c.email[0].value, "lycnix", "Email should match");
   Assert.equal(c.email[0].pref, true, "Pref should match");
   Assert.equal(c.category[0], "google", "Category should match");
   Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7", "UID should match and be scoped to provider");
+
+  c = mockDb._store[mockDb._next_guid - 1];
+  Assert.equal(c.name[0], "+31-6-12345678", "Full name should match");
+  Assert.equal(c.tel[0].type, "mobile", "Email type should match");
+  Assert.equal(c.tel[0].value, "+31-6-12345678", "Email should match");
+  Assert.equal(c.tel[0].pref, false, "Pref should match");
+  Assert.equal(c.category[0], "google", "Category should match");
+  Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8", "UID should match and be scoped to provider");
 });
--- a/browser/components/loop/test/mochitest/fixtures/google_contacts.txt
+++ b/browser/components/loop/test/mochitest/fixtures/google_contacts.txt
@@ -86,9 +86,20 @@
     <app:edited xmlns:app="http://www.w3.org/2007/app">2007-08-01T05:45:52.203Z</app:edited>
     <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
     <title/>
     <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/7" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
     <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
     <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
     <gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
   </entry>
+  <entry gd:etag="&quot;RXkzfjVSLit7I2A9XRdRGUgITgA.&quot;">
+    <id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8</id>
+    <updated>2014-10-10T14:55:44.786Z</updated>
+    <app:edited xmlns:app="http://www.w3.org/2007/app">2014-10-10T14:55:44.786Z</app:edited>
+    <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
+    <title/>
+    <link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/8" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="self" type="application/atom+xml"/>
+    <link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="edit" type="application/atom+xml"/>
+    <gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile" uri="tel:+31-6-12345678">0612345678</gd:phoneNumber>
+  </entry>
 </feed>
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var expect = chai.expect;
 
-describe("loop.ConversationStore", function () {
+describe("loop.store.ConversationStore", function () {
   "use strict";
 
   var CALL_STATES = loop.store.CALL_STATES;
   var WS_STATES = loop.store.WS_STATES;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
   var contact;
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -38,28 +38,30 @@
   <script src="../../content/shared/js/mixins.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/validate.js"></script>
   <script src="../../content/shared/js/actions.js"></script>
   <script src="../../content/shared/js/dispatcher.js"></script>
   <script src="../../content/shared/js/otSdkDriver.js"></script>
+  <script src="../../content/shared/js/localRoomStore.js"></script>
   <script src="../../content/shared/js/conversationStore.js"></script>
   <script src="../../content/shared/js/roomListStore.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
   <script src="mixins_test.js"></script>
   <script src="utils_test.js"></script>
   <script src="views_test.js"></script>
   <script src="websocket_test.js"></script>
   <script src="feedbackApiClient_test.js"></script>
   <script src="validate_test.js"></script>
   <script src="dispatcher_test.js"></script>
+  <script src="localRoomStore_test.js"></script>
   <script src="conversationStore_test.js"></script>
   <script src="otSdkDriver_test.js"></script>
   <script src="roomListStore_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/localRoomStore_test.js
@@ -0,0 +1,110 @@
+/* global chai */
+
+var expect = chai.expect;
+var sharedActions = loop.shared.actions;
+
+describe("loop.store.LocalRoomStore", function () {
+  "use strict";
+
+  var sandbox, dispatcher;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    dispatcher = new loop.Dispatcher();
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  describe("#constructor", function() {
+    it("should throw an error if the dispatcher is missing", function() {
+      expect(function() {
+        new loop.store.LocalRoomStore({mozLoop: {}});
+      }).to.Throw(/dispatcher/);
+    });
+
+    it("should throw an error if mozLoop is missing", function() {
+      expect(function() {
+        new loop.store.LocalRoomStore({dispatcher: dispatcher});
+      }).to.Throw(/mozLoop/);
+    });
+  });
+
+  describe("#setupEmptyRoom", function() {
+    var store, fakeMozLoop, fakeRoomId, fakeRoomName;
+
+    beforeEach(function() {
+      fakeRoomId = "337-ff-54";
+      fakeRoomName = "Monkeys";
+      fakeMozLoop = {
+        rooms: { getRoomData: sandbox.stub() }
+      };
+
+      store = new loop.store.LocalRoomStore(
+        {mozLoop: fakeMozLoop, dispatcher: dispatcher});
+      fakeMozLoop.rooms.getRoomData.
+        withArgs(fakeRoomId).
+        callsArgOnWith(1, // index of callback argument
+        store, // |this| to call it on
+        null, // args to call the callback with...
+        {roomName: fakeRoomName}
+      );
+    });
+
+    it("should trigger a change event", function(done) {
+      store.on("change", function() {
+        done();
+      });
+
+      dispatcher.dispatch(new sharedActions.SetupEmptyRoom(
+        {localRoomId: fakeRoomId}));
+    });
+
+    it("should set localRoomId on the store from the action data",
+      function(done) {
+
+        store.once("change", function () {
+          expect(store.getStoreState()).
+            to.have.property('localRoomId', fakeRoomId);
+          done();
+        });
+
+        dispatcher.dispatch(
+          new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+    });
+
+    it("should set serverData.roomName from the getRoomData callback",
+      function(done) {
+
+        store.once("change", function () {
+          expect(store.getStoreState()).to.have.deep.property(
+            'serverData.roomName', fakeRoomName);
+          done();
+        });
+
+        dispatcher.dispatch(
+          new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+      });
+
+    it("should set error on the store when getRoomData calls back an error",
+      function(done) {
+
+        var fakeError = new Error("fake error");
+        fakeMozLoop.rooms.getRoomData.
+          withArgs(fakeRoomId).
+          callsArgOnWith(1, // index of callback argument
+          store, // |this| to call it on
+          fakeError); // args to call the callback with...
+
+        store.once("change", function() {
+          expect(this.getStoreState()).to.have.property('error', fakeError);
+          done();
+        });
+
+        dispatcher.dispatch(
+          new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
+      });
+
+  });
+});
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -33,16 +33,20 @@ describe("loop.shared.mixins", function(
         onDocumentHidden: onDocumentHiddenStub,
         onDocumentVisible: onDocumentVisibleStub,
         render: function() {
           return React.DOM.div();
         }
       });
     });
 
+    afterEach(function() {
+      loop.shared.mixins.setRootObject(window);
+    });
+
     function setupFakeVisibilityEventDispatcher(event) {
       loop.shared.mixins.setRootObject({
         document: {
           addEventListener: function(_, fn) {
             fn(event);
           },
           removeEventListener: sandbox.stub()
         }
--- a/browser/components/loop/test/xpcshell/head.js
+++ b/browser/components/loop/test/xpcshell/head.js
@@ -3,16 +3,17 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Http.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource:///modules/loop/MozLoopService.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
 const { MozLoopServiceInternal } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 
 const kMockWebSocketChannelName = "Mock WebSocket Channel";
 const kWebSocketChannelContractID = "@mozilla.org/network/protocol;1?name=wss";
 
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -37,16 +37,17 @@
     <script src="../content/shared/js/models.js"></script>
     <script src="../content/shared/js/mixins.js"></script>
     <script src="../content/shared/js/views.js"></script>
     <script src="../content/shared/js/websocket.js"></script>
     <script src="../content/shared/js/validate.js"></script>
     <script src="../content/shared/js/dispatcher.js"></script>
     <script src="../content/shared/js/conversationStore.js"></script>
     <script src="../content/shared/js/roomListStore.js"></script>
+    <script src="../content/js/roomViews.js"></script>
     <script src="../content/js/conversationViews.js"></script>
     <script src="../content/js/client.js"></script>
     <script src="../standalone/content/js/webapp.js"></script>
     <script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
     <script>
       if (!loop.contacts) {
         // For browsers that don't support ES6 without special flags (all but Fx
         // at the moment), we shim the contacts namespace with its most barebone
--- a/browser/components/places/content/sidebarUtils.js
+++ b/browser/components/places/content/sidebarUtils.js
@@ -5,66 +5,63 @@
 
 var SidebarUtils = {
   handleTreeClick: function SU_handleTreeClick(aTree, aEvent, aGutterSelect) {
     // right-clicks are not handled here
     if (aEvent.button == 2)
       return;
 
     var tbo = aTree.treeBoxObject;
-    var row = { }, col = { }, obj = { };
-    tbo.getCellAt(aEvent.clientX, aEvent.clientY, row, col, obj);
+    var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
 
-    if (row.value == -1 || obj.value == "twisty")
+    if (cell.row == -1 || cell.childElt == "twisty")
       return;
 
     var mouseInGutter = false;
     if (aGutterSelect) {
-      var x = { }, y = { }, w = { }, h = { };
-      tbo.getCoordsForCellItem(row.value, col.value, "image",
-                               x, y, w, h);
+      var rect = tbo.getCoordsForCellItem(cell.row, cell.col, "image");
       // getCoordsForCellItem returns the x coordinate in logical coordinates
       // (i.e., starting from the left and right sides in LTR and RTL modes,
       // respectively.)  Therefore, we make sure to exclude the blank area
       // before the tree item icon (that is, to the left or right of it in
       // LTR and RTL modes, respectively) from the click target area.
       var isRTL = window.getComputedStyle(aTree, null).direction == "rtl";
       if (isRTL)
-        mouseInGutter = aEvent.clientX > x.value;
+        mouseInGutter = aEvent.clientX > rect.x;
       else
-        mouseInGutter = aEvent.clientX < x.value;
+        mouseInGutter = aEvent.clientX < rect.x;
     }
 
 #ifdef XP_MACOSX
     var modifKey = aEvent.metaKey || aEvent.shiftKey;
 #else
     var modifKey = aEvent.ctrlKey || aEvent.shiftKey;
 #endif
 
-    var isContainer = tbo.view.isContainer(row.value);
+    var isContainer = tbo.view.isContainer(cell.row);
     var openInTabs = isContainer &&
                      (aEvent.button == 1 ||
                       (aEvent.button == 0 && modifKey)) &&
-                     PlacesUtils.hasChildURIs(tbo.view.nodeForTreeIndex(row.value), true);
+                     PlacesUtils.hasChildURIs(tbo.view.nodeForTreeIndex(cell.row), true);
 
     if (aEvent.button == 0 && isContainer && !openInTabs) {
-      tbo.view.toggleOpenState(row.value);
+      tbo.view.toggleOpenState(cell.row);
       return;
     }
     else if (!mouseInGutter && openInTabs &&
             aEvent.originalTarget.localName == "treechildren") {
-      tbo.view.selection.select(row.value);
+      tbo.view.selection.select(cell.row);
       PlacesUIUtils.openContainerNodeInTabs(aTree.selectedNode, aEvent, aTree);
     }
     else if (!mouseInGutter && !isContainer &&
              aEvent.originalTarget.localName == "treechildren") {
       // Clear all other selection since we're loading a link now. We must
       // do this *before* attempting to load the link since openURL uses
       // selection as an indication of which link to load.
-      tbo.view.selection.select(row.value);
+      tbo.view.selection.select(cell.row);
       PlacesUIUtils.openNodeWithEvent(aTree.selectedNode, aEvent, aTree);
     }
   },
 
   handleTreeKeyPress: function SU_handleTreeKeyPress(aEvent) {
     // XXX Bug 627901: Post Fx4, this method should take a tree parameter.
     let tree = aEvent.target;
     let node = tree.selectedNode;
@@ -79,24 +76,23 @@ var SidebarUtils = {
    * hovered over.
    */
   handleTreeMouseMove: function SU_handleTreeMouseMove(aEvent) {
     if (aEvent.target.localName != "treechildren")
       return;
 
     var tree = aEvent.target.parentNode;
     var tbo = tree.treeBoxObject;
-    var row = { }, col = { }, obj = { };
-    tbo.getCellAt(aEvent.clientX, aEvent.clientY, row, col, obj);
+    var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
 
-    // row.value is -1 when the mouse is hovering an empty area within the tree.
+    // cell.row is -1 when the mouse is hovering an empty area within the tree.
     // To avoid showing a URL from a previously hovered node for a currently
     // hovered non-url node, we must clear the moused-over URL in these cases.
-    if (row.value != -1) {
-      var node = tree.view.nodeForTreeIndex(row.value);
+    if (cell.row != -1) {
+      var node = tree.view.nodeForTreeIndex(cell.row);
       if (PlacesUtils.nodeIsURI(node))
         this.setMouseoverURL(node.uri);
       else
         this.setMouseoverURL("");
     }
     else
       this.setMouseoverURL("");
   },
--- a/browser/components/places/content/tree.xml
+++ b/browser/components/places/content/tree.xml
@@ -742,52 +742,50 @@
         this._controller.setDataTransfer(event);
         event.stopPropagation();
       ]]></handler>
 
       <handler event="dragover"><![CDATA[
         if (event.target.localName != "treechildren")
           return;
 
-        let row = { }, col = { }, child = { };
-        this.treeBoxObject.getCellAt(event.clientX, event.clientY,
-                                     row, col, child);
-        let node = row.value != -1 ?
-                   this.view.nodeForTreeIndex(row.value) :
+        let cell = this.treeBoxObject.getCellAt(event.clientX, event.clientY);
+        let node = cell.row != -1 ?
+                   this.view.nodeForTreeIndex(cell.row) :
                    this.result.root;
         // cache the dropTarget for the view
         PlacesControllerDragHelper.currentDropTarget = node;
 
         // We have to calculate the orientation since view.canDrop will use
         // it and we want to be consistent with the dropfeedback.
         let tbo = this.treeBoxObject;
         let rowHeight = tbo.rowHeight;
         let eventY = event.clientY - tbo.treeBody.boxObject.y -
-                     rowHeight * (row.value - tbo.getFirstVisibleRow());
+                     rowHeight * (cell.row - tbo.getFirstVisibleRow());
 
         let orientation = Ci.nsITreeView.DROP_BEFORE;
 
-        if (row.value == -1) {
+        if (cell.row == -1) {
           // If the row is not valid we try to insert inside the resultNode.
           orientation = Ci.nsITreeView.DROP_ON;
         }
         else if (PlacesUtils.nodeIsContainer(node) &&
                  eventY > rowHeight * 0.75) {
           // If we are below the 75% of a container the treeview we try
           // to drop after the node.
           orientation = Ci.nsITreeView.DROP_AFTER;
         }
         else if (PlacesUtils.nodeIsContainer(node) &&
                  eventY > rowHeight * 0.25) {
           // If we are below the 25% of a container the treeview we try
           // to drop inside the node.
           orientation = Ci.nsITreeView.DROP_ON;
         }
 
-        if (!this.view.canDrop(row.value, orientation, event.dataTransfer))
+        if (!this.view.canDrop(cell.row, orientation, event.dataTransfer))
           return;
 
         event.preventDefault();
         event.stopPropagation();
       ]]></handler>
 
       <handler event="dragend"><![CDATA[
         PlacesControllerDragHelper.currentDropTarget = null;
--- a/browser/components/places/tests/browser/browser_forgetthissite_single.js
+++ b/browser/components/places/tests/browser/browser_forgetthissite_single.js
@@ -57,17 +57,14 @@ function test() {
               organizer.removeEventListener("unload", arguments.callee, false);
               // Proceed
               funcNext();
             }, false);
             // Close Library window.
             organizer.close();
           }, true);
           // Get cell coordinates
-          var x = {}, y = {}, width = {}, height = {};
-          tree.treeBoxObject.getCoordsForCellItem(0, tree.columns[0], "text",
-                                                  x, y, width, height);
+          var rect = tree.treeBoxObject.getCoordsForCellItem(0, tree.columns[0], "text");
           // Initiate a context menu for the selected cell
-          EventUtils.synthesizeMouse(tree.body, x.value + width.value / 2, y.value + height.value / 2, {type: "contextmenu"}, organizer);
+          EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, {type: "contextmenu"}, organizer);
     });
   }
 }
-
--- a/browser/components/places/tests/browser/browser_library_infoBox.js
+++ b/browser/components/places/tests/browser/browser_library_infoBox.js
@@ -47,17 +47,17 @@ gTests.push({
       isnot(childNode, null, "History node first child is not null.");
       PO._places.selectNode(childNode);
       checkInfoBoxSelected(PO);
       ok(infoBoxExpanderWrapper.hidden,
          "Expander button is hidden for history child node.");
       checkAddInfoFieldsCollapsed(PO);
 
       // open history item
-      var view = ContentTree.view.treeBoxObject.view;
+      var view = ContentTree.view.view;
       ok(view.rowCount > 0, "History item exists.");
       view.selection.select(0);
       ok(infoBoxExpanderWrapper.hidden,
          "Expander button is hidden for history item.");
       checkAddInfoFieldsCollapsed(PO);
 
       historyNode.containerOpen = false;
 
@@ -84,17 +84,17 @@ gTests.push({
          "Correctly selected recently bookmarked node.");
       PO._places.selectNode(childNode);
       checkInfoBoxSelected(PO);
       ok(!infoBoxExpanderWrapper.hidden,
          "Expander button is not hidden for recently bookmarked node.");
       checkAddInfoFieldsNotCollapsed(PO);
 
       // open first bookmark
-      var view = ContentTree.view.treeBoxObject.view;
+      var view = ContentTree.view.view;
       ok(view.rowCount > 0, "Bookmark item exists.");
       view.selection.select(0);
       checkInfoBoxSelected(PO);
       ok(!infoBoxExpanderWrapper.hidden,
          "Expander button is not hidden for bookmark item.");
       checkAddInfoFieldsNotCollapsed(PO);
       checkAddInfoFields(PO, "bookmark item");
 
--- a/browser/components/places/tests/browser/browser_library_middleclick.js
+++ b/browser/components/places/tests/browser/browser_library_middleclick.js
@@ -267,15 +267,13 @@ function runNextTest() {
 
 function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) {
   var selection = aTree.view.selection;
   selection.select(aRowIndex);
   aTree.treeBoxObject.ensureRowIsVisible(aRowIndex);
   var column = aTree.columns[aColumnIndex];
 
   // get cell coordinates
-  var x = {}, y = {}, width = {}, height = {};
-  aTree.treeBoxObject.getCoordsForCellItem(aRowIndex, column, "text",
-                                           x, y, width, height);
+  var rect = aTree.treeBoxObject.getCoordsForCellItem(aRowIndex, column, "text");
 
-  EventUtils.synthesizeMouse(aTree.body, x.value, y.value,
+  EventUtils.synthesizeMouse(aTree.body, rect.x, rect.y,
                              aEventDetails, gLibrary);
 }
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -191,17 +191,15 @@ function synthesizeClickOnSelectedTreeCe
   if (tbo.view.selection.count != 1)
      throw new Error("The test node should be successfully selected");
   // Get selection rowID.
   let min = {}, max = {};
   tbo.view.selection.getRangeAt(0, min, max);
   let rowID = min.value;
   tbo.ensureRowIsVisible(rowID);
   // Calculate the click coordinates.
-  let x = {}, y = {}, width = {}, height = {};
-  tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text",
-                           x, y, width, height);
-  x = x.value + width.value / 2;
-  y = y.value + height.value / 2;
+  var rect = tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text");
+  var x = rect.x + rect.width / 2;
+  var y = rect.y + rect.height / 2;
   // Simulate the click.
   EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
                              aTree.ownerDocument.defaultView);
 }
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -33,17 +33,17 @@ var gCookiesWindow = {
     var os = Components.classes["@mozilla.org/observer-service;1"]
                        .getService(Components.interfaces.nsIObserverService);
     os.removeObserver(this, "cookie-changed");
     os.removeObserver(this, "perm-changed");
   },
 
   _populateList: function (aInitialLoad) {
     this._loadCookies();
-    this._tree.treeBoxObject.view = this._view;
+    this._tree.view = this._view;
     if (aInitialLoad)
       this.sort("rawHost");
     if (this._view.rowCount > 0)
       this._tree.view.selection.select(0);
 
     if (aInitialLoad) {
       if ("arguments" in window &&
           window.arguments[0] &&
@@ -785,17 +785,17 @@ var gCookiesWindow = {
     // Clear the Tree Display
     this._view._filtered = false;
     this._view._rowCount = 0;
     this._tree.treeBoxObject.rowCountChanged(0, -this._view._filterSet.length);
     this._view._filterSet = [];
 
     // Just reload the list to make sure deletions are respected
     this._loadCookies();
-    this._tree.treeBoxObject.view = this._view;
+    this._tree.view = this._view;
 
     // Restore sort order
     var sortby = this._lastSortProperty;
     if (sortby == "") {
       this._lastSortAscending = false;
       this.sort("rawHost");
     }
     else {
--- a/browser/components/preferences/permissions.js
+++ b/browser/components/preferences/permissions.js
@@ -327,17 +327,17 @@ var gPermissionManager = {
     while (enumerator.hasMoreElements()) {
       var nextPermission = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission);
       this._addPermissionToList(nextPermission);
     }
    
     this._view._rowCount = this._permissions.length;
 
     // sort and display the table
-    this._tree.treeBoxObject.view = this._view;
+    this._tree.view = this._view;
     this.onPermissionSort("rawHost", false);
 
     // disable "remove all" button if there are none
     document.getElementById("removeAllPermissions").disabled = this._permissions.length == 0;
   },
   
   _addPermissionToList: function (aPermission)
   {
@@ -365,9 +365,8 @@ function setHost(aHost)
 {
   gPermissionManager.setHost(aHost);
 }
 
 function initWithParams(aParams)
 {
   gPermissionManager.init(aParams);
 }
-
--- a/browser/components/preferences/tests/browser_bug705422.js
+++ b/browser/components/preferences/tests/browser_bug705422.js
@@ -77,34 +77,33 @@ function runTest(win, searchTerm, cookie
 
     // "delete all cookies" should be enabled
     isDisabled(win, false);
 
 
     // select first cookie and delete
     var tree = win.document.getElementById("cookiesList");
     var deleteButton = win.document.getElementById("removeCookie");
-    var x = {}, y = {}, width = {}, height = {};
-    tree.treeBoxObject.getCoordsForCellItem(0, tree.columns[0], "cell", x, y, width, height);
-    EventUtils.synthesizeMouse(tree.body, x.value + width.value / 2, y.value + height.value / 2, {}, win);
+    var rect = tree.treeBoxObject.getCoordsForCellItem(0, tree.columns[0], "cell");
+    EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, {}, win);
     EventUtils.synthesizeMouseAtCenter(deleteButton, {}, win);
 
     // count cookies should be matches-1
     is(win.gCookiesWindow._view.rowCount, matches-1, "Deleted selected cookie");
 
     // select two adjacent cells and delete
-    EventUtils.synthesizeMouse(tree.body, x.value + width.value / 2, y.value + height.value / 2, {}, win);
+    EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, {}, win);
     deleteButton = win.document.getElementById("removeCookies"); 
     var eventObj = {};
     if (navigator.platform.indexOf("Mac") >= 0)
         eventObj.metaKey = true;
     else
         eventObj.ctrlKey = true;
-    tree.treeBoxObject.getCoordsForCellItem(1, tree.columns[0], "cell", x, y, width, height);
-    EventUtils.synthesizeMouse(tree.body, x.value + width.value / 2, y.value + height.value / 2, eventObj, win);
+    rect = tree.treeBoxObject.getCoordsForCellItem(1, tree.columns[0], "cell");
+    EventUtils.synthesizeMouse(tree.body, rect.x + rect.width / 2, rect.y + rect.height / 2, eventObj, win);
     EventUtils.synthesizeMouseAtCenter(deleteButton, {}, win);
 
     // count cookies should be matches-3
     is(win.gCookiesWindow._view.rowCount, matches-3, "Deleted selected two adjacent cookies");
 
     // "delete all cookies" should be enabled
     isDisabled(win, false);
 
--- a/browser/components/preferences/translation.js
+++ b/browser/components/preferences/translation.js
@@ -15,17 +15,17 @@ XPCOMUtils.defineLazyGetter(this, "gLang
 
 const kPermissionType = "translate";
 const kLanguagesPref = "browser.translation.neverForLanguages";
 
 function Tree(aId, aData)
 {
   this._data = aData;
   this._tree = document.getElementById(aId);
-  this._tree.treeBoxObject.view = this;
+  this._tree.view = this;
 }
 
 Tree.prototype = {
   get boxObject() this._tree.treeBoxObject,
   get isEmpty() !this._data.length,
   get hasSelection() this.selection.count > 0,
   getSelectedItems: function() {
     let result = [];
--- a/browser/components/search/test/browser_405664.js
+++ b/browser/components/search/test/browser_405664.js
@@ -1,16 +1,16 @@
 function test() {
   var searchBar = BrowserSearch.searchBar;
   ok(searchBar, "got search bar");
   
   searchBar.focus();
 
   var pbo = searchBar._popup.popupBoxObject;
-  ok(pbo, "popup is nsIPopupBoxObject");
+  ok(pbo, "popup is PopupBoxObject");
 
   EventUtils.synthesizeKey("VK_UP", { altKey: true });
   is(pbo.popupState, "showing", "popup is opening after Alt+Up");
 
   EventUtils.synthesizeKey("VK_ESCAPE", {});
   is(pbo.popupState, "closed", "popup is closed after ESC");
 
   EventUtils.synthesizeKey("VK_DOWN", { altKey: true });
--- a/browser/components/sessionstore/content/aboutSessionRestore.js
+++ b/browser/components/sessionstore/content/aboutSessionRestore.js
@@ -139,34 +139,33 @@ function startNewSession() {
     getBrowserWindow().BrowserHome();
 }
 
 function onListClick(aEvent) {
   // don't react to right-clicks
   if (aEvent.button == 2)
     return;
 
-  var row = {}, col = {};
-  treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY, row, col, {});
-  if (col.value) {
+  var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY);
+  if (cell.col) {
     // Restore this specific tab in the same window for middle/double/accel clicking
     // on a tab's title.
 #ifdef XP_MACOSX
     let accelKey = aEvent.metaKey;
 #else
     let accelKey = aEvent.ctrlKey;
 #endif
     if ((aEvent.button == 1 || aEvent.button == 0 && aEvent.detail == 2 || accelKey) &&
-        col.value.id == "title" &&
-        !treeView.isContainer(row.value)) {
-      restoreSingleTab(row.value, aEvent.shiftKey);
+        cell.col.id == "title" &&
+        !treeView.isContainer(cell.row)) {
+      restoreSingleTab(cell.row, aEvent.shiftKey);
       aEvent.stopPropagation();
     }
-    else if (col.value.id == "restore")
-      toggleRowChecked(row.value);
+    else if (cell.col.id == "restore")
+      toggleRowChecked(cell.row);
   }
 }
 
 function onListKeyDown(aEvent) {
   switch (aEvent.keyCode)
   {
   case KeyEvent.DOM_VK_SPACE:
     toggleRowChecked(document.getElementById("tabList").currentIndex);
--- a/browser/components/sessionstore/test/browser_590563.js
+++ b/browser/components/sessionstore/test/browser_590563.js
@@ -33,25 +33,23 @@ function test() {
   });
 }
 
 function middleClickTest(win) {
   let browser = win.gBrowser.selectedBrowser;
   let tree = browser.contentDocument.getElementById("tabList");
   is(tree.view.rowCount, 3, "There should be three items");
 
-  let x = {}, y = {}, width = {}, height = {};
-
   // click on the first tab item
-  tree.treeBoxObject.getCoordsForCellItem(1, tree.columns[1], "text", x, y, width, height);
-  EventUtils.synthesizeMouse(tree.body, x.value, y.value, { button: 1 },
+  var rect = tree.treeBoxObject.getCoordsForCellItem(1, tree.columns[1], "text");
+  EventUtils.synthesizeMouse(tree.body, rect.x, rect.y, { button: 1 },
                              browser.contentWindow);
   // click on the second tab item
-  tree.treeBoxObject.getCoordsForCellItem(2, tree.columns[1], "text", x, y, width, height);
-  EventUtils.synthesizeMouse(tree.body, x.value, y.value, { button: 1 },
+  rect = tree.treeBoxObject.getCoordsForCellItem(2, tree.columns[1], "text");
+  EventUtils.synthesizeMouse(tree.body, rect.x, rect.y, { button: 1 },
                              browser.contentWindow);
 
   is(win.gBrowser.tabs.length, 3,
      "The total number of tabs should be 3 after restoring 2 tabs by middle click.");
   is(win.gBrowser.visibleTabs.length, 3,
      "The total number of visible tabs should be 3 after restoring 2 tabs by middle click");
 }
 
--- a/browser/devtools/scratchpad/test/browser_scratchpad_edit_ui_updates.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_edit_ui_updates.js
@@ -1,21 +1,23 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 /* Bug 699130 */
 
 "use strict";
 
 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
+let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
 
 function test()
 {
   waitForExplicitFinish();
   gBrowser.selectedTab = gBrowser.addTab();
+  Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false);
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
     openScratchpad(runTests);
   }, true);
 
   content.location = "data:text/html,test Edit menu updates Scratchpad - bug 699130";
 }
 
@@ -170,16 +172,17 @@ function runTests()
     closeMenu(hideAfterPaste);
   };
 
   let hideAfterPaste = function() {
     if (pass == 0) {
       pass++;
       testContextMenu();
     } else {
+      Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
       finish();
     }
   };
 
   let testContextMenu = function() {
     info("will test the context menu");
 
     editMenu = null;
--- a/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_initialization.js
@@ -1,16 +1,19 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
+
 function test()
 {
   waitForExplicitFinish();
 
+  Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false);
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
 
     ok(window.Scratchpad, "Scratchpad variable exists");
 
     openScratchpad(runTests);
   }, true);
@@ -39,10 +42,11 @@ function runTests()
      "Error console command is disabled");
 
   let chromeContextCommand = gScratchpadWindow.document.
                             getElementById("sp-cmd-browserContext");
   ok(chromeContextCommand, "Chrome context command element exists");
   is(chromeContextCommand.getAttribute("disabled"), "true",
      "Chrome context command is disabled");
 
+  Services.prefs.clearUserPref(DEVTOOLS_CHROME_ENABLED);
   finish();
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_modeline.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_modeline.js
@@ -15,16 +15,17 @@ let gFile; // Reference to the temporary
 let DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
 
 // The temporary file content.
 let gFileContent = "function main() { return 0; }";
 
 function test() {
   waitForExplicitFinish();
 
+  Services.prefs.setBoolPref(DEVTOOLS_CHROME_ENABLED, false);
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
     openScratchpad(runTests);
   }, true);
 
   content.location = "data:text/html,<p>test file open and save in Scratchpad";
 }
--- a/browser/devtools/shared/widgets/FastListWidget.js
+++ b/browser/devtools/shared/widgets/FastListWidget.js
@@ -190,17 +190,17 @@ FastListWidget.prototype = {
    *        The element to make visible.
    */
   ensureElementIsVisible: function(element) {
     if (!element) {
       return;
     }
 
     // Ensure the element is visible but not scrolled horizontally.
-    let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+    let boxObject = this._list.boxObject;
     boxObject.ensureElementIsVisible(element);
     boxObject.scrollBy(-this._list.clientWidth, 0);
   },
 
   /**
    * Sets the text displayed in this container when empty.
    * @param string aValue
    */
--- a/browser/devtools/shared/widgets/SideMenuWidget.jsm
+++ b/browser/devtools/shared/widgets/SideMenuWidget.jsm
@@ -209,17 +209,17 @@ SideMenuWidget.prototype = {
    *        The element to make visible.
    */
   ensureElementIsVisible: function(aElement) {
     if (!aElement) {
       return;
     }
 
     // Ensure the element is visible but not scrolled horizontally.
-    let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+    let boxObject = this._list.boxObject;
     boxObject.ensureElementIsVisible(aElement);
     boxObject.scrollBy(-this._list.clientWidth, 0);
   },
 
   /**
    * Shows all the groups, even the ones with no visible children.
    */
   showEmptyGroups: function() {
--- a/browser/devtools/shared/widgets/SimpleListWidget.jsm
+++ b/browser/devtools/shared/widgets/SimpleListWidget.jsm
@@ -166,17 +166,17 @@ SimpleListWidget.prototype = {
    *        The element to make visible.
    */
   ensureElementIsVisible: function(aElement) {
     if (!aElement) {
       return;
     }
 
     // Ensure the element is visible but not scrolled horizontally.
-    let boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+    let boxObject = this._list.boxObject;
     boxObject.ensureElementIsVisible(aElement);
     boxObject.scrollBy(-this._list.clientWidth, 0);
   },
 
   /**
    * Sets the text displayed permanently in this container as a header.
    * @param string aValue
    */
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -982,17 +982,17 @@ VariablesView.prototype = {
       this._parent.removeAttribute("actions-first");
     }
   },
 
   /**
    * Gets the parent node holding this view.
    * @return nsIDOMNode
    */
-  get boxObject() this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject),
+  get boxObject() this._list.boxObject,
 
   /**
    * Gets the parent node holding this view.
    * @return nsIDOMNode
    */
   get parentNode() this._parent,
 
   /**
--- a/browser/metro/base/content/bindings/bindings.xml
+++ b/browser/metro/base/content/bindings/bindings.xml
@@ -20,24 +20,21 @@
     <handlers>
       <handler event="scroll">
         <![CDATA[
           // if there no more items to insert, just return early
           if (this._items.length == 0)
             return;
 
           if (this._contentScrollHeight == -1) {
-            let scrollheight = {};
-            this.scrollBoxObject.getScrolledSize({}, scrollheight);
-            this._contentScrollHeight = scrollheight.value;
+            this._contentScrollHeight = this.scrollBoxObject.scrolledHeight;
           }
 
-          let y = {};
-          this.scrollBoxObject.getPosition({}, y);
-          let scrollRatio = (y.value + this._childrenHeight) / this._contentScrollHeight;
+          let y = this.scrollBoxObject.positionY;
+          let scrollRatio = (y + this._childrenHeight) / this._contentScrollHeight;
 
           // If we're scrolled 80% to the bottom of the list, append the next
           // set of items
           if (scrollRatio > 0.8)
             this._insertItems();
         ]]>
       </handler>
     </handlers>
--- a/browser/metro/base/content/input.js
+++ b/browser/metro/base/content/input.js
@@ -470,27 +470,27 @@ var ScrollUtils = {
     // if element is content or the startui page, get the browser scroll interface
     if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) {
       elem = Browser.selectedBrowser;
     }
     for (; elem; elem = elem.parentNode) {
       try {
         if (elem.anonScrollBox) {
           scrollbox = elem.anonScrollBox;
-          qinterface = scrollbox.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+          qinterface = scrollbox.boxObject;
         } else if (elem.scrollBoxObject) {
           scrollbox = elem;
           qinterface = elem.scrollBoxObject;
           break;
         } else if (elem.customDragger) {
           scrollbox = elem;
           break;
         } else if (elem.boxObject) {
           let qi = (elem._cachedSBO) ? elem._cachedSBO
-                                     : elem.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+                                     : elem.boxObject;
           if (qi) {
             scrollbox = elem;
             scrollbox._cachedSBO = qinterface = qi;
             break;
           }
         }
       } catch (e) { /* we aren't here to deal with your exceptions, we'll just keep
                        traversing until we find something more well-behaved, as we
@@ -507,50 +507,43 @@ var ScrollUtils = {
 
   /**
    * The default dragger object used by TouchModule when dragging a scrollable
    * element that provides no customDragger.  Simply performs the expected
    * regular scrollBy calls on the scroller.
    */
   _defaultDragger: {
     isDraggable: function isDraggable(target, scroller) {
-      let sX = {}, sY = {},
-          pX = {}, pY = {};
-      scroller.getPosition(pX, pY);
-      scroller.getScrolledSize(sX, sY);
       let rect = target.getBoundingClientRect();
-      return { x: (sX.value > rect.width  || pX.value != 0),
-               y: (sY.value > rect.height || pY.value != 0) };
+      return { x: (scroller.scrolledWidth > rect.width   || scroller.positionX != 0),
+               y: (scroller.scrolledHeight > rect.height || scroller.positionY != 0) };
     },
 
     dragStart: function dragStart(cx, cy, target, scroller) {
       scroller.element.addEventListener("PanBegin", this._showScrollbars, false);
     },
 
     dragStop: function dragStop(dx, dy, scroller) {
       scroller.element.removeEventListener("PanBegin", this._showScrollbars, false);
       return this.dragMove(dx, dy, scroller);
     },
 
     dragMove: function dragMove(dx, dy, scroller) {
-      if (scroller.getPosition) {
         try {
-          let oldX = {}, oldY = {};
-          scroller.getPosition(oldX, oldY);
+        let oldX = scroller.positionX,
+            oldY = scroller.positionY;
 
           scroller.scrollBy(dx, dy);
 
-          let newX = {}, newY = {};
-          scroller.getPosition(newX, newY);
+        let newX = scroller.positionX,
+            newY = scroller.positionY;
 
-          return (newX.value != oldX.value) || (newY.value != oldY.value);
+        return (newX != oldX) || (newY != oldY);
 
         } catch (e) { /* we have no time for whiny scrollers! */ }
-      }
-
       return false;
     },
 
     _showScrollbars: function _showScrollbars(aEvent) {
       let scrollbox = aEvent.target;
       scrollbox.setAttribute("panning", "true");
 
       let hideScrollbars = function() {
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -6,16 +6,17 @@
 
 this.EXPORTED_SYMBOLS = ["UITour"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ResetProfile",
   "resource://gre/modules/ResetProfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
@@ -34,16 +35,19 @@ const BUCKET_TIMESTEPS    = [
   3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive.
   10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive.
   60 * 60 * 1000, // Until 1 hour after tab is closed/inactive.
 ];
 
 // Time after which seen Page IDs expire.
 const SEENPAGEID_EXPIRY  = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks.
 
+// Prefix for any target matching a search engine.
+const TARGET_SEARCHENGINE_PREFIX = "searchEngine-";
+
 
 this.UITour = {
   url: null,
   seenPageIDs: null,
   pageIDSourceTabs: new WeakMap(),
   pageIDSourceWindows: new WeakMap(),
   /* Map from browser windows to a set of tabs in which a tour is open */
   originTabs: new WeakMap(),
@@ -370,17 +374,20 @@ this.UITour = {
       }
 
       case "removePinnedTab": {
         this.removePinnedTab(window);
         break;
       }
 
       case "showMenu": {
-        this.showMenu(window, data.name);
+        this.showMenu(window, data.name, () => {
+          if (typeof data.showCallbackID == "string")
+            this.sendPageCallback(contentDocument, data.showCallbackID);
+        });
         break;
       }
 
       case "hideMenu": {
         this.hideMenu(window, data.name);
         break;
       }
 
@@ -680,16 +687,21 @@ this.UITour = {
     if (aTargetName == "pinnedTab") {
       deferred.resolve({
           targetName: aTargetName,
           node: this.ensurePinnedTab(aWindow, aSticky)
       });
       return deferred.promise;
     }
 
+    if (aTargetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) {
+      let engineID = aTargetName.slice(TARGET_SEARCHENGINE_PREFIX.length);
+      return this.getSearchEngineTarget(aWindow, engineID);
+    }
+
     let targetObject = this.targets.get(aTargetName);
     if (!targetObject) {
       deferred.reject("The specified target name is not in the allowed set");
       return deferred.promise;
     }
 
     let targetQuery = targetObject.query;
     aWindow.PanelUI.ensureReady().then(() => {
@@ -812,35 +824,49 @@ this.UITour = {
   },
 
   /**
    * @param aTarget    The element to highlight.
    * @param aEffect    (optional) The effect to use from UITour.highlightEffects or "none".
    * @see UITour.highlightEffects
    */
   showHighlight: function(aTarget, aEffect = "none") {
-    function showHighlightPanel(aTargetEl) {
-      let highlighter = aTargetEl.ownerDocument.getElementById("UITourHighlight");
+    let window = aTarget.node.ownerDocument.defaultView;
+
+    function showHighlightPanel() {
+      if (aTarget.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX)) {
+        // This won't affect normal higlights done via the panel, so we need to
+        // manually hide those.
+        this.hideHighlight(window);
+        aTarget.node.setAttribute("_moz-menuactive", true);
+        return;
+      }
+
+      // Conversely, highlights for search engines are highlighted via CSS
+      // rather than a panel, so need to be manually removed.
+      this._hideSearchEngineHighlight(window);
+
+      let highlighter = aTarget.node.ownerDocument.getElementById("UITourHighlight");
 
       let effect = aEffect;
       if (effect == "random") {
         // Exclude "random" from the randomly selected effects.
         let randomEffect = 1 + Math.floor(Math.random() * (this.highlightEffects.length - 1));
         if (randomEffect == this.highlightEffects.length)
           randomEffect--; // On the order of 1 in 2^62 chance of this happening.
         effect = this.highlightEffects[randomEffect];
       }
       // Toggle the effect attribute to "none" and flush layout before setting it so the effect plays.
       highlighter.setAttribute("active", "none");
-      aTargetEl.ownerDocument.defaultView.getComputedStyle(highlighter).animationName;
+      aTarget.node.ownerDocument.defaultView.getComputedStyle(highlighter).animationName;
       highlighter.setAttribute("active", effect);
       highlighter.parentElement.setAttribute("targetName", aTarget.targetName);
       highlighter.parentElement.hidden = false;
 
-      let targetRect = aTargetEl.getBoundingClientRect();
+      let targetRect = aTarget.node.getBoundingClientRect();
       let highlightHeight = targetRect.height;
       let highlightWidth = targetRect.width;
       let minDimension = Math.min(highlightHeight, highlightWidth);
       let maxDimension = Math.max(highlightHeight, highlightWidth);
 
       // If the dimensions are within 200% of each other (to include the bookmarks button),
       // make the highlight a circle with the largest dimension as the diameter.
       if (maxDimension / minDimension <= 3.0) {
@@ -854,52 +880,69 @@ this.UITour = {
       highlighter.style.width = highlightWidth + "px";
 
       // Close a previous highlight so we can relocate the panel.
       if (highlighter.parentElement.state == "showing" || highlighter.parentElement.state == "open") {
         highlighter.parentElement.hidePopup();
       }
       /* The "overlap" position anchors from the top-left but we want to centre highlights at their
          minimum size. */
-      let highlightWindow = aTargetEl.ownerDocument.defaultView;
+      let highlightWindow = aTarget.node.ownerDocument.defaultView;
       let containerStyle = highlightWindow.getComputedStyle(highlighter.parentElement);
       let paddingTopPx = 0 - parseFloat(containerStyle.paddingTop);
       let paddingLeftPx = 0 - parseFloat(containerStyle.paddingLeft);
       let highlightStyle = highlightWindow.getComputedStyle(highlighter);
       let highlightHeightWithMin = Math.max(highlightHeight, parseFloat(highlightStyle.minHeight));
       let highlightWidthWithMin = Math.max(highlightWidth, parseFloat(highlightStyle.minWidth));
       let offsetX = paddingTopPx
                       - (Math.max(0, highlightWidthWithMin - targetRect.width) / 2);
       let offsetY = paddingLeftPx
                       - (Math.max(0, highlightHeightWithMin - targetRect.height) / 2);
-
       this._addAnnotationPanelMutationObserver(highlighter.parentElement);
-      highlighter.parentElement.openPopup(aTargetEl, "overlap", offsetX, offsetY);
+      highlighter.parentElement.openPopup(aTarget.node, "overlap", offsetX, offsetY);
     }
 
     // Prevent showing a panel at an undefined position.
     if (!this.isElementVisible(aTarget.node))
       return;
 
     this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
                                        this.targetIsInAppMenu(aTarget),
-                                       showHighlightPanel.bind(this, aTarget.node));
+                                       showHighlightPanel.bind(this));
   },
 
   hideHighlight: function(aWindow) {
     let tabData = this.pinnedTabs.get(aWindow);
     if (tabData && !tabData.sticky)
       this.removePinnedTab(aWindow);
 
     let highlighter = aWindow.document.getElementById("UITourHighlight");
     this._removeAnnotationPanelMutationObserver(highlighter.parentElement);
     highlighter.parentElement.hidePopup();
     highlighter.removeAttribute("active");
 
     this._setAppMenuStateForAnnotation(aWindow, "highlight", false);
+    this._hideSearchEngineHighlight(aWindow);
+  },
+
+  _hideSearchEngineHighlight: function(aWindow) {
+    // We special case highlighting items in the search engines dropdown,
+    // so just blindly remove any highlight there.
+    let searchMenuBtn = null;
+    try {
+      searchMenuBtn = this.targets.get("searchProvider").query(aWindow.document);
+    } catch (e) { /* This is ok to fail. */ }
+    if (searchMenuBtn) {
+      let searchPopup = aWindow.document
+                               .getAnonymousElementByAttribute(searchMenuBtn,
+                                                               "anonid",
+                                                               "searchbar-popup");
+      for (let menuItem of searchPopup.children)
+        menuItem.removeAttribute("_moz-menuactive");
+    }
   },
 
   /**
    * Show an info panel.
    *
    * @param {Document} aContentDocument
    * @param {Node}     aAnchor
    * @param {String}   [aTitle=""]
@@ -989,16 +1032,21 @@ this.UITour = {
       this._addAnnotationPanelMutationObserver(tooltip);
       tooltip.openPopup(aAnchorEl, alignment);
     }
 
     // Prevent showing a panel at an undefined position.
     if (!this.isElementVisible(aAnchor.node))
       return;
 
+    // Due to a platform limitation, we can't anchor a panel to an element in a
+    // <menupopup>. So we can't support showing info panels for search engines.
+    if (aAnchor.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX))
+      return;
+
     this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info",
                                        this.targetIsInAppMenu(aAnchor),
                                        showInfoPanel.bind(this, aAnchor.node));
   },
 
   hideInfo: function(aWindow) {
     let document = aWindow.document;
 
@@ -1008,25 +1056,25 @@ this.UITour = {
     this._setAppMenuStateForAnnotation(aWindow, "info", false);
 
     let tooltipButtons = document.getElementById("UITourTooltipButtons");
     while (tooltipButtons.firstChild)
       tooltipButtons.firstChild.remove();
   },
 
   showMenu: function(aWindow, aMenuName, aOpenCallback = null) {
-    function openMenuButton(aID) {
-      let menuBtn = aWindow.document.getElementById(aID);
-      if (!menuBtn || !menuBtn.boxObject) {
-        aOpenCallback();
+    function openMenuButton(aMenuBtn) {
+      if (!aMenuBtn || !aMenuBtn.boxObject || aMenuBtn.open) {
+        if (aOpenCallback)
+          aOpenCallback();
         return;
       }
       if (aOpenCallback)
-        menuBtn.addEventListener("popupshown", onPopupShown);
-      menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(true);
+        aMenuBtn.addEventListener("popupshown", onPopupShown);
+      aMenuBtn.boxObject.openMenu(true);
     }
     function onPopupShown(event) {
       this.removeEventListener("popupshown", onPopupShown);
       aOpenCallback(event);
     }
 
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.panel.setAttribute("noautohide", "true");
@@ -1036,33 +1084,41 @@ this.UITour = {
       }
       aWindow.PanelUI.panel.addEventListener("popuphiding", this.hidePanelAnnotations);
       aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations);
       if (aOpenCallback) {
         aWindow.PanelUI.panel.addEventListener("popupshown", onPopupShown);
       }
       aWindow.PanelUI.show();
     } else if (aMenuName == "bookmarks") {
-      openMenuButton("bookmarks-menu-button");
+      let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
+      openMenuButton(menuBtn);
+    } else if (aMenuName == "searchEngines") {
+      this.getTarget(aWindow, "searchProvider").then(target => {
+        openMenuButton(target.node);
+      }).catch(Cu.reportError);
     }
   },
 
   hideMenu: function(aWindow, aMenuName) {
-    function closeMenuButton(aID) {
-      let menuBtn = aWindow.document.getElementById(aID);
-      if (menuBtn && menuBtn.boxObject)
-        menuBtn.boxObject.QueryInterface(Ci.nsIMenuBoxObject).openMenu(false);
+    function closeMenuButton(aMenuBtn) {
+      if (aMenuBtn && aMenuBtn.boxObject)
+        aMenuBtn.boxObject.openMenu(false);
     }
 
     if (aMenuName == "appMenu") {
       aWindow.PanelUI.panel.removeAttribute("noautohide");
       aWindow.PanelUI.hide();
       this.recreatePopup(aWindow.PanelUI.panel);
     } else if (aMenuName == "bookmarks") {
-      closeMenuButton("bookmarks-menu-button");
+      let menuBtn = aWindow.document.getElementById("bookmarks-menu-button");
+      closeMenuButton(menuBtn);
+    } else if (aMenuName == "searchEngines") {
+      let menuBtn = this.targets.get("searchProvider").query(aWindow.document);
+      closeMenuButton(menuBtn);
     }
   },
 
   hidePanelAnnotations: function(aEvent) {
     let win = aEvent.target.ownerDocument.defaultView;
     let annotationElements = new Map([
       // [annotationElement (panel), method to hide the annotation]
       [win.document.getElementById("UITourHighlightContainer"), UITour.hideHighlight.bind(UITour)],
@@ -1153,41 +1209,49 @@ this.UITour = {
         break;
       default:
         Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
         break;
     }
   },
 
   getAvailableTargets: function(aContentDocument, aCallbackID) {
-    let window = this.getChromeWindow(aContentDocument);
-    let data = this.availableTargetsCache.get(window);
-    if (data) {
-      this.sendPageCallback(aContentDocument, aCallbackID, data);
-      return;
-    }
+    Task.spawn(function*() {
+      let window = this.getChromeWindow(aContentDocument);
+      let data = this.availableTargetsCache.get(window);
+      if (data) {
+        this.sendPageCallback(aContentDocument, aCallbackID, data);
+        return;
+      }
 
-    let promises = [];
-    for (let targetName of this.targets.keys()) {
-      promises.push(this.getTarget(window, targetName));
-    }
-    Promise.all(promises).then((targetObjects) => {
+      let promises = [];
+      for (let targetName of this.targets.keys()) {
+        promises.push(this.getTarget(window, targetName));
+      }
+      let targetObjects = yield Promise.all(promises);
+
       let targetNames = [
         "pinnedTab",
       ];
+
       for (let targetObject of targetObjects) {
         if (targetObject.node)
           targetNames.push(targetObject.targetName);
       }
-      let data = {
+
+      targetNames = targetNames.concat(
+        yield this.getAvailableSearchEngineTargets(window)
+      );
+
+      data = {
         targets: targetNames,
       };
       this.availableTargetsCache.set(window, data);
       this.sendPageCallback(aContentDocument, aCallbackID, data);
-    }, (err) => {
+    }.bind(this)).catch(err => {
       Cu.reportError(err);
       this.sendPageCallback(aContentDocument, aCallbackID, {
         targets: [],
       });
     });
   },
 
   addNavBarWidget: function (aTarget, aContentDocument, aCallbackID) {
@@ -1243,11 +1307,60 @@ this.UITour = {
   _annotationMutationCallback: function(aMutations) {
     for (let mutation of aMutations) {
       // Remove both attributes at once and ignore remaining mutations to be proccessed.
       mutation.target.removeAttribute("width");
       mutation.target.removeAttribute("height");
       return;
     }
   },
+
+  getAvailableSearchEngineTargets(aWindow) {
+    return new Promise(resolve => {
+      this.getTarget(aWindow, "search").then(searchTarget => {
+        if (!searchTarget.node || this.targetIsInAppMenu(searchTarget))
+          return resolve([]);
+
+        Services.search.init(() => {
+          let engines = Services.search.getVisibleEngines();
+          resolve([TARGET_SEARCHENGINE_PREFIX + engine.identifier
+                   for (engine of engines)
+                   if (engine.identifier)]);
+        });
+      }).catch(() => resolve([]));
+    });
+  },
+
+  // We only allow matching based on a search engine's identifier - this gives
+  // us a non-changing ID and guarentees we only match against app-provided
+  // engines.
+  getSearchEngineTarget(aWindow, aIdentifier) {
+    return new Promise((resolve, reject) => {
+      Task.spawn(function*() {
+        let searchTarget = yield this.getTarget(aWindow, "search");
+        // We're not supporting having the searchbar in the app-menu, because
+        // popups within popups gets crazy. This restriction should be lifted
+        // once bug 988151 is implemented, as the page can then be responsible
+        // for opening each menu when appropriate.
+        if (!searchTarget.node || this.targetIsInAppMenu(searchTarget))
+          return reject("Search engine not available");
+
+        yield Services.search.init();
+
+        let searchPopup = searchTarget.node._popup;
+        for (let engineNode of searchPopup.children) {
+          let engine = engineNode.engine;
+          if (engine && engine.identifier == aIdentifier) {
+            return resolve({
+              targetName: TARGET_SEARCHENGINE_PREFIX + engine.identifier,
+              node: engineNode,
+            });
+          }
+        }
+        reject("Search engine not available");
+      }.bind(this)).catch(() => {
+        reject("Search engine not available");
+      });
+    });
+  }
 };
 
 this.UITour.init();
--- a/browser/modules/test/browser_UITour.js
+++ b/browser/modules/test/browser_UITour.js
@@ -190,16 +190,63 @@ let tests = [
     }
 
     let highlight = document.getElementById("UITourHighlight");
     is_element_hidden(highlight, "Highlight should initially be hidden");
 
     gContentAPI.showHighlight("urlbar");
     waitForElementToBeVisible(highlight, checkDefaultEffect, "Highlight should be shown after showHighlight()");
   },
+  function test_highlight_search_engine(done) {
+    let highlight = document.getElementById("UITourHighlight");
+    gContentAPI.showHighlight("urlbar");
+    waitForElementToBeVisible(highlight, () => {
+
+      gContentAPI.showMenu("searchEngines", function() {
+        let searchbar = document.getElementById("searchbar");
+        isnot(searchbar, null, "Should have found searchbar");
+        let searchPopup = document.getAnonymousElementByAttribute(searchbar,
+                                                                   "anonid",
+                                                                   "searchbar-popup");
+        isnot(searchPopup, null, "Should have found search popup");
+
+        function getEngineNode(identifier) {
+          let engineNode = null;
+          for (let node of searchPopup.children) {
+            if (node.engine.identifier == identifier) {
+              engineNode = node;
+              break;
+            }
+          }
+          isnot(engineNode, null, "Should have found search engine node in popup");
+          return engineNode;
+        }
+        let googleEngineNode = getEngineNode("google");
+        let bingEngineNode = getEngineNode("bing");
+
+        gContentAPI.showHighlight("searchEngine-google");
+        waitForCondition(() => googleEngineNode.getAttribute("_moz-menuactive") == "true", function() {
+          is_element_hidden(highlight, "Highlight panel should be hidden by highlighting search engine");
+
+          gContentAPI.showHighlight("searchEngine-bing");
+          waitForCondition(() => bingEngineNode.getAttribute("_moz-menuactive") == "true", function() {
+            isnot(googleEngineNode.getAttribute("_moz-menuactive"), "true", "Previous engine should no longer be highlighted");
+
+            gContentAPI.hideHighlight();
+            waitForCondition(() => bingEngineNode.getAttribute("_moz-menuactive") != "true", function() {
+              gContentAPI.hideMenu("searchEngines");
+              waitForCondition(() => searchPopup.state == "closed", function() {
+                done();
+              }, "Search dropdown should close");
+            }, "Menu item should get attribute removed");
+          }, "Menu item should get attribute to make it look active");
+        });
+      });
+    });
+  },
   function test_highlight_effect_unsupported(done) {
     function checkUnsupportedEffect() {
       is(highlight.getAttribute("active"), "none", "No effect should be used when an unsupported effect is requested");
       done();
     }
 
     let highlight = document.getElementById("UITourHighlight");
     is_element_hidden(highlight, "Highlight should initially be hidden");
--- a/browser/modules/test/browser_UITour_availableTargets.js
+++ b/browser/modules/test/browser_UITour_availableTargets.js
@@ -9,16 +9,23 @@ let gContentWindow;
 
 Components.utils.import("resource:///modules/UITour.jsm");
 
 function test() {
   requestLongerTimeout(2);
   UITourTest();
 }
 
+function searchEngineTargets() {
+  let engines = Services.search.getVisibleEngines();
+  return ["searchEngine-" + engine.identifier
+          for (engine of engines)
+          if (engine.identifier)];
+}
+
 let tests = [
   function test_availableTargets(done) {
     gContentAPI.getConfiguration("availableTargets", (data) => {
       ok_targets(data, [
         "accountStatus",
         "addons",
         "appMenu",
         "backForward",
@@ -28,17 +35,17 @@ let tests = [
         "home",
         "loop",
         "pinnedTab",
         "privateWindow",
         "quit",
         "search",
         "searchProvider",
         "urlbar",
-      ]);
+      ].concat(searchEngineTargets()));
       ok(UITour.availableTargetsCache.has(window),
          "Targets should now be cached");
       done();
     });
   },
 
   function test_availableTargets_changeWidgets(done) {
     CustomizableUI.removeWidgetFromArea("bookmarks-menu-button");
@@ -55,17 +62,17 @@ let tests = [
         "loop",
         "home",
         "pinnedTab",
         "privateWindow",
         "quit",
         "search",
         "searchProvider",
         "urlbar",
-      ]);
+      ].concat(searchEngineTargets()));
       ok(UITour.availableTargetsCache.has(window),
          "Targets should now be cached again");
       CustomizableUI.reset();
       ok(!UITour.availableTargetsCache.has(window),
          "Targets should not be cached after reset");
       done();
     });
   },
--- a/browser/modules/test/uitour.js
+++ b/browser/modules/test/uitour.js
@@ -1,21 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// Copied from the proposed JS library for Bedrock (ie, www.mozilla.org).
-
 // create namespace
 if (typeof Mozilla == 'undefined') {
 	var Mozilla = {};
 }
 
-(function($) {
-  'use strict';
+;(function($) {
+	'use strict';
 
 	// create namespace
 	if (typeof Mozilla.UITour == 'undefined') {
 		Mozilla.UITour = {};
 	}
 
 	var themeIntervalId = null;
 	function _stopCyclingThemes() {
@@ -55,16 +53,19 @@ if (typeof Mozilla == 'undefined') {
 		}
 		document.addEventListener("mozUITourResponse", listener);
 
 		return id;
 	}
 
 	Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY = 10 * 1000;
 
+	Mozilla.UITour.CONFIGNAME_SYNC = "sync";
+	Mozilla.UITour.CONFIGNAME_AVAILABLETARGETS = "availableTargets";
+
 	Mozilla.UITour.registerPageID = function(pageID) {
 		_sendEvent('registerPageID', {
 			pageID: pageID
 		});
 	};
 
 	Mozilla.UITour.showHighlight = function(target, effect) {
 		_sendEvent('showHighlight', {
@@ -81,17 +82,17 @@ if (typeof Mozilla == 'undefined') {
 		var buttonData = [];
 		if (Array.isArray(buttons)) {
 			for (var i = 0; i < buttons.length; i++) {
 				buttonData.push({
 					label: buttons[i].label,
 					icon: buttons[i].icon,
 					style: buttons[i].style,
 					callbackID: _waitForCallback(buttons[i].callback)
-			});
+				});
 			}
 		}
 
 		var closeButtonCallbackID, targetCallbackID;
 		if (options && options.closeButtonCallback)
 			closeButtonCallbackID = _waitForCallback(options.closeButtonCallback);
 		if (options && options.targetCallback)
 			targetCallbackID = _waitForCallback(options.targetCallback);
@@ -151,28 +152,44 @@ if (typeof Mozilla == 'undefined') {
 	Mozilla.UITour.addPinnedTab = function() {
 		_sendEvent('addPinnedTab');
 	};
 
 	Mozilla.UITour.removePinnedTab = function() {
 		_sendEvent('removePinnedTab');
 	};
 
-	Mozilla.UITour.showMenu = function(name) {
+	Mozilla.UITour.showMenu = function(name, callback) {
+		var showCallbackID;
+		if (callback)
+			showCallbackID = _waitForCallback(callback);
+
 		_sendEvent('showMenu', {
-			name: name
+			name: name,
+			showCallbackID: showCallbackID,
 		});
 	};
 
 	Mozilla.UITour.hideMenu = function(name) {
 		_sendEvent('hideMenu', {
 			name: name
 		});
 	};
 
+	Mozilla.UITour.startUrlbarCapture = function(text, url) {
+		_sendEvent('startUrlbarCapture', {
+			text: text,
+			url: url
+		});
+	};
+
+	Mozilla.UITour.endUrlbarCapture = function() {
+		_sendEvent('endUrlbarCapture');
+	};
+
 	Mozilla.UITour.getConfiguration = function(configName, callback) {
 		_sendEvent('getConfiguration', {
 			callbackID: _waitForCallback(callback),
 			configuration: configName,
 		});
 	};
 
 	Mozilla.UITour.showFirefoxAccounts = function() {
deleted file mode 100644
--- a/build/autoconf/ccache.m4
+++ /dev/null
@@ -1,36 +0,0 @@
-dnl This Source Code Form is subject to the terms of the Mozilla Public
-dnl License, v. 2.0. If a copy of the MPL was not distributed with this
-dnl file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-dnl ======================================================
-dnl = Enable compiling with ccache
-dnl ======================================================
-AC_DEFUN([MOZ_CHECK_CCACHE],
-[
-MOZ_ARG_WITH_STRING(ccache,
-[  --with-ccache[=path/to/ccache]
-                          Enable compiling with ccache],
-    CCACHE=$withval, CCACHE="no")
-
-if test "$CCACHE" != "no"; then
-    if test -z "$CCACHE" -o "$CCACHE" = "yes"; then
-        CCACHE=
-    else
-        if test ! -e "$CCACHE"; then
-            AC_MSG_ERROR([$CCACHE not found])
-        fi
-    fi
-    MOZ_PATH_PROGS(CCACHE, $CCACHE ccache)
-    if test -z "$CCACHE" -o "$CCACHE" = ":"; then
-        AC_MSG_ERROR([ccache not found])
-    elif test -x "$CCACHE"; then
-        CC="$CCACHE $CC"
-        CXX="$CCACHE $CXX"
-        MOZ_USING_CCACHE=1
-    else
-        AC_MSG_ERROR([$CCACHE is not executable])
-    fi
-fi
-
-AC_SUBST(MOZ_USING_CCACHE)
-])
--- a/build/autoconf/nspr-build.m4
+++ b/build/autoconf/nspr-build.m4
@@ -192,16 +192,21 @@ AC_SUBST(NSPR_PKGCONF_CHECK)
 fi # _IS_OUTER_CONFIGURE
 
 ])
 
 AC_DEFUN([MOZ_SUBCONFIGURE_NSPR], [
 
 if test -z "$MOZ_NATIVE_NSPR"; then
     ac_configure_args="$_SUBDIR_CONFIG_ARGS --with-dist-prefix=$MOZ_BUILD_ROOT/dist --with-mozilla"
+    if test -n "$MOZ_USING_CCACHE"; then
+        # Avoid double prepending ccache by omitting --with-ccache in building NSPR.
+        ac_configure_args="`echo $ac_configure_args | sed -e 's/--with-ccache[[^ ]]*//'`"
+    fi
+
     if test -z "$MOZ_DEBUG"; then
         ac_configure_args="$ac_configure_args --disable-debug"
     else
         ac_configure_args="$ac_configure_args --enable-debug"
         if test -n "$MOZ_NO_DEBUG_RTL"; then
             ac_configure_args="$ac_configure_args --disable-debug-rtl"
         fi
     fi
--- a/build/autoconf/wrapper.m4
+++ b/build/autoconf/wrapper.m4
@@ -7,16 +7,46 @@ dnl = Enable compiling with various comp
 dnl =======================================================================
 AC_DEFUN([MOZ_CHECK_COMPILER_WRAPPER],
 [
 MOZ_ARG_WITH_STRING(compiler_wrapper,
 [  --with-compiler-wrapper[=path/to/wrapper]
     Enable compiling with wrappers such as distcc and ccache],
     COMPILER_WRAPPER=$withval, COMPILER_WRAPPER="no")
 
+MOZ_ARG_WITH_STRING(ccache,
+[  --with-ccache[=path/to/ccache]
+                          Enable compiling with ccache],
+    CCACHE=$withval, CCACHE="no")
+
+if test "$CCACHE" != "no"; then
+    if test -z "$CCACHE" -o "$CCACHE" = "yes"; then
+        CCACHE=
+    else
+        if test ! -e "$CCACHE"; then
+            AC_MSG_ERROR([$CCACHE not found])
+        fi
+    fi
+    MOZ_PATH_PROGS(CCACHE, $CCACHE ccache)
+    if test -z "$CCACHE" -o "$CCACHE" = ":"; then
+        AC_MSG_ERROR([ccache not found])
+    elif test -x "$CCACHE"; then
+        if test "$COMPILER_WRAPPER" != "no"; then
+            COMPILER_WRAPPER="$CCACHE $COMPILER_WRAPPER"
+        else
+            COMPILER_WRAPPER="$CCACHE"
+        fi
+        MOZ_USING_CCACHE=1
+    else
+        AC_MSG_ERROR([$CCACHE is not executable])
+    fi
+fi
+
+AC_SUBST(MOZ_USING_CCACHE)
+
 if test "$COMPILER_WRAPPER" != "no"; then
     case "$target" in
     *-mingw*)
         dnl When giving a windows path with backslashes, js/src/configure
         dnl fails because of double wrapping because the test further below
         dnl doesn't work with backslashes. While fixing that test to work
         dnl might seem better, a lot of the make build backend actually
         dnl doesn't like backslashes, so normalize windows paths to use
--- a/client.mk
+++ b/client.mk
@@ -86,16 +86,19 @@ ifneq (,$(findstring mingw,$(CONFIG_GUES
 
 # check for CRLF line endings
 ifneq (0,$(shell $(PERL) -e 'binmode(STDIN); while (<STDIN>) { if (/\r/) { print "1"; exit } } print "0"' < $(TOPSRCDIR)/client.mk))
 $(error This source tree appears to have Windows-style line endings. To \
 convert it to Unix-style line endings, check \
 "https://developer.mozilla.org/en-US/docs/Developer_Guide/Mozilla_build_FAQ\#Win32-specific_questions" \
 for a workaround of this issue.)
 endif
+
+# Set this for baseconfig.mk
+HOST_OS_ARCH=WINNT
 endif
 
 ####################################
 # Load mozconfig Options
 
 # See build pages, http://www.mozilla.org/build/ for how to set up mozconfig.
 
 define CR
@@ -162,16 +165,19 @@ OBJDIR_TARGETS = install export libs cle
 
 #######################################################################
 # Rules
 
 # The default rule is build
 build::
 	$(MAKE) -f $(TOPSRCDIR)/client.mk $(if $(MOZ_PGO),profiledbuild,realbuild) CREATE_MOZCONFIG_JSON=
 
+# Include baseconfig.mk for its $(MAKE) validation.
+include $(TOPSRCDIR)/config/baseconfig.mk
+
 # Define mkdir
 include $(TOPSRCDIR)/config/makefiles/makeutils.mk
 include $(TOPSRCDIR)/config/makefiles/autotargets.mk
 
 # Create a makefile containing the mk_add_options values from mozconfig,
 # but only do so when OBJDIR is defined (see further above).
 ifdef MOZ_BUILD_PROJECTS
 ifdef MOZ_CURRENT_PROJECT
--- a/configure.in
+++ b/configure.in
@@ -7322,18 +7322,16 @@ dnl ====================================
 MOZ_ARG_ENABLE_BOOL(gczeal,
 [  --enable-gczeal         Enable zealous JavaScript GCing],
     JS_GC_ZEAL=1,
     JS_GC_ZEAL= )
 if test -n "$JS_GC_ZEAL" -o -n "$MOZ_DEBUG"; then
     AC_DEFINE(JS_GC_ZEAL)
 fi
 
-MOZ_CHECK_CCACHE
-
 dnl ========================================================
 dnl = Enable static checking using gcc-dehydra
 dnl ========================================================
 
 MOZ_ARG_WITH_STRING(static-checking,
 [  --with-static-checking=path/to/gcc_dehydra.so
                           Enable static checking of code using GCC-dehydra],
     DEHYDRA_PATH=$withval,
@@ -9187,21 +9185,16 @@ if test -n "$_subconfigure_subdir"; then
   srcdir="$_save_srcdir"
 fi
 
 # No need to run subconfigures when building with LIBXUL_SDK_DIR
 if test "$COMPILE_ENVIRONMENT" -a -z "$LIBXUL_SDK_DIR"; then
 
 export WRAP_LDFLAGS
 
-if test -n "$MOZ_USING_CCACHE"; then
-    # Avoid double prepending ccache by omitting --with-ccache in building NSPR.
-    _SUBDIR_CONFIG_ARGS="`echo $_SUBDIR_CONFIG_ARGS | sed -e 's/--with-ccache[[^ ]]*//'`"
-fi
-
 MOZ_SUBCONFIGURE_NSPR()
 
 dnl ========================================================
 dnl = Setup a nice relatively clean build environment for
 dnl = sub-configures.
 dnl ========================================================
 CC="$_SUBDIR_CC"
 CXX="$_SUBDIR_CXX"
--- a/content/base/public/Element.h
+++ b/content/base/public/Element.h
@@ -128,18 +128,18 @@ class AnimationPlayer;
 class Link;
 class UndoManager;
 class DOMRect;
 class DOMRectList;
 class DestinationInsertionPointList;
 
 // IID for the dom::Element interface
 #define NS_ELEMENT_IID \
-{ 0xb0135f9d, 0xa476, 0x4711, \
-  { 0x8b, 0xb9, 0xca, 0xe5, 0x2a, 0x05, 0xf9, 0xbe } }
+{ 0xaa79cb98, 0xc785, 0x44c5, \
+  { 0x80, 0x80, 0x2e, 0x5f, 0x0c, 0xa5, 0xbd, 0x63 } }
 
 class Element : public FragmentOrElement
 {
 public:
 #ifdef MOZILLA_INTERNAL_API
   explicit Element(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo) :
     FragmentOrElement(aNodeInfo),
     mState(NS_EVENT_STATE_MOZ_READONLY)
@@ -669,16 +669,20 @@ public:
   }
   void SetPointerCapture(int32_t aPointerId, ErrorResult& aError)
   {
     bool activeState = false;
     if (!nsIPresShell::GetPointerInfo(aPointerId, activeState)) {
       aError.Throw(NS_ERROR_DOM_INVALID_POINTER_ERR);
       return;
     }
+    if (!IsInDoc()) {
+      aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+      return;
+    }
     if (!activeState) {
       return;
     }
     nsIPresShell::SetPointerCapturingContent(aPointerId, this);
   }
   void ReleasePointerCapture(int32_t aPointerId, ErrorResult& aError)
   {
     bool activeState = false;
--- a/content/base/public/File.h
+++ b/content/base/public/File.h
@@ -86,21 +86,27 @@ public:
   static already_AddRefed<File>
   Create(nsISupports* aParent, const nsAString& aContentType,
          uint64_t aLength);
 
   static already_AddRefed<File>
   Create(nsISupports* aParent, const nsAString& aContentType, uint64_t aStart,
          uint64_t aLength);
 
+  // The returned File takes ownership of aMemoryBuffer. aMemoryBuffer will be
+  // freed by moz_free so it must be allocated by moz_malloc or something
+  // compatible with it.
   static already_AddRefed<File>
   CreateMemoryFile(nsISupports* aParent, void* aMemoryBuffer, uint64_t aLength,
                    const nsAString& aName, const nsAString& aContentType,
                    uint64_t aLastModifiedDate);
 
+  // The returned File takes ownership of aMemoryBuffer. aMemoryBuffer will be
+  // freed by moz_free so it must be allocated by moz_malloc or something
+  // compatible with it.
   static already_AddRefed<File>
   CreateMemoryFile(nsISupports* aParent, void* aMemoryBuffer, uint64_t aLength,
                    const nsAString& aContentType);
 
   static already_AddRefed<File>
   CreateTemporaryFileBlob(nsISupports* aParent, PRFileDesc* aFD,
                           uint64_t aStartPos, uint64_t aLength,
                           const nsAString& aContentType);
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -33,17 +33,16 @@ class nsIDocShell;
 class nsDocShell;
 class nsDOMNavigationTiming;
 class nsFrameLoader;
 class nsHTMLCSSStyleSheet;
 class nsHTMLDocument;
 class nsHTMLStyleSheet;
 class nsIAtom;
 class nsIBFCacheEntry;
-class nsIBoxObject;
 class nsIChannel;
 class nsIContent;
 class nsIContentSink;
 class nsIDocShell;
 class nsIDocumentEncoder;
 class nsIDocumentObserver;
 class nsIDOMDocument;
 class nsIDOMDocumentFragment;
@@ -90,16 +89,17 @@ class SVGAttrAnimationRuleProcessor;
 namespace css {
 class Loader;
 class ImageLoader;
 } // namespace css
 
 namespace dom {
 class AnimationTimeline;
 class Attr;
+class BoxObject;
 class CDATASection;
 class Comment;
 struct CustomElementDefinition;
 class DocumentFragment;
 class DocumentType;
 class DOMImplementation;
 class DOMStringList;
 class Element;
@@ -1495,17 +1495,17 @@ public:
    * @param aElement canonical nsIContent pointer of the box object's element
    */
   virtual void ClearBoxObjectFor(nsIContent *aContent) = 0;
 
   /**
    * Get the box object for an element. This is not exposed through a
    * scriptable interface except for XUL documents.
    */
-  virtual already_AddRefed<nsIBoxObject>
+  virtual already_AddRefed<mozilla::dom::BoxObject>
     GetBoxObjectFor(mozilla::dom::Element* aElement,
                     mozilla::ErrorResult& aRv) = 0;
 
   /**
    * Get the compatibility mode for this document
    */
   nsCompatibility GetCompatibilityMode() const {
     return mCompatMode;
--- a/content/base/public/nsINode.h
+++ b/content/base/public/nsINode.h
@@ -243,18 +243,18 @@ private:
 
 // Categories of node properties
 // 0 is global.
 #define DOM_USER_DATA         1
 #define SMIL_MAPPED_ATTR_ANIMVAL 2
 
 // IID for the nsINode interface
 #define NS_INODE_IID \
-{ 0x3a60353e, 0x04e5, 0x49ca, \
-  { 0x84, 0x1c, 0x59, 0xc6, 0xde, 0xe6, 0x36, 0xcc } }
+{ 0x8deda3f4, 0x0f45, 0x497a, \
+  { 0x89, 0x7c, 0xe6, 0x09, 0x12, 0x8a, 0xad, 0xd8 } }
 
 /**
  * An internal interface that abstracts some DOMNode-related parts that both
  * nsIContent and nsIDocument share.  An instance of this interface has a list
  * of nsIContent children and provides access to them.
  */
 class nsINode : public mozilla::dom::EventTarget
 {
--- a/content/base/src/ImportManager.cpp
+++ b/content/base/src/ImportManager.cpp
@@ -479,17 +479,17 @@ ImportLoader::Open()
     return;
   }
 
   nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
   rv = secMan->CheckLoadURIWithPrincipal(principal, mURI,
                                          nsIScriptSecurityManager::STANDARD);
   NS_ENSURE_SUCCESS_VOID(rv);
 
-  nsCOMPtr<nsILoadGroup> loadGroup = mImportParent->GetDocumentLoadGroup();
+  nsCOMPtr<nsILoadGroup> loadGroup = master->GetDocumentLoadGroup();
   nsCOMPtr<nsIChannelPolicy> channelPolicy;
   nsCOMPtr<nsIContentSecurityPolicy> csp;
   rv = principal->GetCsp(getter_AddRefs(csp));
   NS_ENSURE_SUCCESS_VOID(rv);
 
   if (csp) {
     channelPolicy = do_CreateInstance("@mozilla.org/nschannelpolicy;1");
     channelPolicy->SetContentSecurityPolicy(csp);
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -215,16 +215,17 @@
 #include "nsIDocumentActivity.h"
 #include "nsIStructuredCloneContainer.h"
 #include "nsIMutableArray.h"
 #include "nsContentPermissionHelper.h"
 #include "mozilla/dom/DOMStringList.h"
 #include "nsWindowMemoryReporter.h"
 #include "nsLocation.h"
 #include "mozilla/dom/FontFaceSet.h"
+#include "mozilla/dom/BoxObject.h"
 
 #ifdef MOZ_MEDIA_NAVIGATOR
 #include "mozilla/MediaManager.h"
 #endif // MOZ_MEDIA_NAVIGATOR
 #ifdef MOZ_WEBRTC
 #include "IPeerConnection.h"
 #endif // MOZ_WEBRTC
 
@@ -6942,17 +6943,17 @@ nsDocument::DoNotifyPossibleTitleChange(
   }
 
   // Fire a DOM event for the title change.
   nsContentUtils::DispatchChromeEvent(this, static_cast<nsIDocument*>(this),
                                       NS_LITERAL_STRING("DOMTitleChanged"),
                                       true, true);
 }
 
-already_AddRefed<nsIBoxObject>
+already_AddRefed<BoxObject>
 nsDocument::GetBoxObjectFor(Element* aElement, ErrorResult& aRv)
 {
   if (!aElement) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
 
   nsIDocument* doc = aElement->OwnerDoc();
@@ -6969,17 +6970,17 @@ nsDocument::GetBoxObjectFor(Element* aEl
                                     "UseOfGetBoxObjectForWarning");
   }
 
   if (!mBoxObjectTable) {
     mBoxObjectTable = new nsInterfaceHashtable<nsPtrHashKey<nsIContent>, nsPIBoxObject>(6);
   } else {
     nsCOMPtr<nsPIBoxObject> boxObject = mBoxObjectTable->Get(aElement);
     if (boxObject) {
-      return boxObject.forget();
+      return boxObject.forget().downcast<BoxObject>();
     }
   }
 
   int32_t namespaceID;
   nsCOMPtr<nsIAtom> tag = BindingManager()->ResolveTag(aElement, &namespaceID);
 
   nsAutoCString contractID("@mozilla.org/layout/xul-boxobject");
   if (namespaceID == kNameSpaceID_XUL) {
@@ -7010,17 +7011,17 @@ nsDocument::GetBoxObjectFor(Element* aEl
   }
 
   boxObject->Init(aElement);
 
   if (mBoxObjectTable) {
     mBoxObjectTable->Put(aElement, boxObject.get());
   }
 
-  return boxObject.forget();
+  return boxObject.forget().downcast<BoxObject>();
 }
 
 void
 nsDocument::ClearBoxObjectFor(nsIContent* aContent)
 {
   if (mBoxObjectTable) {
     nsPIBoxObject *boxObject = mBoxObjectTable->GetWeak(aContent);
     if (boxObject) {
--- a/content/base/src/nsDocument.h
+++ b/content/base/src/nsDocument.h
@@ -25,18 +25,16 @@
 #include "nsStubDocumentObserver.h"
 #include "nsIScriptGlobalObject.h"
 #include "nsIContent.h"
 #include "nsIPrincipal.h"
 #include "nsIParser.h"
 #include "nsBindingManager.h"
 #include "nsInterfaceHashtable.h"
 #include "nsJSThingHashtable.h"
-#include "nsIBoxObject.h"
-#include "nsPIBoxObject.h"
 #include "nsIScriptObjectPrincipal.h"
 #include "nsIURI.h"
 #include "nsScriptLoader.h"
 #include "nsIRadioGroupContainer.h"
 #include "nsILayoutHistoryState.h"
 #include "nsIRequest.h"
 #include "nsILoadGroup.h"
 #include "nsTObserverArray.h"
@@ -90,20 +88,22 @@ class nsChildContentList;
 class nsHTMLStyleSheet;
 class nsHTMLCSSStyleSheet;
 class nsDOMNavigationTiming;
 class nsWindowSizes;
 class nsHtml5TreeOpExecutor;
 class nsDocumentOnStack;
 class nsPointerLockPermissionRequest;
 class nsISecurityConsoleMessage;
+class nsPIBoxObject;
 
 namespace mozilla {
 class EventChainPreVisitor;
 namespace dom {
+class BoxObject;
 class UndoManager;
 struct LifecycleCallbacks;
 class CallbackFunction;
 }
 }
 
 /**
  * Right now our identifier map entries contain information for 'name'
@@ -997,18 +997,20 @@ public:
 
   virtual void BlockOnload();
   virtual void UnblockOnload(bool aFireSync);
 
   virtual void AddStyleRelevantLink(mozilla::dom::Link* aLink);
   virtual void ForgetLink(mozilla::dom::Link* aLink);
 
   void ClearBoxObjectFor(nsIContent* aContent);
-  already_AddRefed<nsIBoxObject> GetBoxObjectFor(mozilla::dom::Element* aElement,
-                                                 mozilla::ErrorResult& aRv) MOZ_OVERRIDE;
+
+  virtual already_AddRefed<mozilla::dom::BoxObject>
+  GetBoxObjectFor(mozilla::dom::Element* aElement,
+                  mozilla::ErrorResult& aRv) MOZ_OVERRIDE;
 
   virtual Element*
     GetAnonymousElementByAttribute(nsIContent* aElement,
                                    nsIAtom* aAttrName,
                                    const nsAString& aAttrValue) const;
 
   virtual Element* ElementFromPointHelper(float aX, float aY,
                                                       bool aIgnoreRootScrollFrame,
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -491,16 +491,17 @@ GK_ATOM(itemref, "itemref")
 GK_ATOM(itemscope, "itemscope")
 GK_ATOM(itemtype, "itemtype")
 GK_ATOM(kbd, "kbd")
 GK_ATOM(noautofocus, "noautofocus")
 GK_ATOM(keepcurrentinview, "keepcurrentinview")
 GK_ATOM(keepobjectsalive, "keepobjectsalive")
 GK_ATOM(key, "key")
 GK_ATOM(keycode, "keycode")
+GK_ATOM(keyschange, "keyschange")
 GK_ATOM(keydown, "keydown")
 GK_ATOM(keygen, "keygen")
 GK_ATOM(keypress, "keypress")
 GK_ATOM(keyset, "keyset")
 GK_ATOM(keysystem, "keysystem")
 GK_ATOM(keytext, "keytext")
 GK_ATOM(keyup, "keyup")
 GK_ATOM(kind, "kind")
--- a/content/base/src/nsScriptLoader.cpp
+++ b/content/base/src/nsScriptLoader.cpp
@@ -1243,17 +1243,17 @@ nsScriptLoader::ReadyToExecuteScripts()
     nsScriptLoader* ancestor = doc->ScriptLoader();
     if (!ancestor->SelfReadyToExecuteScripts() &&
         ancestor->AddPendingChildLoader(this)) {
       AddExecuteBlocker();
       return false;
     }
   }
 
-  if (!mDocument->IsMasterDocument()) {
+  if (mDocument && !mDocument->IsMasterDocument()) {
     nsRefPtr<ImportManager> im = mDocument->ImportManager();
     nsRefPtr<ImportLoader> loader = im->Find(mDocument);
     MOZ_ASSERT(loader, "How can we have an import document without a loader?");
 
     // The referring link that counts in the execution order calculation
     // (in spec: flagged as branch)
     nsCOMPtr<nsINode> referrer = loader->GetMainReferrer();
     MOZ_ASSERT(referrer, "There has to be a main referring link for each imports");
--- a/content/base/test/test_bug527896.html
+++ b/content/base/test/test_bug527896.html
@@ -17,18 +17,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 527896 **/
 
 SimpleTest.waitForExplicitFinish();
 
-SimpleTest.expectAssertions(1);
-
 var docWrittenSrcExecuted = false;
 var scriptInsertedSrcExecuted = false;
 
 // the iframe test runs with the HTML5 parser
 
 var iframe = document.getElementsByTagName('iframe')[0];
 iframe.contentWindow.document.open();
 iframe.contentWindow.document.write("<!DOCTYPE html>");
--- a/content/html/content/src/HTMLMediaElement.cpp
+++ b/content/html/content/src/HTMLMediaElement.cpp
@@ -1159,17 +1159,18 @@ nsresult HTMLMediaElement::LoadResource(
     nsRefPtr<MediaSourceDecoder> decoder = new MediaSourceDecoder(this);
     if (!source->Attach(decoder)) {
       // TODO: Handle failure: run "If the media data cannot be fetched at
       // all, due to network errors, causing the user agent to give up
       // trying to fetch the resource" section of resource fetch algorithm.
       return NS_ERROR_FAILURE;
     }
     mMediaSource = source.forget();
-    nsRefPtr<MediaResource> resource = MediaSourceDecoder::CreateResource();
+    nsRefPtr<MediaResource> resource =
+      MediaSourceDecoder::CreateResource(mMediaSource->GetPrincipal());
     return FinishDecoderSetup(decoder, resource, nullptr, nullptr);
   }
 
   nsCOMPtr<nsILoadGroup> loadGroup = GetDocumentLoadGroup();
 
   // check for a Content Security Policy to pass down to the channel
   // created to load the media content
   nsCOMPtr<nsIChannelPolicy> channelPolicy;
--- a/content/html/content/test/forms/test_meter_element.html
+++ b/content/html/content/test/forms/test_meter_element.html
@@ -16,22 +16,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="visibility: hidden;">
   <form id='f' method='get' target='submit_frame' action='foo'>
     <meter id='m' value=0.5></meter>
   </form>
 </div>
 <pre id="test">
 <script type="application/javascript">
 
-if (navigator.platform.startsWith("Win")) {
-  SimpleTest.expectAssertions(0, 1);
-} else {
-  SimpleTest.expectAssertions(1);
-}
-
 /** Test for <meter> **/
 
 function checkFormIDLAttribute(aElement)
 {
   is('form' in aElement, false, "<meter> shouldn't have a form attribute");
 }
 
 function checkAttribute(aElement, aAttribute, aNewValue, aExpectedValueForIDL)
--- a/content/html/content/test/test_bug242709.html
+++ b/content/html/content/test/test_bug242709.html
@@ -12,18 +12,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=242709">Mozilla Bug 242709</a>
 <p id="display"></p>
 <div id="content">
 <iframe src="bug242709_iframe.html" id="a"></iframe> 
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-SimpleTest.expectAssertions(1);
-
 /** Test for Bug 242709 **/
 
 SimpleTest.waitForExplicitFinish();
 
 var submitted = function() {
   ok(true, "Disabling button after form submission doesn't prevent submitting");
   SimpleTest.finish();
 }
--- a/content/html/content/test/test_bug277890.html
+++ b/content/html/content/test/test_bug277890.html
@@ -12,18 +12,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=277890">Mozilla Bug 277890</a>
 <p id="display"></p>
 <div id="content">
 <iframe src="bug277890_iframe.html" id="a"></iframe> 
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-SimpleTest.expectAssertions(1);
-
 /** Test for Bug 277890 **/
 
 SimpleTest.waitForExplicitFinish();
 
 var submitted = function() {
   ok(true, "Disabling button after form submission doesn't prevent submitting");
   SimpleTest.finish();
 }
--- a/content/html/content/test/test_bug523771.html
+++ b/content/html/content/test/test_bug523771.html
@@ -15,22 +15,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 <form action="form_submit_server.sjs" target="target_iframe" id="form"
 method="POST" enctype="multipart/form-data">
   <input id=singleFile name=singleFile type=file>
   <input id=multiFile name=multiFile type=file multiple>
 </form>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-if (navigator.platform.startsWith("Win")) {
-  SimpleTest.expectAssertions(0, 1);
-} else {
-  SimpleTest.expectAssertions(1);
-}
-
 var filesToKill = [];
 singleFileInput = document.getElementById('singleFile');
 multiFileInput = document.getElementById('multiFile');
 var input1File = { name: "523771_file1", type: "", body: "file1 contents"};
 var input2Files =
   [{ name: "523771_file2", type: "", body: "second file contents" },
    { name: "523771_file3.txt", type: "text/plain", body: "123456" },
    { name: "523771_file4.html", type: "text/html", body: "<html>content</html>" }
--- a/content/html/content/test/test_iframe_sandbox_inheritance.html
+++ b/content/html/content/test/test_iframe_sandbox_inheritance.html
@@ -9,19 +9,16 @@ Implement HTML5 sandbox attribute for IF
   <title>Test for Bug 341604</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <script type="application/javascript">
 /** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/
 /** Inheritance Tests **/
 
-// Assertion failure in docshell/shistory/src/nsSHEntry.cpp (currently line 625).
-// Bug 901876 raised.
-SimpleTest.expectAssertions(1);
 SimpleTest.waitForExplicitFinish();
 
 // A postMessage handler that is used by sandboxed iframes without
 // 'allow-same-origin' to communicate pass/fail back to this main page.
 // It expects to be called with an object like {ok: true/false, desc:
 // <description of the test> which it then forwards to ok().
 window.addEventListener("message", receiveMessage, false);
 
--- a/content/html/content/test/test_iframe_sandbox_navigation.html
+++ b/content/html/content/test/test_iframe_sandbox_navigation.html
@@ -10,17 +10,16 @@ Implement HTML5 sandbox attribute for IF
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <script type="application/javascript">
 /** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/
 /** Navigation tests Part 1**/
 
-SimpleTest.expectAssertions(1, 3);
 SimpleTest.waitForExplicitFinish();
 // a postMessage handler that is used by sandboxed iframes without
 // 'allow-same-origin'/other windows to communicate pass/fail back to this main page.
 // it expects to be called with an object like {ok: true/false, desc:
 // <description of the test> which it then forwards to ok()
 window.addEventListener("message", receiveMessage, false);
 
 var testPassesReceived = 0;
--- a/content/html/document/test/test_bug448564.html
+++ b/content/html/document/test/test_bug448564.html
@@ -15,18 +15,16 @@ https://bugzilla.mozilla.org/show_bug.cg
   <iframe src="bug448564-iframe-2.html"></iframe>
   <iframe src="bug448564-iframe-3.html"></iframe>
 </p>
 <div id="content" style="display: none">
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-SimpleTest.expectAssertions(3);
-
 /** Test for Bug 448564 **/
 
 /**
  * The three iframes are going to be loaded with some dirty constructed forms.
  * Each of them will be submitted before the load event and a SJS will replace
  * the frame content with the query string.
  * Then, on the load event, our test file will check the content of each iframes
  * and check if the query string were correctly formatted (implying that all
--- a/content/html/document/test/test_bug478251.html
+++ b/content/html/document/test/test_bug478251.html
@@ -12,18 +12,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478251">Mozilla Bug 478251</a>
 <p id="display"><iframe id="t"></iframe></p>
 <div id="content" style="display: none">
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
-SimpleTest.expectAssertions(9, 10);
-
 /** Test for Bug 478251 **/
 var doc = $("t").contentDocument;
 doc.open();
 doc.write();
 doc.close();
 is(doc.documentElement.textContent, "", "Writing || failed");
 
 doc.open();
--- a/content/media/eme/CDMCallbackProxy.cpp
+++ b/content/media/eme/CDMCallbackProxy.cpp
@@ -267,28 +267,48 @@ CDMCallbackProxy::SessionError(const nsC
 }
 
 void
 CDMCallbackProxy::KeyIdUsable(const nsCString& aSessionId,
                               const nsTArray<uint8_t>& aKeyId)
 {
   MOZ_ASSERT(mProxy->IsOnGMPThread());
 
-  CDMCaps::AutoLock caps(mProxy->Capabilites());
-  caps.SetKeyUsable(aKeyId, NS_ConvertUTF8toUTF16(aSessionId));
+  bool keysChange = false;
+  {
+    CDMCaps::AutoLock caps(mProxy->Capabilites());
+    keysChange = caps.SetKeyUsable(aKeyId, NS_ConvertUTF8toUTF16(aSessionId));
+  }
+  if (keysChange) {
+    nsRefPtr<nsIRunnable> task;
+    task = NS_NewRunnableMethodWithArg<nsString>(mProxy,
+                                                 &CDMProxy::OnKeysChange,
+                                                 NS_ConvertUTF8toUTF16(aSessionId));
+    NS_DispatchToMainThread(task);
+  }
 }
 
 void
 CDMCallbackProxy::KeyIdNotUsable(const nsCString& aSessionId,
                                  const nsTArray<uint8_t>& aKeyId)
 {
   MOZ_ASSERT(mProxy->IsOnGMPThread());
 
-  CDMCaps::AutoLock caps(mProxy->Capabilites());
-  caps.SetKeyUnusable(aKeyId, NS_ConvertUTF8toUTF16(aSessionId));
+  bool keysChange = false;
+  {
+    CDMCaps::AutoLock caps(mProxy->Capabilites());
+    keysChange = caps.SetKeyUnusable(aKeyId, NS_ConvertUTF8toUTF16(aSessionId));
+  }
+  if (keysChange) {
+    nsRefPtr<nsIRunnable> task;
+    task = NS_NewRunnableMethodWithArg<nsString>(mProxy,
+                                                 &CDMProxy::OnKeysChange,
+                                                 NS_ConvertUTF8toUTF16(aSessionId));
+    NS_DispatchToMainThread(task);
+  }
 }
 
 void
 CDMCallbackProxy::SetCaps(uint64_t aCaps)
 {
   MOZ_ASSERT(mProxy->IsOnGMPThread());
 
   CDMCaps::AutoLock caps(mProxy->Capabilites());
--- a/content/media/eme/CDMCaps.cpp
+++ b/content/media/eme/CDMCaps.cpp
@@ -83,53 +83,63 @@ CDMCaps::AutoLock::IsKeyUsable(const Cen
   for (size_t i = 0; i < keys.Length(); i++) {
     if (keys[i].mId == aKeyId) {
       return true;
     }
   }
   return false;
 }
 
-void
+bool
 CDMCaps::AutoLock::SetKeyUsable(const CencKeyId& aKeyId,
                                 const nsString& aSessionId)
 {
   mData.mMonitor.AssertCurrentThreadOwns();
-  mData.mUsableKeyIds.AppendElement(UsableKey(aKeyId, aSessionId));
+  UsableKey key(aKeyId, aSessionId);
+  if (mData.mUsableKeyIds.Contains(key)) {
+    return false;
+  }
+  mData.mUsableKeyIds.AppendElement(key);
   auto& waiters = mData.mWaitForKeys;
   size_t i = 0;
   while (i < waiters.Length()) {
     auto& w = waiters[i];
     if (w.mKeyId == aKeyId) {
       if (waiters[i].mTarget) {
         EME_LOG("SetKeyUsable() notified waiter.");
         w.mTarget->Dispatch(w.mContinuation, NS_DISPATCH_NORMAL);
       } else {
         w.mContinuation->Run();
       }
       waiters.RemoveElementAt(i);
     } else {
       i++;
     }
   }
+  return true;
 }
 
-void
+bool
 CDMCaps::AutoLock::SetKeyUnusable(const CencKeyId& aKeyId,
                                   const nsString& aSessionId)
 {
   mData.mMonitor.AssertCurrentThreadOwns();
+  UsableKey key(aKeyId, aSessionId);
+  if (!mData.mUsableKeyIds.Contains(key)) {
+    return false;
+  }
   auto& keys = mData.mUsableKeyIds;
   for (size_t i = 0; i < keys.Length(); i++) {
     if (keys[i].mId == aKeyId &&
         keys[i].mSessionId == aSessionId) {
       keys.RemoveElementAt(i);
       break;
     }
   }
+  return true;
 }
 
 void
 CDMCaps::AutoLock::CallWhenKeyUsable(const CencKeyId& aKey,
                                      nsIRunnable* aContinuation,
                                      nsIThread* aTarget)
 {
   mData.mMonitor.AssertCurrentThreadOwns();
--- a/content/media/eme/CDMCaps.h
+++ b/content/media/eme/CDMCaps.h
@@ -33,18 +33,23 @@ public:
     ~AutoLock();
 
     // Returns true if the capabilities of the CDM are known, i.e. they have
     // been reported by the CDM to Gecko.
     bool AreCapsKnown();
 
     bool IsKeyUsable(const CencKeyId& aKeyId);
 
-    void SetKeyUsable(const CencKeyId& aKeyId, const nsString& aSessionId);
-    void SetKeyUnusable(const CencKeyId& aKeyId, const nsString& aSessionId);
+    // Returns true if setting this key usable results in the usable keys
+    // changing for this session, i.e. the key was not previously marked usable.
+    bool SetKeyUsable(const CencKeyId& aKeyId, const nsString& aSessionId);
+
+    // Returns true if setting this key unusable results in the usable keys
+    // changing for this session, i.e. the key was previously marked usable.
+    bool SetKeyUnusable(const CencKeyId& aKeyId, const nsString& aSessionId);
 
     void DropKeysForSession(const nsAString& aSessionId);
     void GetUsableKeysForSession(const nsAString& aSessionId,
                                  nsTArray<CencKeyId>& aOutKeyIds);
 
     // Sets the capabilities of the CDM. aCaps is the logical OR of the
     // GMP_EME_CAP_* flags from gmp-decryption.h.
     void SetCaps(uint64_t aCaps);
@@ -94,16 +99,21 @@ private:
               const nsString& aSessionId)
       : mId(aId)
       , mSessionId(aSessionId)
     {}
     UsableKey(const UsableKey& aOther)
       : mId(aOther.mId)
       , mSessionId(aOther.mSessionId)
     {}
+    bool operator==(const UsableKey& aOther) const {
+      return mId == aOther.mId &&
+             mSessionId == aOther.mSessionId;
+    };
+
     CencKeyId mId;
     nsString mSessionId;
   };
   nsTArray<UsableKey> mUsableKeyIds;
 
   nsTArray<WaitForKeys> mWaitForKeys;
 
   nsTArray<nsRefPtr<nsIRunnable>> mWaitForCaps;
--- a/content/media/eme/CDMProxy.cpp
+++ b/content/media/eme/CDMProxy.cpp
@@ -421,16 +421,29 @@ CDMProxy::OnSessionMessage(const nsAStri
   }
   nsRefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
   if (session) {
     session->DispatchKeyMessage(aMessage, aDestinationURL);
   }
 }
 
 void
+CDMProxy::OnKeysChange(const nsAString& aSessionId)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (mKeys.IsNull()) {
+    return;
+  }
+  nsRefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+  if (session) {
+    session->DispatchKeysChange();
+  }
+}
+
+void
 CDMProxy::OnExpirationChange(const nsAString& aSessionId,
                              GMPTimestamp aExpiryTime)
 {
   MOZ_ASSERT(NS_IsMainThread());
   NS_WARNING("CDMProxy::OnExpirationChange() not implemented");
 }
 
 void
--- a/content/media/eme/CDMProxy.h
+++ b/content/media/eme/CDMProxy.h
@@ -157,16 +157,19 @@ public:
                      GMPErr aResult,
                      const nsTArray<uint8_t>& aDecryptedData);
 
   // GMP thread only.
   void gmp_Terminated();
 
   CDMCaps& Capabilites();
 
+  // Main thread only.
+  void OnKeysChange(const nsAString& aSessionId);
+
 #ifdef DEBUG
   bool IsOnGMPThread();
 #endif
 
 private:
 
   struct InitData {
     uint32_t mPromiseId;
--- a/content/media/eme/MediaKeySession.cpp
+++ b/content/media/eme/MediaKeySession.cpp
@@ -8,16 +8,17 @@
 #include "mozilla/dom/MediaKeySession.h"
 #include "mozilla/dom/MediaKeyError.h"
 #include "mozilla/dom/MediaKeyMessageEvent.h"
 #include "mozilla/dom/MediaEncryptedEvent.h"
 #include "nsCycleCollectionParticipant.h"
 #include "mozilla/CDMProxy.h"
 #include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/Move.h"
+#include "nsContentUtils.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession,
                                    DOMEventTargetHelper,
                                    mMediaKeyError,
                                    mKeys,
@@ -279,10 +280,29 @@ void
 MediaKeySession::DispatchKeyError(uint32_t aSystemCode)
 {
   RefPtr<MediaKeyError> event(new MediaKeyError(this, aSystemCode));
   nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
     new AsyncEventDispatcher(this, event);
   asyncDispatcher->PostDOMEvent();
 }
 
+void
+MediaKeySession::DispatchKeysChange()
+{
+  if (IsClosed()) {
+    return;
+  }
+  DebugOnly<nsresult> rv =
+    nsContentUtils::DispatchTrustedEvent(mKeys->GetOwnerDoc(),
+                                         this,
+                                         NS_LITERAL_STRING("keyschange"),
+                                         false,
+                                         false);
+#ifdef DEBUG
+  if (NS_FAILED(rv)) {
+    NS_WARNING("Failed to dispatch keyschange event");
+  }
+#endif
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/content/media/eme/MediaKeySession.h
+++ b/content/media/eme/MediaKeySession.h
@@ -79,16 +79,18 @@ public:
 
   already_AddRefed<Promise> GetUsableKeyIds(ErrorResult& aRv);
 
   void DispatchKeyMessage(const nsTArray<uint8_t>& aMessage,
                           const nsAString& aURL);
 
   void DispatchKeyError(uint32_t system_code);
 
+  void DispatchKeysChange();
+
   void OnClosed();
 
   bool IsClosed() const;
 
 private:
   ~MediaKeySession();
 
   nsRefPtr<Promise> mClosed;
--- a/content/media/eme/MediaKeys.cpp
+++ b/content/media/eme/MediaKeys.cpp
@@ -586,10 +586,16 @@ CopyArrayBufferViewOrArrayBufferData(con
     bufferview.ComputeLengthAndData();
     aOutData.AppendElements(bufferview.Data(), bufferview.Length());
   } else {
     return false;
   }
   return true;
 }
 
+nsIDocument*
+MediaKeys::GetOwnerDoc() const
+{
+  return mElement ? mElement->OwnerDoc() : nullptr;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/content/media/eme/MediaKeys.h
+++ b/content/media/eme/MediaKeys.h
@@ -117,16 +117,20 @@ public:
 
   // Returns true if this MediaKeys has been bound to a media element.
   bool IsBoundToMediaElement() const;
 
   // Return NS_OK if the principals are the same as when the MediaKeys
   // was created, failure otherwise.
   nsresult CheckPrincipals();
 
+  // Returns a pointer to the bound media element's owner doc.
+  // If we're not bound, this returns null.
+  nsIDocument* GetOwnerDoc() const;
+
 private:
 
   static bool IsTypeSupported(const nsAString& aKeySystem,
                               const Optional<nsAString>& aInitDataType = Optional<nsAString>(),
                               const Optional<nsAString>& aContentType = Optional<nsAString>());
 
   bool IsInPrivateBrowsing();
   already_AddRefed<Promise> Init(ErrorResult& aRv);
--- a/content/media/mediasource/MediaSource.cpp
+++ b/content/media/mediasource/MediaSource.cpp
@@ -17,16 +17,17 @@
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/TimeRanges.h"
 #include "mozilla/mozalloc.h"
 #include "nsContentTypeParser.h"
 #include "nsDebug.h"
 #include "nsError.h"
 #include "nsIRunnable.h"
+#include "nsIScriptObjectPrincipal.h"
 #include "nsPIDOMWindow.h"
 #include "nsString.h"
 #include "nsThreadUtils.h"
 #include "prlog.h"
 
 struct JSContext;
 class JSObject;
 
@@ -389,22 +390,29 @@ MediaSource::GetBuffered(TimeRanges* aBu
 
   MSE_DEBUG("MediaSource(%p)::GetBuffered ranges=%s", this, DumpTimeRanges(intersectionRanges).get());
 }
 
 MediaSource::MediaSource(nsPIDOMWindow* aWindow)
   : DOMEventTargetHelper(aWindow)
   , mDuration(UnspecifiedNaN<double>())
   , mDecoder(nullptr)
+  , mPrincipal(nullptr)
   , mReadyState(MediaSourceReadyState::Closed)
   , mFirstSourceBufferInitialized(false)
 {
   MOZ_ASSERT(NS_IsMainThread());
   mSourceBuffers = new SourceBufferList(this);
   mActiveSourceBuffers = new SourceBufferList(this);
+
+  nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
+  if (sop) {
+    mPrincipal = sop->GetPrincipal();
+  }
+
   MSE_API("MediaSource(%p)::MediaSource(aWindow=%p) mSourceBuffers=%p mActiveSourceBuffers=%p",
           this, aWindow, mSourceBuffers.get(), mActiveSourceBuffers.get());
 }
 
 void
 MediaSource::SetReadyState(MediaSourceReadyState aState)
 {
   MOZ_ASSERT(NS_IsMainThread());
--- a/content/media/mediasource/MediaSource.h
+++ b/content/media/mediasource/MediaSource.h
@@ -80,16 +80,21 @@ public:
   void SetReadyState(MediaSourceReadyState aState);
 
  // Used by SourceBuffer to call CreateSubDecoder.
   MediaSourceDecoder* GetDecoder()
   {
     return mDecoder;
   }
 
+  nsIPrincipal* GetPrincipal()
+  {
+    return mPrincipal;
+  }
+
   // Called by SourceBuffers to notify this MediaSource that data has
   // been evicted from the buffered data. The start and end times
   // that were evicted are provided.
   void NotifyEvicted(double aStart, double aEnd);
 
   // Queue InitializationEvent to run on the main thread.  Called when a
   // SourceBuffer has an initialization segment appended, but only
   // dispatched the first time (using mFirstSourceBufferInitialized).
@@ -123,16 +128,18 @@ private:
 
   double mDuration;
 
   nsRefPtr<SourceBufferList> mSourceBuffers;
   nsRefPtr<SourceBufferList> mActiveSourceBuffers;
 
   nsRefPtr<MediaSourceDecoder> mDecoder;
 
+  nsRefPtr<nsIPrincipal> mPrincipal;
+
   MediaSourceReadyState mReadyState;
 
   bool mFirstSourceBufferInitialized;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(MediaSource, MOZILLA_DOM_MEDIASOURCE_IMPLEMENTATION_IID)
 
 } // namespace dom
--- a/content/media/mediasource/MediaSourceDecoder.cpp
+++ b/content/media/mediasource/MediaSourceDecoder.cpp
@@ -102,19 +102,19 @@ MediaSourceDecoder::Shutdown()
   }
   // Kick WaitForData out of its slumber.
   ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
   mon.NotifyAll();
 }
 
 /*static*/
 already_AddRefed<MediaResource>
-MediaSourceDecoder::CreateResource()
+MediaSourceDecoder::CreateResource(nsIPrincipal* aPrincipal)
 {
-  return nsRefPtr<MediaResource>(new MediaSourceResource()).forget();
+  return nsRefPtr<MediaResource>(new MediaSourceResource(aPrincipal)).forget();
 }
 
 void
 MediaSourceDecoder::AttachMediaSource(dom::MediaSource* aMediaSource)
 {
   MOZ_ASSERT(!mMediaSource && !mDecoderStateMachine && NS_IsMainThread());
   mMediaSource = aMediaSource;
 }
--- a/content/media/mediasource/MediaSourceDecoder.h
+++ b/content/media/mediasource/MediaSourceDecoder.h
@@ -36,17 +36,17 @@ public:
 
   virtual MediaDecoder* Clone() MOZ_OVERRIDE;
   virtual MediaDecoderStateMachine* CreateStateMachine() MOZ_OVERRIDE;
   virtual nsresult Load(nsIStreamListener**, MediaDecoder*) MOZ_OVERRIDE;
   virtual nsresult GetSeekable(dom::TimeRanges* aSeekable) MOZ_OVERRIDE;
 
   virtual void Shutdown() MOZ_OVERRIDE;
 
-  static already_AddRefed<MediaResource> CreateResource();
+  static already_AddRefed<MediaResource> CreateResource(nsIPrincipal* aPrincipal = nullptr);
 
   void AttachMediaSource(dom::MediaSource* aMediaSource);
   void DetachMediaSource();
 
   already_AddRefed<SourceBufferDecoder> CreateSubDecoder(const nsACString& aType);
   void AddTrackBuffer(TrackBuffer* aTrackBuffer);
   void RemoveTrackBuffer(TrackBuffer* aTrackBuffer);
   void OnTrackBufferConfigured(TrackBuffer* aTrackBuffer, const MediaInfo& aInfo);
--- a/content/media/mediasource/MediaSourceResource.h
+++ b/content/media/mediasource/MediaSourceResource.h
@@ -21,22 +21,22 @@ extern PRLogModuleInfo* GetMediaSourceAP
 
 #define UNIMPLEMENTED() MSE_DEBUG("MediaSourceResource(%p): UNIMPLEMENTED FUNCTION at %s:%d", this, __FILE__, __LINE__)
 
 namespace mozilla {
 
 class MediaSourceResource MOZ_FINAL : public MediaResource
 {
 public:
-  MediaSourceResource() {}
+  MediaSourceResource(nsIPrincipal* aPrincipal = nullptr)
+    : mPrincipal(aPrincipal) {}
 
   virtual nsresult Close() MOZ_OVERRIDE { return NS_OK; }
   virtual void Suspend(bool aCloseImmediately) MOZ_OVERRIDE { UNIMPLEMENTED(); }
   virtual void Resume() MOZ_OVERRIDE { UNIMPLEMENTED(); }
-  virtual already_AddRefed<nsIPrincipal> GetCurrentPrincipal() MOZ_OVERRIDE { UNIMPLEMENTED(); return nullptr; }
   virtual bool CanClone() MOZ_OVERRIDE { UNIMPLEMENTED(); return false; }
   virtual already_AddRefed<MediaResource> CloneData(MediaDecoder* aDecoder) MOZ_OVERRIDE { UNIMPLEMENTED(); return nullptr; }
   virtual void SetReadMode(MediaCacheStream::ReadMode aMode) MOZ_OVERRIDE { UNIMPLEMENTED(); }
   virtual void SetPlaybackRate(uint32_t aBytesPerSecond) MOZ_OVERRIDE  { UNIMPLEMENTED(); }
   virtual nsresult Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes) MOZ_OVERRIDE { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
   virtual nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes) MOZ_OVERRIDE { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
   virtual nsresult Seek(int32_t aWhence, int64_t aOffset) MOZ_OVERRIDE { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
   virtual int64_t Tell() MOZ_OVERRIDE { UNIMPLEMENTED(); return -1; }
@@ -47,16 +47,21 @@ public:
   virtual int64_t GetNextCachedData(int64_t aOffset) MOZ_OVERRIDE { UNIMPLEMENTED(); return -1; }
   virtual int64_t GetCachedDataEnd(int64_t aOffset) MOZ_OVERRIDE { UNIMPLEMENTED(); return -1; }
   virtual bool IsDataCachedToEndOfResource(int64_t aOffset) MOZ_OVERRIDE { UNIMPLEMENTED(); return false; }
   virtual bool IsSuspendedByCache() MOZ_OVERRIDE { UNIMPLEMENTED(); return false; }
   virtual bool IsSuspended() MOZ_OVERRIDE { UNIMPLEMENTED(); return false; }
   virtual nsresult ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount) MOZ_OVERRIDE { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
   virtual nsresult Open(nsIStreamListener** aStreamListener) MOZ_OVERRIDE { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
 
+  virtual already_AddRefed<nsIPrincipal> GetCurrentPrincipal() MOZ_OVERRIDE
+  {
+    return nsRefPtr<nsIPrincipal>(mPrincipal).forget();
+  }
+
   virtual nsresult GetCachedRanges(nsTArray<MediaByteRange>& aRanges) MOZ_OVERRIDE
   {
     UNIMPLEMENTED();
     aRanges.AppendElement(MediaByteRange(0, GetLength()));
     return NS_OK;
   }
 
   virtual bool IsTransportSeekable() MOZ_OVERRIDE { return true; }
@@ -71,16 +76,17 @@ private:
     return size;
   }
 
   virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const MOZ_OVERRIDE
   {
     return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
   }
 
+  nsRefPtr<nsIPrincipal> mPrincipal;
   const nsCString mType;
 };
 
 } // namespace mozilla
 
 #undef UNIMPLEMENTED
 
 #endif /* MOZILLA_MEDIASOURCERESOURCE_H_ */
new file mode 100644
--- /dev/null
+++ b/content/media/test/crashtests/1080986.html
@@ -0,0 +1,3 @@
+<html>
+<audio autoplay src="1080986.wav"></audio>
+</html>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b96c59b7ec2ecdc5033904eb9ceb779adb05d154
GIT binary patch
literal 592
zc$^hcT}YE*6n@_CCl^^QFDjWt+TTUA#c*?Y5lw40`qwd67vdo7rmmVIQK3Yj-2@s~
zS6vi{8Fi6el#pnV&6El;gCHzpHODj!o!_?adwM;G1Lr(E&-vlh1_ObB0@VA%N1HCk
zb`}8;(*LNyrvgBMT_Pufbs<SY@KoS*ur3fPT*IO*O`dY6`=GNN7oxGKq~crVz{*o|
zvo>P2WoMOp`h#pNzsy7VN~KG4s)wz(c}ek#Au)j^vzdMRO)Zo=k79Ff^^Nt#c%Mz-
zy0V>RqC!m@y~bT%nVLrXI)_aae*L*x%iC-bhG-QXe933LrCrprc^7^PCla<!J-@Ok
z7uS1v#p=en+y}K)+{uoLX_T5jxJ?_^1Gxsq)h%XHnIO1Fn^GO9qEBgOmpN!u8}nSn
z8RLGYSILMQ`8KNf%qOBpq*yGP(PP<#S16X7%VG!{;KYD6LA$7Cx4FmxG%~{oPGSs8
zEMpi6eCJt&M3TSQh$Fa)esr>rbErXtLo7s+Uc6=tkHL%Me8e2TQo(w3@GqlC(S{<7
z@|r~JF-Z@0;4$Z!<S^gSl<XO<*(<5p2p4Lx8;dNFznh^071)afPV*nDunTEg+=@Y|
z+lLYq;3NCFOoOvb@U86kkRvi5<_k_qpJZBAI`Uj$DJo@O7u?tnkJS1RMgt=9h8nrG
QPev(BF34@h`CHEY54@=vF#rGn
--- a/content/media/test/crashtests/crashtests.list
+++ b/content/media/test/crashtests/crashtests.list
@@ -69,12 +69,13 @@ load 986901.html
 load 990794.html
 load 1015662.html
 skip-if(Android||B2G) test-pref(media.navigator.permission.disabled,true) load 1028458.html # bug 1048863
 load buffer-source-ended-1.html
 HTTP load media-element-source-seek-1.html
 load offline-buffer-source-ended-1.html
 load oscillator-ended-1.html
 load oscillator-ended-2.html
+load 1080986.html
 include ../../mediasource/test/crashtests/crashtests.list
 
 # This needs to run at the end to avoid leaking busted state into other tests.
 skip-if(winWidget) load 691096-1.html
--- a/content/media/test/manifest.js
+++ b/content/media/test/manifest.js
@@ -644,29 +644,28 @@ var gEMETests = [
     keys: {
       // "keyid" : "key"
       "7e571d017e571d017e571d017e571d01" : "7e5711117e5711117e5711117e571111",
       "7e571d027e571d027e571d027e571d02" : "7e5722227e5722227e5722227e572222",
     },
     sessionType:"temporary",
     duration:0.47
   },
-  // XXX Bug 1082239
-  //{
-  //  name:"gizmo-frag-cencinit.mp4",
-  //  fragments: [ "gizmo-frag-cencinit.mp4", "gizmo-frag-cenc1.m4s", "gizmo-frag-cenc2.m4s" ],
-  //  type:"video/mp4; codecs=\"avc1.64000d,mp4a.40.2\"",
-  //  keys: {
-  //    // "keyid" : "key"
-  //    "7e571d037e571d037e571d037e571d03" : "7e5733337e5733337e5733337e573333",
-  //    "7e571d047e571d047e571d047e571d04" : "7e5744447e5744447e5744447e574444",
-  //  },
-  //  sessionType:"temporary",
-  //  duration:2.00,
-  //},
+  {
+    name:"gizmo-frag-cencinit.mp4",
+    fragments: [ "gizmo-frag-cencinit.mp4", "gizmo-frag-cenc1.m4s", "gizmo-frag-cenc2.m4s" ],
+    type:"video/mp4; codecs=\"avc1.64000d,mp4a.40.2\"",
+    keys: {
+      // "keyid" : "key"
+      "7e571d037e571d037e571d037e571d03" : "7e5733337e5733337e5733337e573333",
+      "7e571d047e571d047e571d047e571d04" : "7e5744447e5744447e5744447e574444",
+    },
+    sessionType:"temporary",
+    duration:2.00,
+  },
 ];
 
 function checkMetadata(msg, e, test) {
   if (test.width) {
     is(e.videoWidth, test.width, msg + " video width");
   }
   if (test.height) {
     is(e.videoHeight, test.height, msg + " video height");
--- a/content/media/test/mochitest.ini
+++ b/content/media/test/mochitest.ini
@@ -472,16 +472,17 @@ skip-if = buildapp == 'b2g' # bug 102168
 [test_texttrackcue.html]
 [test_texttracklist.html]
 [test_texttrackregion.html]
 [test_timeupdate_small_files.html]
 [test_trackelementevent.html]
 [test_trackevent.html]
 [test_unseekable.html]
 [test_video_to_canvas.html]
+[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]
 
 # The tests below contain backend-specific tests. Write backend independent
--- a/content/media/test/test_encryptedMediaExtensions.html
+++ b/content/media/test/test_encryptedMediaExtensions.html
@@ -148,16 +148,35 @@ function PlayTest(test, elem)
     return;
   }
 
   // This file isn't fragmented; set the media source normally.
   elem.src = test.name;
   elem.play();
 }
 
+function KeysChangeFunc(session, keys) {
+  session.keyIdsReceived = [];
+  for (var keyid in keys) {
+    info("Set " + keyid + " to false in session.keyIdsReceived");
+    session.keyIdsReceived[keyid] = false;
+  }
+  return function(ev) {
+    var session = ev.target;
+    session.gotKeysChanged = true;
+    session.getUsableKeyIds().then(function(keyIds) {
+      for (var k = 0; k < keyIds.length; k++) {
+        var kid = Base64ToHex(window.btoa(ArrayBufferToString(keyIds[k])));
+        ok(kid in session.keyIdsReceived, "session.keyIdsReceived contained " + kid + " as expected.");
+        session.keyIdsReceived[kid] = true;
+      }
+    }, bail("Failed to get keyIds"));
+  }
+}
+
 function startTest(test, token)
 {
   manager.started(test._token);
 
   var v = document.createElement("video");
   var gotEncrypted = false;
   var gotPlaying = false;
 
@@ -165,24 +184,27 @@ function startTest(test, token)
     gotEncrypted = true;
 
     info(token + " got encrypted event");
     ok(MediaKeys.isTypeSupported(KEYSYSTEM_TYPE, ev.initDataType, test.type),
        token + " MediaKeys should support this keysystem");
 
     MediaKeys.create(KEYSYSTEM_TYPE).then(function(mediaKeys) {
       info(token + " created MediaKeys object ok");
+      mediaKeys.sessions = [];
       return v.setMediaKeys(mediaKeys);
     }, bail("failed to create MediaKeys object")).then(function() {
       info(token + " set MediaKeys on <video> element ok");
 
       ok(MediaKeys.isTypeSupported(KEYSYSTEM_TYPE, ev.initDataType, test.type),
          "MediaKeys should still support keysystem after CDM created...");
 
       var session = v.mediaKeys.createSession(test.sessionType);
+      v.mediaKeys.sessions.push(session);
+      session.addEventListener("keyschange", KeysChangeFunc(session, test.keys), false);
       session.addEventListener("message", UpdateSessionFunc(test));
       session.generateRequest(ev.initDataType, ev.initData).then(function() {
       }, bail(token + " Failed to initialise MediaKeySession"));
 
     }, bail(token + " Failed to set MediaKeys on <video> element"));
   });
 
   v.addEventListener("playing", function () { gotPlaying = true; });
@@ -193,16 +215,27 @@ function startTest(test, token)
 
     ok(gotEncrypted, token + " encrypted event should have fired");
     ok(gotPlaying, token + " playing event should have fired");
 
     ok(Math.abs(test.duration - v.duration) < 0.1,
        token + " Duration of video should be corrrect");
     ok(Math.abs(test.duration - v.currentTime) < 0.1,
        token + " Current time should be same as duration");
+    // Verify all sessions had all keys went sent the to the CDM usable, and thus
+    // that we received keyschange event(s).
+    var sessions = v.mediaKeys.sessions;
+    is(sessions.length, 1, "should have 1 session");
+    for (var i = 0; i < sessions.length; i++) {
+      var session = sessions[i];
+      ok(session.gotKeysChanged, "Should have received at least one keychange event");
+      for (var kid in session.keyIdsReceived) {
+        ok(session.keyIdsReceived[kid], "key with id " + kid + " was usable as expected");
+      }
+    }
   });
 
   v.addEventListener("error", bail(token + " got error event"));
 
   PlayTest(test, v);
 }
 
 function testIsTypeSupported()
--- a/content/media/test/test_imagecapture.html
+++ b/content/media/test/test_imagecapture.html
@@ -33,16 +33,17 @@ function gcTest(track) {
       };
       imageCapture.onerror = function(error) {
         ok(false, "takePhoto failure in gc testing");
         reject();
       };
 
       imageCapture.takePhoto();
     }
+    info("Call gc ");
     SpecialPowers.gc();
   });
 }
 
 // Continue calling takePhoto() in rapid succession.
 function rapidTest(track) {
   return new Promise(function(resolve, reject) {
     var imageCapture = new ImageCapture(track);
@@ -106,18 +107,30 @@ function trackTest(track) {
 
     track.enabled = false;
     imageCapture.takePhoto()
   });
 }
 
 function init() {
   return new Promise(function(resolve, reject) {
+    var constraints;
+    if (SpecialPowers.Services.appinfo.widgetToolkit == "gonk") {
+      info("B2G ImageCapture test");
+      // Reduce repeat count due to b2g emulator is very slow.
+      repeat = 20;
+      // Use gonk camera, MedieEngine will be the backend of ImageCapture.
+      constraints = {video: true};
+    } else {
+      // use fake camera, MediaStreamGraph will be the backend of ImageCapture.
+      constraints = {video: true, fake: true}
+    }
+
     window.navigator.mozGetUserMedia(
-      {video: true, fake: true},
+      constraints,
       function(stream) {
         var track = stream.getVideoTracks()[0];
         resolve(track);
       },
       function(err) {
         reject(err);
       }
     );
@@ -132,20 +145,21 @@ function start() {
     info("ImageCapture blob test.");
     return blobTest(track);
   }).then(function(track) {
     info("ImageCapture rapid takePhoto() test.");
     return rapidTest(track);
   }).then(function(track) {
     info("ImageCapture multiple instances test.");
     return gcTest(track);
-  }).then(function() {
-    SimpleTest.finish();
-  });
+  }).then(SimpleTest.finish);
 }
 
+SimpleTest.requestCompleteLog();
 SimpleTest.waitForExplicitFinish();
-SpecialPowers.pushPrefEnv({"set": [["dom.imagecapture.enabled", true]]}, start);
 
+SpecialPowers.pushPrefEnv({"set": [["dom.imagecapture.enabled", true],
+                                  ["media.navigator.permission.disabled", true]
+                                  ]}, start);
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/content/media/test/test_video_in_audio_element.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1060896
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1060896</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="text/javascript" src="manifest.js"></script>
+  <script type="application/javascript">
+
+  /**
+   * Test for Bug 1060896; tests that loading a video inside an audio element works.
+   **/
+
+  var manager = new MediaTestManager;
+   
+  function ended(event) {
+    var a = event.target;
+    removeNodeAndSource(a);
+    manager.finished(a.token);
+  }
+   
+  function initTest(test, token) {
+    var a = document.createElement('audio');
+    a.token = token;
+    manager.started(token);
+    a.autoplay = true;
+    
+    a.addEventListener("ended", ended, false);
+
+    a.src = test.name;
+  }
+
+  var videos = gSmallTests.filter(function(x){return /^video/.test(x.type);});
+  
+  manager.runTests(videos, initTest);
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1060896">Mozilla Bug 1060896</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/content/media/wave/WaveReader.cpp
+++ b/content/media/wave/WaveReader.cpp
@@ -546,27 +546,27 @@ WaveReader::LoadListChunk(uint32_t aChun
   // List chunks are always word (two byte) aligned.
   NS_ABORT_IF_FALSE(mDecoder->GetResource()->Tell() % 2 == 0,
                     "LoadListChunk called with unaligned resource");
 
   static const unsigned int MAX_CHUNK_SIZE = 1 << 16;
   static_assert(uint64_t(MAX_CHUNK_SIZE) < UINT_MAX / sizeof(char),
                 "MAX_CHUNK_SIZE too large for enumerator.");
 
-  if (aChunkSize > MAX_CHUNK_SIZE) {
+  if (aChunkSize > MAX_CHUNK_SIZE || aChunkSize < 4) {
     return false;
   }
 
   nsAutoArrayPtr<char> chunk(new char[aChunkSize]);
   if (!ReadAll(chunk.get(), aChunkSize)) {
     return false;
   }
 
   static const uint32_t INFO_LIST_MAGIC = 0x494e464f;
-  const char *p = chunk.get();
+  const char* p = chunk.get();
   if (ReadUint32BE(&p) != INFO_LIST_MAGIC) {
     return false;
   }
 
   const waveIdToName ID_TO_NAME[] = {
     { 0x49415254, NS_LITERAL_CSTRING("artist") },   // IART
     { 0x49434d54, NS_LITERAL_CSTRING("comments") }, // ICMT
     { 0x49474e52, NS_LITERAL_CSTRING("genre") },    // IGNR
--- a/content/xul/content/src/nsXULElement.cpp
+++ b/content/xul/content/src/nsXULElement.cpp
@@ -68,17 +68,17 @@
 #include "nsXULControllers.h"
 #include "nsIBoxObject.h"
 #include "nsPIBoxObject.h"
 #include "XULDocument.h"
 #include "nsXULPopupListener.h"
 #include "nsRuleWalker.h"
 #include "nsIDOMCSSStyleDeclaration.h"
 #include "nsCSSParser.h"
-#include "nsIListBoxObject.h"
+#include "ListBoxObject.h"
 #include "nsContentUtils.h"
 #include "nsContentList.h"
 #include "mozilla/InternalMutationEvent.h"
 #include "mozilla/MouseEvents.h"
 #include "nsIDOMMutationEvent.h"
 #include "nsPIDOMWindow.h"
 #include "nsJSPrincipals.h"
 #include "nsDOMAttributeMap.h"
@@ -104,16 +104,17 @@
 #include "nsXBLBinding.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozAutoDocUpdate.h"
 #include "nsIDOMXULCommandEvent.h"
 #include "nsCCUncollectableMarker.h"
 #include "nsICSSDeclaration.h"
 
 #include "mozilla/dom/XULElementBinding.h"
+#include "mozilla/dom/BoxObject.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 #ifdef XUL_PROTOTYPE_ATTRIBUTE_METERING
 uint32_t             nsXULPrototypeAttribute::gNumElements;
 uint32_t             nsXULPrototypeAttribute::gNumAttributes;
 uint32_t             nsXULPrototypeAttribute::gNumCacheTests;
@@ -1500,17 +1501,17 @@ nsXULElement::GetControllers(ErrorResult
 NS_IMETHODIMP
 nsXULElement::GetBoxObject(nsIBoxObject** aResult)
 {
     ErrorResult rv;
     *aResult = GetBoxObject(rv).take();
     return rv.ErrorCode();
 }
 
-already_AddRefed<nsIBoxObject>
+already_AddRefed<BoxObject>
 nsXULElement::GetBoxObject(ErrorResult& rv)
 {
     // XXX sXBL/XBL2 issue! Owner or current document?
     return OwnerDoc()->GetBoxObjectFor(this, rv);
 }
 
 // Methods for setting/getting attributes from nsIDOMXULElement
 #define NS_IMPL_XUL_STRING_ATTR(_method, _atom)                     \
--- a/content/xul/content/src/nsXULElement.h
+++ b/content/xul/content/src/nsXULElement.h
@@ -21,17 +21,16 @@
 #include "nsIControllers.h"
 #include "nsIDOMElement.h"
 #include "nsIDOMXULElement.h"
 #include "nsIDOMXULMultSelectCntrlEl.h"
 #include "nsIRDFCompositeDataSource.h"
 #include "nsIRDFResource.h"
 #include "nsIURI.h"
 #include "nsIXULTemplateBuilder.h"
-#include "nsIBoxObject.h"
 #include "nsLayoutCID.h"
 #include "nsAttrAndChildArray.h"
 #include "nsGkAtoms.h"
 #include "nsAutoPtr.h"
 #include "nsStyledElement.h"
 #include "nsIFrameLoader.h"
 #include "nsFrameLoader.h"
 #include "mozilla/dom/DOMRect.h"
@@ -49,16 +48,19 @@ class nsXULPrototypeNode;
 typedef nsTArray<nsRefPtr<nsXULPrototypeNode> > nsPrototypeArray;
 
 namespace mozilla {
 class EventChainPreVisitor;
 class EventListenerManager;
 namespace css {
 class StyleRule;
 }
+namespace dom {
+class BoxObject;
+}
 }
 
 namespace JS {
 class SourceBufferHolder;
 }
 
 ////////////////////////////////////////////////////////////////////////
 
@@ -582,17 +584,17 @@ public:
     bool AllowEvents() const
     {
         return BoolAttrIsTrue(nsGkAtoms::allowevents);
     }
     already_AddRefed<nsIRDFCompositeDataSource> GetDatabase();
     already_AddRefed<nsIXULTemplateBuilder> GetBuilder();
     already_AddRefed<nsIRDFResource> GetResource(mozilla::ErrorResult& rv);
     nsIControllers* GetControllers(mozilla::ErrorResult& rv);
-    already_AddRefed<nsIBoxObject> GetBoxObject(mozilla::ErrorResult& rv);
+    already_AddRefed<mozilla::dom::BoxObject> GetBoxObject(mozilla::ErrorResult& rv);
     void Focus(mozilla::ErrorResult& rv);
     void Blur(mozilla::ErrorResult& rv);
     void Click(mozilla::ErrorResult& rv);
     // The XPCOM DoCommand never fails, so it's OK for us.
     already_AddRefed<nsINodeList>
       GetElementsByAttribute(const nsAString& aAttribute,
                              const nsAString& aValue);
     already_AddRefed<nsINodeList>
--- a/content/xul/document/src/XULDocument.cpp
+++ b/content/xul/document/src/XULDocument.cpp
@@ -39,16 +39,17 @@
 #include "nsXMLContentSink.h"
 #include "nsXULContentSink.h"
 #include "nsXULContentUtils.h"
 #include "nsIXULOverlayProvider.h"
 #include "nsIStringEnumerator.h"
 #include "nsNetUtil.h"
 #include "nsParserCIID.h"
 #include "nsPIBoxObject.h"
+#include "mozilla/dom/BoxObject.h"
 #include "nsXPIDLString.h"
 #include "nsPIDOMWindow.h"
 #include "nsPIWindowRoot.h"
 #include "nsXULCommandDispatcher.h"
 #include "nsXULElement.h"
 #include "prlog.h"
 #include "rdf.h"
 #include "nsIFrame.h"
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -4190,17 +4190,17 @@ nsDocShell::GetChildSHEntry(int32_t aChi
 
 NS_IMETHODIMP
 nsDocShell::AddChildSHEntry(nsISHEntry * aCloneRef, nsISHEntry * aNewEntry,
                             int32_t aChildOffset, uint32_t loadType,
                             bool aCloneChildren)
 {
     nsresult rv;
 
-    if (mLSHE && loadType != LOAD_PUSHSTATE) {
+    if (mLSHE && loadType != LOAD_PUSHSTATE && !aCloneRef) {
         /* You get here if you are currently building a 
          * hierarchy ie.,you just visited a frameset page
          */
         nsCOMPtr<nsISHContainer> container(do_QueryInterface(mLSHE, &rv));
         if (container) {
             rv = container->AddChild(aNewEntry, aChildOffset);
         }
     }
--- a/docshell/test/test_bug580069.html
+++ b/docshell/test/test_bug580069.html
@@ -11,18 +11,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=580069">Mozilla Bug 580069</a>
 
 <iframe id='iframe' src='file_bug580069_1.html'></iframe>
 
 <script type="application/javascript">
 
-SimpleTest.expectAssertions(1);
-
 SimpleTest.waitForExplicitFinish();
 
 var iframe = document.getElementById('iframe');
 var iframeCw = iframe.contentWindow;
 
 // Called when file_bug580069_1.html loads.
 function page1Load() {
   // This should cause us to load file 2.
--- a/dom/base/nsDOMClassInfo.cpp
+++ b/dom/base/nsDOMClassInfo.cpp
@@ -81,17 +81,16 @@
 #include "nsIDOMCSSSupportsRule.h"
 #include "nsIDOMMozCSSKeyframeRule.h"
 #include "nsIDOMMozCSSKeyframesRule.h"
 #include "nsIDOMCSSCounterStyleRule.h"
 #include "nsIDOMCSSPageRule.h"
 #include "nsIDOMCSSStyleRule.h"
 #include "nsIDOMXULCommandDispatcher.h"
 #include "nsIControllers.h"
-#include "nsIBoxObject.h"
 #ifdef MOZ_XUL
 #include "nsITreeSelection.h"
 #include "nsITreeContentView.h"
 #include "nsITreeView.h"
 #include "nsIXULTemplateBuilder.h"
 #include "nsITreeColumns.h"
 #endif
 #include "nsIDOMXPathNSResolver.h"
@@ -246,18 +245,16 @@ static nsDOMClassInfoData sClassInfoData
 
   // XUL classes
 #ifdef MOZ_XUL
   NS_DEFINE_CHROME_XBL_CLASSINFO_DATA(XULCommandDispatcher, nsDOMGenericSH,
                                       DOM_DEFAULT_SCRIPTABLE_FLAGS)
 #endif
   NS_DEFINE_CHROME_XBL_CLASSINFO_DATA(XULControllers, nsNonDOMObjectSH,
                                       DEFAULT_SCRIPTABLE_FLAGS)
-  NS_DEFINE_CHROME_XBL_CLASSINFO_DATA(BoxObject, nsDOMGenericSH,
-                                      DEFAULT_SCRIPTABLE_FLAGS)
 #ifdef MOZ_XUL
   NS_DEFINE_CHROME_XBL_CLASSINFO_DATA(TreeSelection, nsDOMGenericSH,
                                       DEFAULT_SCRIPTABLE_FLAGS)
   NS_DEFINE_CHROME_XBL_CLASSINFO_DATA(TreeContentView, nsDOMGenericSH,
                                       DEFAULT_SCRIPTABLE_FLAGS)
 #endif
 
 #ifdef MOZ_XUL
@@ -691,20 +688,16 @@ nsDOMClassInfo::Init()
     DOM_CLASSINFO_MAP_ENTRY(nsIDOMXULCommandDispatcher)
   DOM_CLASSINFO_MAP_END
 #endif
 
   DOM_CLASSINFO_MAP_BEGIN_NO_CLASS_IF(XULControllers, nsIControllers)
     DOM_CLASSINFO_MAP_ENTRY(nsIControllers)
   DOM_CLASSINFO_MAP_END
 
-  DOM_CLASSINFO_MAP_BEGIN(BoxObject, nsIBoxObject)
-    DOM_CLASSINFO_MAP_ENTRY(nsIBoxObject)
-  DOM_CLASSINFO_MAP_END
-
 #ifdef MOZ_XUL
   DOM_CLASSINFO_MAP_BEGIN(TreeSelection, nsITreeSelection)
     DOM_CLASSINFO_MAP_ENTRY(nsITreeSelection)
   DOM_CLASSINFO_MAP_END
 
   DOM_CLASSINFO_MAP_BEGIN(TreeContentView, nsITreeContentView)
     DOM_CLASSINFO_MAP_ENTRY(nsITreeContentView)
     DOM_CLASSINFO_MAP_ENTRY(nsITreeView)
@@ -1387,41 +1380,28 @@ nsDOMClassInfo::ShutDown()
 
   sConstructor_id     = JSID_VOID;
   sWrappedJSObject_id = JSID_VOID;
 
   NS_IF_RELEASE(sXPConnect);
   sIsInitialized = false;
 }
 
-static nsDOMConstructorFunc
-FindConstructorFunc(const nsDOMClassInfoData *aDOMClassInfoData)
-{
-  return nullptr;
-}
-
 static nsresult
 BaseStubConstructor(nsIWeakReference* aWeakOwner,
                     const nsGlobalNameStruct *name_struct, JSContext *cx,
                     JS::Handle<JSObject*> obj, const JS::CallArgs &args)
 {
   MOZ_ASSERT(obj);
   MOZ_ASSERT(cx == nsContentUtils::GetCurrentJSContext());
 
   nsresult rv;
   nsCOMPtr<nsISupports> native;
   if (name_struct->mType == nsGlobalNameStruct::eTypeClassConstructor) {
-    const nsDOMClassInfoData* ci_data =
-      &sClassInfoData[name_struct->mDOMClassInfoID];
-    nsDOMConstructorFunc func = FindConstructorFunc(ci_data);
-    if (func) {
-      rv = func(getter_AddRefs(native));
-    } else {
-      rv = NS_ERROR_NOT_AVAILABLE;
-    }
+    rv = NS_ERROR_NOT_AVAILABLE;
   } else if (name_struct->mType == nsGlobalNameStruct::eTypeExternalConstructor) {
     native = do_CreateInstance(name_struct->mCID, &rv);
   } else if (name_struct->mType == nsGlobalNameStruct::eTypeExternalConstructorAlias) {
     native = do_CreateInstance(name_struct->mAlias->mCID, &rv);
   } else {
     native = do_CreateInstance(*name_struct->mData->mConstructorCID, &rv);
   }
   if (NS_FAILED(rv)) {
@@ -1621,17 +1601,17 @@ private:
   static bool IsConstructable(const nsDOMClassInfoData *aData)
   {
     if (IS_EXTERNAL(aData->mCachedClassInfo)) {
       const nsExternalDOMClassInfoData* data =
         static_cast<const nsExternalDOMClassInfoData*>(aData);
       return data->mConstructorCID != nullptr;
     }
 
-    return FindConstructorFunc(aData);
+    return nullptr;
   }
   static bool IsConstructable(const nsGlobalNameStruct *aNameStruct)
   {
     return
       (aNameStruct->mType == nsGlobalNameStruct::eTypeClassConstructor &&
        IsConstructable(&sClassInfoData[aNameStruct->mDOMClassInfoID])) ||
       (aNameStruct->mType == nsGlobalNameStruct::eTypeExternalClassInfo &&
        IsConstructable(aNameStruct->mData)) ||
--- a/dom/base/nsDOMClassInfoClasses.h
+++ b/dom/base/nsDOMClassInfoClasses.h
@@ -14,17 +14,16 @@ DOMCI_CLASS(CSSImportRule)
 DOMCI_CLASS(CSSMediaRule)
 DOMCI_CLASS(CSSNameSpaceRule)
 
 // XUL classes
 #ifdef MOZ_XUL
 DOMCI_CLASS(XULCommandDispatcher)
 #endif
 DOMCI_CLASS(XULControllers)
-DOMCI_CLASS(BoxObject)
 #ifdef MOZ_XUL
 DOMCI_CLASS(TreeSelection)
 DOMCI_CLASS(TreeContentView)
 #endif
 
 #ifdef MOZ_XUL
 DOMCI_CLASS(XULTemplateBuilder)
 DOMCI_CLASS(XULTreeBuilder)
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -170,16 +170,20 @@ DOMInterfaces = {
     'nativeType': 'mozilla::dom::bluetooth::BluetoothPairingHandle',
 },
 
 'BluetoothPairingListener': {
     'nativeType':
       'mozilla::dom::bluetooth::BluetoothPairingListener',
 },
 
+'BoxObject': {
+    'resultNotAddRefed': ['element'],
+},
+
 'CameraCapabilities': {
     'nativeType': 'mozilla::dom::CameraCapabilities',
     'headerFile': 'DOMCameraCapabilities.h'
 },
 
 'CameraControl': {
     'nativeType': 'mozilla::nsDOMCameraControl',
     'headerFile': 'DOMCameraControl.h',
@@ -861,16 +865,20 @@ DOMInterfaces = {
     'headerFile' : 'nsPluginArray.h',
     'nativeType': 'nsPluginElement',
 },
 
 'PluginArray': {
     'nativeType': 'nsPluginArray',
 },
 
+'PopupBoxObject': {
+    'resultNotAddRefed': ['triggerNode', 'anchorNode'],
+},
+
 'Position': {
     'headerFile': 'nsGeoPosition.h'
 },
 
 'PositionError': {
     'headerFile': 'nsGeolocation.h'
 },
 
@@ -1797,31 +1805,30 @@ def addExternalIface(iface, nativeType=N
 addExternalIface('ApplicationCache', nativeType='nsIDOMOfflineResourceList')
 addExternalIface('Counter')
 addExternalIface('CSSRule')
 addExternalIface('RTCDataChannel', nativeType='nsIDOMDataChannel')
 addExternalIface('HitRegionOptions', nativeType='nsISupports')
 addExternalIface('imgINotificationObserver', nativeType='imgINotificationObserver')
 addExternalIface('imgIRequest', nativeType='imgIRequest', notflattened=True)
 addExternalIface('MenuBuilder', nativeType='nsIMenuBuilder', notflattened=True)
-addExternalIface('MozBoxObject', nativeType='nsIBoxObject')
 addExternalIface('MozControllers', nativeType='nsIControllers')
 addExternalIface('MozFrameLoader', nativeType='nsIFrameLoader', notflattened=True)
 addExternalIface('MozFrameRequestCallback', nativeType='nsIFrameRequestCallback',
                  notflattened=True)
 addExternalIface('MozMmsMessage')
 addExternalIface('MozObserver', nativeType='nsIObserver', notflattened=True)
 addExternalIface('MozRDFCompositeDataSource', nativeType='nsIRDFCompositeDataSource',
                  notflattened=True)
 addExternalIface('MozRDFResource', nativeType='nsIRDFResource', notflattened=True)
 addExternalIface('MozSmsMessage')
-addExternalIface('MozTreeBoxObject', nativeType='nsITreeBoxObject',
-                 notflattened=True)
 addExternalIface('MozTreeColumn', nativeType='nsITreeColumn',
                  headerFile='nsITreeColumns.h')
+addExternalIface('MozTreeView', nativeType='nsITreeView',
+                  headerFile='nsITreeView.h')
 addExternalIface('MozWakeLockListener', headerFile='nsIDOMWakeLockListener.h')
 addExternalIface('MozXULTemplateBuilder', nativeType='nsIXULTemplateBuilder')
 addExternalIface('nsIBrowserDOMWindow', nativeType='nsIBrowserDOMWindow',
                  notflattened=True)
 addExternalIface('nsIControllers', nativeType='nsIControllers')
 addExternalIface('nsIDOMCrypto', nativeType='nsIDOMCrypto',
                  headerFile='Crypto.h')
 addExternalIface('nsIInputStreamCallback', nativeType='nsIInputStreamCallback',
@@ -1830,16 +1837,17 @@ addExternalIface('nsIFile', nativeType='
 addExternalIface('nsIMessageBroadcaster', nativeType='nsIMessageBroadcaster',
                  headerFile='nsIMessageManager.h', notflattened=True)
 addExternalIface('nsISelectionListener', nativeType='nsISelectionListener')
 addExternalIface('nsIStreamListener', nativeType='nsIStreamListener', notflattened=True)
 addExternalIface('nsISupports', nativeType='nsISupports')
 addExternalIface('nsIDocShell', nativeType='nsIDocShell', notflattened=True)
 addExternalIface('nsIEditor', nativeType='nsIEditor', notflattened=True)
 addExternalIface('nsIVariant', nativeType='nsIVariant', notflattened=True)
+addExternalIface('nsIScriptableRegion', nativeType='nsIScriptableRegion', notflattened=True)
 addExternalIface('OutputStream', nativeType='nsIOutputStream',
                  notflattened=True)
 addExternalIface('Principal', nativeType='nsIPrincipal',
                  headerFile='nsIPrincipal.h', notflattened=True)
 addExternalIface('StackFrame', nativeType='nsIStackFrame',
                  headerFile='nsIException.h', notflattened=True)
 addExternalIface('URI', nativeType='nsIURI', headerFile='nsIURI.h',
                  notflattened=True)
--- a/dom/bluetooth2/BluetoothAdapter.cpp
+++ b/dom/bluetooth2/BluetoothAdapter.cpp
@@ -9,16 +9,17 @@
 #include "BluetoothUtils.h"
 #include "DOMRequest.h"
 #include "nsTArrayHelpers.h"
 
 #include "mozilla/dom/BluetoothAdapter2Binding.h"
 #include "mozilla/dom/BluetoothAttributeEvent.h"
 #include "mozilla/dom/BluetoothStatusChangedEvent.h"
 #include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/File.h"
 
 #include "mozilla/dom/bluetooth/BluetoothAdapter.h"
 #include "mozilla/dom/bluetooth/BluetoothClassOfDevice.h"
 #include "mozilla/dom/bluetooth/BluetoothDevice.h"
 #include "mozilla/dom/bluetooth/BluetoothDiscoveryHandle.h"
 #include "mozilla/dom/bluetooth/BluetoothPairingListener.h"
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 
@@ -1020,17 +1021,17 @@ BluetoothAdapter::Disconnect(BluetoothDe
   }
   bs->Disconnect(address, serviceUuid, results);
 
   return request.forget();
 }
 
 already_AddRefed<DOMRequest>
 BluetoothAdapter::SendFile(const nsAString& aDeviceAddress,
-                           nsIDOMBlob* aBlob, ErrorResult& aRv)
+                           File& aBlob, ErrorResult& aRv)
 {
   nsCOMPtr<nsPIDOMWindow> win = GetOwner();
   if (!win) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   nsRefPtr<DOMRequest> request = new DOMRequest(win);
@@ -1040,25 +1041,25 @@ BluetoothAdapter::SendFile(const nsAStri
   BluetoothService* bs = BluetoothService::Get();
   if (!bs) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   if (XRE_GetProcessType() == GeckoProcessType_Default) {
     // In-process transfer
-    bs->SendFile(aDeviceAddress, aBlob, results);
+    bs->SendFile(aDeviceAddress, &aBlob, results);
   } else {
     ContentChild *cc = ContentChild::GetSingleton();
     if (!cc) {
       aRv.Throw(NS_ERROR_FAILURE);
       return nullptr;
     }
 
-    BlobChild* actor = cc->GetOrCreateActorForBlob(aBlob);
+    BlobChild* actor = cc->GetOrCreateActorForBlob(&aBlob);
     if (!actor) {
       aRv.Throw(NS_ERROR_FAILURE);
       return nullptr;
     }
 
     bs->SendFile(aDeviceAddress, nullptr, actor, results);
   }
 
--- a/dom/bluetooth2/BluetoothAdapter.h
+++ b/dom/bluetooth2/BluetoothAdapter.h
@@ -13,16 +13,17 @@
 #include "mozilla/dom/BluetoothAdapter2Binding.h"
 #include "mozilla/dom/BluetoothDeviceEvent.h"
 #include "mozilla/dom/Promise.h"
 #include "nsCOMPtr.h"
 
 namespace mozilla {
 namespace dom {
 class DOMRequest;
+class File;
 struct MediaMetaData;
 struct MediaPlayStatus;
 }
 }
 
 BEGIN_BLUETOOTH_NAMESPACE
 
 class BluetoothDevice;
@@ -119,17 +120,17 @@ public:
     Disconnect(BluetoothDevice& aDevice,
                const Optional<short unsigned int>& aServiceUuid,
                ErrorResult& aRv);
   already_AddRefed<DOMRequest> GetConnectedDevices(uint16_t aServiceUuid,
                                                    ErrorResult& aRv);
 
   // OPP file transfer related methods
   already_AddRefed<DOMRequest> SendFile(const nsAString& aDeviceAddress,
-                                        nsIDOMBlob* aBlob,
+                                        File& aBlob,
                                         ErrorResult& aRv);
   already_AddRefed<DOMRequest> StopSendingFile(const nsAString& aDeviceAddress,
                                                ErrorResult& aRv);
   already_AddRefed<DOMRequest>
     ConfirmReceivingFile(const nsAString& aDeviceAddress,
                          bool aConfirmation,
                          ErrorResult& aRv);
 
--- a/dom/bluetooth2/bluedroid/BluetoothOppManager.cpp
+++ b/dom/bluetooth2/bluedroid/BluetoothOppManager.cpp
@@ -10,36 +10,37 @@
 #include "BluetoothService.h"
 #include "BluetoothSocket.h"
 #include "BluetoothUtils.h"
 #include "BluetoothUuid.h"
 #include "ObexBase.h"
 
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 #include "mozilla/dom/ipc/BlobParent.h"
+#include "mozilla/dom/File.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "nsAutoPtr.h"
 #include "nsCExternalHandlerService.h"
 #include "nsIObserver.h"
 #include "nsIObserverService.h"
-#include "nsIDOMFile.h"
 #include "nsIFile.h"
 #include "nsIInputStream.h"
 #include "nsIMIMEService.h"
 #include "nsIOutputStream.h"
 #include "nsIVolumeService.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 
 #define TARGET_SUBDIR "Download/Bluetooth/"
 
 USING_BLUETOOTH_NAMESPACE
 using namespace mozilla;
+using namespace mozilla::dom;
 using namespace mozilla::ipc;
 
 namespace {
 // Sending system message "bluetooth-opp-update-progress" every 50kb
 static const uint32_t kUpdateProgressBase = 50 * 1024;
 
 /*
  * The format of the header of an PUT request is
@@ -344,17 +345,18 @@ BluetoothOppManager::StartSendingNextFil
 }
 
 bool
 BluetoothOppManager::SendFile(const nsAString& aDeviceAddress,
                               BlobParent* aActor)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  nsCOMPtr<nsIDOMBlob> blob = aActor->GetBlob();
+  nsRefPtr<FileImpl> impl = aActor->GetBlobImpl();
+  nsCOMPtr<nsIDOMBlob> blob = new File(nullptr, impl);
 
   return SendFile(aDeviceAddress, blob.get());
 }
 
 bool
 BluetoothOppManager::SendFile(const nsAString& aDeviceAddress,
                               nsIDOMBlob* aBlob)
 {
--- a/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp
+++ b/dom/bluetooth2/bluedroid/BluetoothServiceBluedroid.cpp
@@ -820,17 +820,17 @@ public:
 
   void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE
   {
     sBondingRunnableArray.RemoveElement(mRunnable);
     ReplyStatusError(mRunnable, aStatus, NS_LITERAL_STRING("CreatedPairedDevice"));
   }
 
 private:
-  BluetoothReplyRunnable* mRunnable;
+  nsRefPtr<BluetoothReplyRunnable> mRunnable;
 };
 
 nsresult
 BluetoothServiceBluedroid::CreatePairedDeviceInternal(
   const nsAString& aDeviceAddress, int aTimeout,
   BluetoothReplyRunnable* aRunnable)
 {
   MOZ_ASSERT(NS_IsMainThread());
@@ -855,17 +855,17 @@ public:
 
   void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE
   {
     sUnbondingRunnableArray.RemoveElement(mRunnable);
     ReplyStatusError(mRunnable, aStatus, NS_LITERAL_STRING("RemoveDevice"));
   }
 
 private:
-  BluetoothReplyRunnable* mRunnable;
+  nsRefPtr<BluetoothReplyRunnable> mRunnable;
 };
 
 nsresult
 BluetoothServiceBluedroid::RemoveDeviceInternal(
   const nsAString& aDeviceAddress, BluetoothReplyRunnable* aRunnable)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
--- a/dom/bluetooth2/bluedroid/BluetoothSocket.cpp
+++ b/dom/bluetooth2/bluedroid/BluetoothSocket.cpp
@@ -363,24 +363,37 @@ public:
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     if (mImpl->IsShutdownOnMainThread()) {
       BT_LOGD("mConsumer is null, aborting receive!");
       return;
     }
 
+    if (aConnectionStatus != 0) {
+      mImpl->mConsumer->NotifyError();
+      return;
+    }
+
     mImpl->mConsumer->SetAddress(aBdAddress);
     XRE_GetIOMessageLoop()->PostTask(FROM_HERE, new AcceptTask(mImpl, aFd));
   }
 
   void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE
   {
     MOZ_ASSERT(NS_IsMainThread());
     BT_LOGR("BluetoothSocketInterface::Accept failed: %d", (int)aStatus);
+
+    if (!mImpl->IsShutdownOnMainThread()) {
+      // Instead of NotifyError(), call NotifyDisconnect() to trigger
+      // BluetoothOppManager::OnSocketDisconnect() as
+      // DroidSocketImpl::OnFileCanReadWithoutBlocking() in Firefox OS 2.0 in
+      // order to keep the same behavior and reduce regression risk.
+      mImpl->mConsumer->NotifyDisconnect();
+    }
   }
 
 private:
   DroidSocketImpl* mImpl;
 };
 
 class AcceptRunnable MOZ_FINAL : public SocketIORunnable<DroidSocketImpl>
 {
@@ -499,27 +512,43 @@ public:
     MOZ_ASSERT(mImpl);
   }
 
   void Connect(int aFd, const nsAString& aBdAddress,
                int aConnectionStatus) MOZ_OVERRIDE
   {
     MOZ_ASSERT(NS_IsMainThread());
 
-    if (!mImpl->IsShutdownOnMainThread()) {
-      mImpl->mConsumer->SetAddress(aBdAddress);
+    if (mImpl->IsShutdownOnMainThread()) {
+      BT_LOGD("mConsumer is null, aborting send!");
+      return;
     }
+
+    if (aConnectionStatus != 0) {
+      mImpl->mConsumer->NotifyError();
+      return;
+    }
+
+    mImpl->mConsumer->SetAddress(aBdAddress);
     XRE_GetIOMessageLoop()->PostTask(FROM_HERE,
                                      new SocketConnectTask(mImpl, aFd));
   }
 
   void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE
   {
     MOZ_ASSERT(NS_IsMainThread());
     BT_WARNING("Connect failed: %d", (int)aStatus);
+
+    if (!mImpl->IsShutdownOnMainThread()) {
+      // Instead of NotifyError(), call NotifyDisconnect() to trigger
+      // BluetoothOppManager::OnSocketDisconnect() as
+      // DroidSocketImpl::OnFileCanReadWithoutBlocking() in Firefox OS 2.0 in
+      // order to keep the same behavior and reduce regression risk.
+      mImpl->mConsumer->NotifyDisconnect();
+    }
   }
 
 private:
   DroidSocketImpl* mImpl;
 };
 
 bool
 BluetoothSocket::ConnectSocket(const nsAString& aDeviceAddress, int aChannel)
--- a/dom/bluetooth2/bluedroid/BluetoothSocketHALInterface.cpp
+++ b/dom/bluetooth2/bluedroid/BluetoothSocketHALInterface.cpp
@@ -281,17 +281,17 @@ private:
     iv.iov_len = MSG1_SIZE;
 
     struct msghdr msg;
     memset(&msg, 0, sizeof(msg));
     msg.msg_iov = &iv;
     msg.msg_iovlen = 1;
 
     ssize_t res = TEMP_FAILURE_RETRY(recvmsg(mFd, &msg, MSG_NOSIGNAL));
-    if (res < 0) {
+    if (res <= 0) {
       return STATUS_FAIL;
     }
 
     mLen += res;
 
     return STATUS_SUCCESS;
   }
 
@@ -306,17 +306,17 @@ private:
     struct cmsghdr cmsgbuf[2 * sizeof(cmsghdr) + 0x100];
     memset(&msg, 0, sizeof(msg));
     msg.msg_iov = &iv;
     msg.msg_iovlen = 1;
     msg.msg_control = cmsgbuf;
     msg.msg_controllen = sizeof(cmsgbuf);
 
     ssize_t res = TEMP_FAILURE_RETRY(recvmsg(mFd, &msg, MSG_NOSIGNAL));
-    if (res < 0) {
+    if (res <= 0) {
       return STATUS_FAIL;
     }
 
     mLen += res;
 
     if (msg.msg_flags & (MSG_CTRUNC | MSG_OOB | MSG_ERRQUEUE)) {
       return STATUS_FAIL;
     }
--- a/dom/bluetooth2/bluez/BluetoothOppManager.cpp
+++ b/dom/bluetooth2/bluez/BluetoothOppManager.cpp
@@ -10,36 +10,37 @@
 #include "BluetoothService.h"
 #include "BluetoothSocket.h"
 #include "BluetoothUtils.h"
 #include "BluetoothUuid.h"
 #include "ObexBase.h"
 
 #include "mozilla/dom/bluetooth/BluetoothTypes.h"
 #include "mozilla/dom/ipc/BlobParent.h"
+#include "mozilla/dom/File.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "nsAutoPtr.h"
 #include "nsCExternalHandlerService.h"
 #include "nsIObserver.h"
 #include "nsIObserverService.h"
-#include "nsIDOMFile.h"
 #include "nsIFile.h"
 #include "nsIInputStream.h"
 #include "nsIMIMEService.h"
 #include "nsIOutputStream.h"
 #include "nsIVolumeService.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 
 #define TARGET_SUBDIR "Download/Bluetooth/"
 
 USING_BLUETOOTH_NAMESPACE
 using namespace mozilla;
+using namespace mozilla::dom;
 using namespace mozilla::ipc;
 using mozilla::TimeDuration;
 using mozilla::TimeStamp;
 
 namespace {
 // Sending system message "bluetooth-opp-update-progress" every 50kb
 static const uint32_t kUpdateProgressBase = 50 * 1024;
 
@@ -366,17 +367,18 @@ BluetoothOppManager::StartSendingNextFil
 }
 
 bool
 BluetoothOppManager::SendFile(const nsAString& aDeviceAddress,
                               BlobParent* aActor)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  nsCOMPtr<nsIDOMBlob> blob = aActor->GetBlob();
+  nsRefPtr<FileImpl> impl = aActor->GetBlobImpl();
+  nsCOMPtr<nsIDOMBlob> blob = new File(nullptr, impl);
 
   return SendFile(aDeviceAddress, blob.get());
 }
 
 bool
 BluetoothOppManager::SendFile(const nsAString& aDeviceAddress,
                               nsIDOMBlob* aBlob)
 {
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -3199,17 +3199,17 @@ struct MOZ_STACK_CLASS CanvasBidiProcess
         // in the font, and adjust accordingly.
         // (The same will be true for HTML text layout.)
         const gfxFont::Metrics& metrics = mTextRun->GetFontGroup()->
           GetFirstValidFont()->GetMetrics(gfxFont::eHorizontal);
         mCtx->mTarget->SetTransform(mCtx->mTarget->GetTransform().Copy().
           PreTranslate(baselineOrigin).      // translate origin for rotation
           PreRotate(gfx::Float(M_PI / 2.0)). // turn 90deg clockwise
           PreTranslate(-baselineOrigin).     // undo the translation
-          PreTranslate(Point(0, metrics.emAscent - metrics.emDescent) / 2));
+          PreTranslate(Point(0, (metrics.emAscent - metrics.emDescent) / 2)));
                               // and offset the (alphabetic) baseline of the
                               // horizontally-shaped text from the (centered)
                               // default baseline used for vertical
       }
 
       RefPtr<GlyphRenderingOptions> renderingOptions = font->GetGlyphRenderingOptions();
 
       GlyphBuffer buffer;
--- a/dom/canvas/WebGL2Context.cpp
+++ b/dom/canvas/WebGL2Context.cpp
@@ -73,17 +73,18 @@ WebGLContext::InitWebGL2()
         WebGLExtensionID::OES_texture_half_float,
         WebGLExtensionID::OES_texture_half_float_linear,
         WebGLExtensionID::OES_vertex_array_object,
         WebGLExtensionID::WEBGL_depth_texture,
         WebGLExtensionID::WEBGL_draw_buffers
     };
     const GLFeature sFeatureRequiredArr[] = {
         GLFeature::instanced_non_arrays,
-        GLFeature::transform_feedback2
+        GLFeature::transform_feedback2,
+        GLFeature::invalidate_framebuffer
     };
 
     // check WebGL extensions that are supposed to be natively supported
     for (size_t i = 0; i < size_t(MOZ_ARRAY_LENGTH(sExtensionNativelySupportedArr)); i++)
     {
         WebGLExtensionID extension = sExtensionNativelySupportedArr[i];
 
         if (!IsExtensionSupported(extension)) {
--- a/dom/canvas/WebGL2Context.h
+++ b/dom/canvas/WebGL2Context.h
@@ -243,13 +243,14 @@ public:
 private:
 
     WebGL2Context();
 
     bool ValidateSizedInternalFormat(GLenum internalFormat, const char* info);
     bool ValidateTexStorage(GLenum target, GLsizei levels, GLenum internalformat,
                                 GLsizei width, GLsizei height, GLsizei depth,
                                 const char* info);
+    JS::Value GetTexParameterInternal(const TexTarget& target, GLenum pname) MOZ_OVERRIDE;
 };
 
 } // namespace mozilla
 
 #endif
--- a/dom/canvas/WebGL2ContextFramebuffers.cpp
+++ b/dom/canvas/WebGL2ContextFramebuffers.cpp
@@ -27,27 +27,83 @@ WebGL2Context::FramebufferTextureLayer(G
 }
 
 void
 WebGL2Context::GetInternalformatParameter(JSContext*, GLenum target, GLenum internalformat, GLenum pname, JS::MutableHandleValue retval)
 {
     MOZ_CRASH("Not Implemented.");
 }
 
+// Map attachments intended for the default buffer, to attachments for a non-
+// default buffer.
+static void
+TranslateDefaultAttachments(const dom::Sequence<GLenum>& in, dom::Sequence<GLenum>* out)
+{
+    for (size_t i = 0; i < in.Length(); i++) {
+        switch (in[i]) {
+            case LOCAL_GL_COLOR:
+                out->AppendElement(LOCAL_GL_COLOR_ATTACHMENT0);
+                break;
+            case LOCAL_GL_DEPTH:
+                out->AppendElement(LOCAL_GL_DEPTH_ATTACHMENT);
+                break;
+            case LOCAL_GL_STENCIL:
+                out->AppendElement(LOCAL_GL_STENCIL_ATTACHMENT);
+                break;
+        }
+    }
+}
+
 void
 WebGL2Context::InvalidateFramebuffer(GLenum target, const dom::Sequence<GLenum>& attachments)
 {
-    MOZ_CRASH("Not Implemented.");
+    if (IsContextLost())
+        return;
+    MakeContextCurrent();
+
+    if (target != LOCAL_GL_FRAMEBUFFER)
+        return ErrorInvalidEnumInfo("invalidateFramebuffer: target", target);
+    for (size_t i = 0; i < attachments.Length(); i++) {
+        if (!ValidateFramebufferAttachment(attachments[i], "invalidateFramebuffer"))
+            return;
+    }
+
+    if (!mBoundFramebuffer && !gl->IsDrawingToDefaultFramebuffer()) {
+        dom::Sequence<GLenum> tmpAttachments;
+        TranslateDefaultAttachments(attachments, &tmpAttachments);
+        gl->fInvalidateFramebuffer(target, tmpAttachments.Length(), tmpAttachments.Elements());
+    } else {
+        gl->fInvalidateFramebuffer(target, attachments.Length(), attachments.Elements());
+    }
 }
 
 void
-WebGL2Context::InvalidateSubFramebuffer (GLenum target, const dom::Sequence<GLenum>& attachments,
-                                         GLint x, GLint y, GLsizei width, GLsizei height)
+WebGL2Context::InvalidateSubFramebuffer(GLenum target, const dom::Sequence<GLenum>& attachments,
+                                        GLint x, GLint y, GLsizei width, GLsizei height)
 {
-    MOZ_CRASH("Not Implemented.");
+    if (IsContextLost())
+        return;
+    MakeContextCurrent();
+
+    if (target != LOCAL_GL_FRAMEBUFFER)
+        return ErrorInvalidEnumInfo("invalidateFramebuffer: target", target);
+    for (size_t i = 0; i < attachments.Length(); i++) {
+        if (!ValidateFramebufferAttachment(attachments[i], "invalidateSubFramebuffer"))
+            return;
+    }
+
+    if (!mBoundFramebuffer && !gl->IsDrawingToDefaultFramebuffer()) {
+        dom::Sequence<GLenum> tmpAttachments;
+        TranslateDefaultAttachments(attachments, &tmpAttachments);
+        gl->fInvalidateSubFramebuffer(target, tmpAttachments.Length(), tmpAttachments.Elements(),
+                                      x, y, width, height);
+    } else {
+        gl->fInvalidateSubFramebuffer(target, attachments.Length(), attachments.Elements(),
+                                      x, y, width, height);
+    }
 }
 
 void
 WebGL2Context::ReadBuffer(GLenum mode)
 {
     MOZ_CRASH("Not Implemented.");
 }
 
--- a/dom/canvas/WebGL2ContextTextures.cpp
+++ b/dom/canvas/WebGL2ContextTextures.cpp
@@ -287,17 +287,17 @@ WebGL2Context::TexSubImage3D(GLenum rawT
                                 yoffset == 0 &&
                                 zoffset == 0 &&
                                 width == imageInfo.Width() &&
                                 height == imageInfo.Height() &&
                                 depth == imageInfo.Depth();
         if (coversWholeImage) {
             tex->SetImageDataStatus(texImageTarget, level, WebGLImageDataStatus::InitializedImageData);
         } else {
-            tex->DoDeferredImageInitialization(texImageTarget, level);
+            tex->EnsureNoUninitializedImageData(texImageTarget, level);
         }
     }
 
     GLenum driverType = LOCAL_GL_NONE;
     GLenum driverInternalFormat = LOCAL_GL_NONE;
     GLenum driverFormat = LOCAL_GL_NONE;
     DriverFormatsFromEffectiveInternalFormat(gl,
                                              existingEffectiveInternalFormat,
@@ -340,8 +340,22 @@ WebGL2Context::CompressedTexImage3D(GLen
 
 void
 WebGL2Context::CompressedTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
                                        GLsizei width, GLsizei height, GLsizei depth,
                                        GLenum format, GLsizei imageSize, const dom::ArrayBufferView& data)
 {
     MOZ_CRASH("Not Implemented.");
 }
+
+JS::Value
+WebGL2Context::GetTexParameterInternal(const TexTarget& target, GLenum pname)
+{
+    switch (pname) {
+        case LOCAL_GL_TEXTURE_IMMUTABLE_FORMAT:
+        {
+            GLint i = 0;
+            gl->fGetTexParameteriv(target.get(), pname, &i);
+            return JS::NumberValue(uint32_t(i));
+        }
+    }
+    return WebGLContext::GetTexParameterInternal(target, pname);
+}
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -985,16 +985,18 @@ protected:
     void UndoFakeVertexAttrib0();
 
     static CheckedUint32 GetImageSize(GLsizei height,
                                       GLsizei width,
                                       GLsizei depth,
                                       uint32_t pixelSize,
                                       uint32_t alignment);
 
+    virtual JS::Value GetTexParameterInternal(const TexTarget& target, GLenum pname);
+
     // Returns x rounded to the next highest multiple of y.
     static CheckedUint32 RoundedToNextMultipleOf(CheckedUint32 x, CheckedUint32 y) {
         return ((x + y - 1) / y) * y;
     }
 
     nsRefPtr<gl::GLContext> gl;
 
     CheckedUint32 mGeneration;
--- a/dom/canvas/WebGLContextGL.cpp
+++ b/dom/canvas/WebGLContextGL.cpp
@@ -448,17 +448,17 @@ WebGLContext::CopyTexSubImage2D_base(Tex
 
         // the rect doesn't fit in the framebuffer
 
         // first, we initialize the texture as black
         if (!sub) {
             tex->SetImageInfo(texImageTarget, level, width, height, 1,
                       effectiveInternalFormat,
                       WebGLImageDataStatus::UninitializedImageData);
-            tex->DoDeferredImageInitialization(texImageTarget, level);
+            tex->EnsureNoUninitializedImageData(texImageTarget, level);
         }
 
         // if we are completely outside of the framebuffer, we can exit now with our black texture
         if (   x >= framebufferWidth
             || x+width <= 0
             || y >= framebufferHeight
             || y+height <= 0)
         {
@@ -596,17 +596,17 @@ WebGLContext::CopyTexSubImage2D(GLenum r
     if (imageInfo.HasUninitializedImageData()) {
         bool coversWholeImage = xoffset == 0 &&
                                 yoffset == 0 &&
                                 width == texWidth &&
                                 height == texHeight;
         if (coversWholeImage) {
             tex->SetImageDataStatus(texImageTarget, level, WebGLImageDataStatus::InitializedImageData);
         } else {
-            tex->DoDeferredImageInitialization(texImageTarget, level);
+            tex->EnsureNoUninitializedImageData(texImageTarget, level);
         }
     }
 
     TexInternalFormat internalformat;
     TexType type;
     UnsizedInternalFormatAndTypeFromEffectiveInternalFormat(imageInfo.EffectiveInternalFormat(),
                                              &internalformat, &type);
     return CopyTexSubImage2D_base(texImageTarget, level, internalformat, xoffset, yoffset, x, y, width, height, true);
@@ -922,17 +922,17 @@ WebGLContext::GenerateMipmap(GLenum rawT
     const TexImageTarget imageTarget = (target == LOCAL_GL_TEXTURE_2D)
                                                   ? LOCAL_GL_TEXTURE_2D
                                                   : LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X;
     if (!tex->HasImageInfoAt(imageTarget, 0))
     {
         return ErrorInvalidOperation("generateMipmap: Level zero of texture is not defined.");
     }
 
-    if (!tex->IsFirstImagePowerOfTwo())
+    if (!IsWebGL2() && !tex->IsFirstImagePowerOfTwo())
         return ErrorInvalidOperation("generateMipmap: Level zero of texture does not have power-of-two width and height.");
 
     TexInternalFormat internalformat = tex->ImageInfoAt(imageTarget, 0).EffectiveInternalFormat();
     if (IsTextureFormatCompressed(internalformat))
         return ErrorInvalidOperation("generateMipmap: Texture data at level zero is compressed.");
 
     if (IsExtensionEnabled(WebGLExtensionID::WEBGL_depth_texture) &&
         (IsGLDepthFormat(internalformat) || IsGLDepthStencilFormat(internalformat)))
@@ -1606,16 +1606,22 @@ WebGLContext::GetTexParameter(GLenum raw
 
     const TexTarget target(rawTarget);
 
     if (!activeBoundTextureForTarget(target)) {
         ErrorInvalidOperation("getTexParameter: no texture bound");
         return JS::NullValue();
     }
 
+    return GetTexParameterInternal(target, pname);
+}
+
+JS::Value
+WebGLContext::GetTexParameterInternal(const TexTarget& target, GLenum pname)
+{
     switch (pname) {
         case LOCAL_GL_TEXTURE_MIN_FILTER:
         case LOCAL_GL_TEXTURE_MAG_FILTER:
         case LOCAL_GL_TEXTURE_WRAP_S:
         case LOCAL_GL_TEXTURE_WRAP_T:
         {
             GLint i = 0;
             gl->fGetTexParameteriv(target.get(), pname, &i);
@@ -3425,17 +3431,17 @@ WebGLContext::CompressedTexSubImage2D(GL
     if (levelInfo.HasUninitializedImageData()) {
         bool coversWholeImage = xoffset == 0 &&
                                 yoffset == 0 &&
                                 width == levelInfo.Width() &&
                                 height == levelInfo.Height();
         if (coversWholeImage) {
             tex->SetImageDataStatus(texImageTarget, level, WebGLImageDataStatus::InitializedImageData);
         } else {
-            tex->DoDeferredImageInitialization(texImageTarget, level);
+            tex->EnsureNoUninitializedImageData(texImageTarget, level);
         }
     }
 
     MakeContextCurrent();
     gl->fCompressedTexSubImage2D(texImageTarget.get(), level, xoffset, yoffset, width, height, internalformat, byteLength, view.Data());
 }
 
 JS::Value
@@ -3961,17 +3967,17 @@ WebGLContext::TexSubImage2D_base(TexImag
     if (imageInfo.HasUninitializedImageData()) {
         bool coversWholeImage = xoffset == 0 &&
                                 yoffset == 0 &&
                                 width == imageInfo.Width() &&
                                 height == imageInfo.Height();
         if (coversWholeImage) {
             tex->SetImageDataStatus(texImageTarget, level, WebGLImageDataStatus::InitializedImageData);
         } else {
-            tex->DoDeferredImageInitialization(texImageTarget, level);
+            tex->EnsureNoUninitializedImageData(texImageTarget, level);
         }
     }
     MakeContextCurrent();
 
     size_t   srcStride = srcStrideOrZero ? srcStrideOrZero : checked_alignedRowSize.value();
     uint32_t dstTexelSize = GetBitsPerTexel(existingEffectiveInternalFormat) / 8;
     size_t   dstPlainRowSize = dstTexelSize * width;
     // There are checks above to ensure that this won't overflow.
--- a/dom/canvas/WebGLContextState.cpp
+++ b/dom/canvas/WebGLContextState.cpp
@@ -172,16 +172,28 @@ WebGLContext::GetParameter(JSContext* cx
             if (mBoundVertexArray == mDefaultVertexArray){
                 return WebGLObjectAsJSValue(cx, (WebGLVertexArray *) nullptr, rv);
             }
 
             return WebGLObjectAsJSValue(cx, mBoundVertexArray.get(), rv);
         }
     }
 
+    if (IsWebGL2()) {
+        switch (pname) {
+            case LOCAL_GL_MAX_SAMPLES:
+            case LOCAL_GL_MAX_UNIFORM_BLOCK_SIZE:
+            case LOCAL_GL_MAX_VERTEX_UNIFORM_COMPONENTS: {
+                GLint val;
+                gl->fGetIntegerv(pname, &val);
+                return JS::NumberValue(uint32_t(val));
+            }
+        }
+    }
+
     switch (pname) {
         //
         // String params
         //
         case LOCAL_GL_VENDOR:
             return StringValue(cx, "Mozilla", rv);
         case LOCAL_GL_RENDERER:
             return StringValue(cx, "Mozilla", rv);
--- a/dom/canvas/WebGLContextValidate.cpp
+++ b/dom/canvas/WebGLContextValidate.cpp
@@ -334,16 +334,28 @@ bool WebGLContext::ValidateGLSLString(co
 
 /**
  * Return true if the framebuffer attachment is valid. Attachment must
  * be one of depth/stencil/depth_stencil/color attachment.
  */
 bool
 WebGLContext::ValidateFramebufferAttachment(GLenum attachment, const char* funcName)
 {
+    if (!mBoundFramebuffer) {
+        switch (attachment) {
+            case LOCAL_GL_COLOR:
+            case LOCAL_GL_DEPTH:
+            case LOCAL_GL_STENCIL:
+                return true;
+            default:
+                ErrorInvalidEnum("%s: attachment: invalid enum value 0x%x.", funcName, attachment);
+                return false;
+        }
+    }
+
     if (attachment == LOCAL_GL_DEPTH_ATTACHMENT ||
         attachment == LOCAL_GL_STENCIL_ATTACHMENT ||
         attachment == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT)
     {
         return true;
     }
 
     GLenum colorAttachCount = 1;
@@ -799,18 +811,20 @@ WebGLContext::ValidateTexImageSize(TexIm
                               InfoFrom(func, dims), level, maxTexImageSize);
             return false;
         }
 
         /* GL ES Version 2.0.25 - 3.7.1 Texture Image Specification
          *   "If level is greater than zero, and either width or
          *   height is not a power-of-two, the error INVALID_VALUE is
          *   generated."
+         *
+         * This restriction does not apply to GL ES Version 3.0+.
          */
-        if (level > 0) {
+        if (!IsWebGL2() && level > 0) {
             if (!is_pot_assuming_nonnegative(width)) {
                 ErrorInvalidValue("%s: level >= 0, width of %d must be a power of two.",
                                   InfoFrom(func, dims), width);
                 return false;
             }
 
             if (!is_pot_assuming_nonnegative(height)) {
                 ErrorInvalidValue("%s: level >= 0, height of %d must be a power of two.",
@@ -822,17 +836,17 @@ WebGLContext::ValidateTexImageSize(TexIm
 
     // TODO: WebGL 2
     if (texImageTarget == LOCAL_GL_TEXTURE_3D) {
         if (depth < 0) {
             ErrorInvalidValue("%s: depth must be >= 0", InfoFrom(func, dims));
             return false;
         }
 
-        if (depth > 0 && !is_pot_assuming_nonnegative(depth)) {
+        if (!IsWebGL2() && !is_pot_assuming_nonnegative(depth)) {
             ErrorInvalidValue("%s: level >= 0, depth of %d must be a power of two.",
                               InfoFrom(func, dims), depth);
             return false;
         }
     }
 
     return true;
 }
--- a/dom/canvas/WebGLTexture.cpp
+++ b/dom/canvas/WebGLTexture.cpp
@@ -181,29 +181,29 @@ void
 WebGLTexture::SetCustomMipmap() {
     if (mHaveGeneratedMipmap) {
         // if we were in GeneratedMipmap mode and are now switching to CustomMipmap mode,
         // we need to compute now all the mipmap image info.
 
         // since we were in GeneratedMipmap mode, we know that the level 0 images all have the same info,
         // and are power-of-two.
         ImageInfo imageInfo = ImageInfoAtFace(0, 0);
-        NS_ASSERTION(imageInfo.IsPowerOfTwo(), "this texture is NPOT, so how could GenerateMipmap() ever accept it?");
+        NS_ASSERTION(mContext->IsWebGL2() || imageInfo.IsPowerOfTwo(),
+                     "this texture is NPOT, so how could GenerateMipmap() ever accept it?");
 
         GLsizei size = std::max(std::max(imageInfo.mWidth, imageInfo.mHeight), imageInfo.mDepth);
 
-        // so, the size is a power of two, let's find its log in base 2.
+        // Find floor(log2(size)). (ES 3.0.4, 3.8 - Mipmapping).
         size_t maxLevel = 0;
         for (GLsizei n = size; n > 1; n >>= 1)
             ++maxLevel;
 
         EnsureMaxLevelWithCustomImagesAtLeast(maxLevel);
 
         for (size_t level = 1; level <= maxLevel; ++level) {
-            // again, since the sizes are powers of two, no need for any max(1,x) computation
             imageInfo.mWidth = std::max(imageInfo.mWidth / 2, 1);
             imageInfo.mHeight = std::max(imageInfo.mHeight / 2, 1);
             imageInfo.mDepth = std::max(imageInfo.mDepth / 2, 1);
             for(size_t face = 0; face < mFacesCount; ++face)
                 ImageInfoAtFace(face, level) = imageInfo;
         }
     }
     mHaveGeneratedMipmap = false;
@@ -280,67 +280,69 @@ WebGLTexture::ResolvedFakeBlackStatus() 
         int dim = mTarget == LOCAL_GL_TEXTURE_2D ? 2 : 3;
         if (DoesMinFilterRequireMipmap())
         {
             if (!IsMipmapComplete()) {
                 mContext->GenerateWarning
                     ("%s is a %dD texture, with a minification filter requiring a mipmap, "
                       "and is not mipmap complete (as defined in section 3.7.10).", msg_rendering_as_black, dim);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
-            } else if (!ImageInfoBase().IsPowerOfTwo()) {
+            } else if (!mContext->IsWebGL2() && !ImageInfoBase().IsPowerOfTwo()) {
                 mContext->GenerateWarning
                     ("%s is a %dD texture, with a minification filter requiring a mipmap, "
                       "and either its width or height is not a power of two.", msg_rendering_as_black);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
             }
         }
         else // no mipmap required
         {
             if (!ImageInfoBase().IsPositive()) {
                 mContext->GenerateWarning
                     ("%s is a %dD texture and its width or height is equal to zero.",
                       msg_rendering_as_black, dim);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
-            } else if (!AreBothWrapModesClampToEdge() && !ImageInfoBase().IsPowerOfTwo()) {
+            } else if (!AreBothWrapModesClampToEdge() && !mContext->IsWebGL2() && !ImageInfoBase().IsPowerOfTwo()) {
                 mContext->GenerateWarning
                     ("%s is a %dD texture, with a minification filter not requiring a mipmap, "
                       "with its width or height not a power of two, and with a wrap mode "
                       "different from CLAMP_TO_EDGE.", msg_rendering_as_black, dim);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
             }
         }
     }
     else // cube map
     {
-        bool areAllLevel0ImagesPOT = true;
-        for (size_t face = 0; face < mFacesCount; ++face)
-            areAllLevel0ImagesPOT &= ImageInfoAtFace(face, 0).IsPowerOfTwo();
+        bool legalImageSize = true;
+        if (!mContext->IsWebGL2()) {
+            for (size_t face = 0; face < mFacesCount; ++face)
+                legalImageSize &= ImageInfoAtFace(face, 0).IsPowerOfTwo();
+        }
 
         if (DoesMinFilterRequireMipmap())
         {
             if (!IsMipmapCubeComplete()) {
                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter requiring a mipmap, "
                             "and is not mipmap cube complete (as defined in section 3.7.10).",
                             msg_rendering_as_black);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
-            } else if (!areAllLevel0ImagesPOT) {
+            } else if (!legalImageSize) {
                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter requiring a mipmap, "
                             "and either the width or the height of some level 0 image is not a power of two.",
                             msg_rendering_as_black);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
             }
         }
         else // no mipmap required
         {
             if (!IsCubeComplete()) {
                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter not requiring a mipmap, "
                             "and is not cube complete (as defined in section 3.7.10).",
                             msg_rendering_as_black);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
-            } else if (!AreBothWrapModesClampToEdge() && !areAllLevel0ImagesPOT) {
+            } else if (!AreBothWrapModesClampToEdge() && !legalImageSize) {
                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter not requiring a mipmap, "
                             "with some level 0 image having width or height not a power of two, and with a wrap mode "
                             "different from CLAMP_TO_EDGE.", msg_rendering_as_black);
                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
             }
         }
     }
 
@@ -417,17 +419,17 @@ WebGLTexture::ResolvedFakeBlackStatus() 
             // in this case we know that we can't be dealing with a depth texture per WEBGL_depth_texture
             // and ANGLE_depth_texture (which allow only one image per texture) so we can assume that
             // glTexImage2D is able to upload data to images.
             for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) {
                 for (size_t face = 0; face < mFacesCount; ++face) {
                     TexImageTarget imageTarget = TexImageTargetForTargetAndFace(mTarget, face);
                     const ImageInfo& imageInfo = ImageInfoAt(imageTarget, level);
                     if (imageInfo.mImageDataStatus == WebGLImageDataStatus::UninitializedImageData) {
-                        DoDeferredImageInitialization(imageTarget, level);
+                        EnsureNoUninitializedImageData(imageTarget, level);
                     }
                 }
             }
             mFakeBlackStatus = WebGLTextureFakeBlackStatus::NotNeeded;
         } else {
             // The texture only contains uninitialized image data. In this case,
             // we can use a black texture for it.
             mFakeBlackStatus = WebGLTextureFakeBlackStatus::UninitializedImageData;
@@ -537,20 +539,21 @@ ClearWithTempFB(WebGLContext* context, G
     mask |= LOCAL_GL_COLOR_BUFFER_BIT;
 
     // Last chance!
     return ClearByMask(context, mask);
 }
 
 
 void
-WebGLTexture::DoDeferredImageInitialization(TexImageTarget imageTarget, GLint level)
+WebGLTexture::EnsureNoUninitializedImageData(TexImageTarget imageTarget, GLint level)
 {
     const ImageInfo& imageInfo = ImageInfoAt(imageTarget, level);
-    MOZ_ASSERT(imageInfo.mImageDataStatus == WebGLImageDataStatus::UninitializedImageData);
+    if (!imageInfo.HasUninitializedImageData())
+        return;
 
     mContext->MakeContextCurrent();
 
     // Try to clear with glCLear.
 
     if (imageTarget == LOCAL_GL_TEXTURE_2D) {
         bool cleared = ClearWithTempFB(mContext, GLName(),
                                        imageTarget, level,
--- a/dom/canvas/WebGLTexture.h
+++ b/dom/canvas/WebGLTexture.h
@@ -194,17 +194,17 @@ public:
         MOZ_ASSERT(newStatus != WebGLImageDataStatus::NoImageData ||
                    imageInfo.mImageDataStatus == WebGLImageDataStatus::NoImageData);
         if (imageInfo.mImageDataStatus != newStatus) {
             SetFakeBlackStatus(WebGLTextureFakeBlackStatus::Unknown);
         }
         imageInfo.mImageDataStatus = newStatus;
     }
 
-    void DoDeferredImageInitialization(TexImageTarget imageTarget, GLint level);
+    void EnsureNoUninitializedImageData(TexImageTarget imageTarget, GLint level);
 
 protected:
 
     TexMinFilter mMinFilter;
     TexMagFilter mMagFilter;
     TexWrap mWrapS, mWrapT;
 
     size_t mFacesCount, mMaxLevelWithCustomImages;
--- a/dom/canvas/test/webgl-mochitest.ini
+++ b/dom/canvas/test/webgl-mochitest.ini
@@ -19,8 +19,10 @@ skip-if = toolkit == 'android' #bug 8654
 [webgl-mochitest/test_webgl_conformance.html]
 skip-if = buildapp == 'mulet' || toolkit == 'android' #bug 865443- seperate suite - the non_conf* tests pass except for one on armv6 tests
 [webgl-mochitest/test_webgl_request_context.html]
 skip-if = toolkit == 'android' #bug 865443- seperate suite - the non_conf* tests pass except for one on armv6 tests
 [webgl-mochitest/test_webgl_request_mismatch.html]
 skip-if = toolkit == 'android' #bug 865443- seperate suite - the non_conf* tests pass except for one on armv6 tests
 [webgl-mochitest/test_webgl2_not_exposed.html]
 skip-if = toolkit == 'android' #bug 865443- seperate suite - the non_conf* tests pass except for one on armv6 tests
+[webgl-mochitest/test_webgl2_invalidate_framebuffer.html]
+skip-if = toolkit == 'android' #bug 865443- seperate suite - the non_conf* tests pass except for one on armv6 tests
new file mode 100644
--- /dev/null
+++ b/dom/canvas/test/webgl-mochitest/test_webgl2_invalidate_framebuffer.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+
+<title>WebGL2 test: Framebuffers</title>
+
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script src="driver-info.js"></script>
+<script src="webgl-util.js"></script>
+<body>
+<canvas id="c" width="64" height="64"></canvas>
+<script>
+
+WebGLUtil.withWebGL2('c', function (gl) {
+  gl.invalidateFramebuffer(gl.FRAMEBUFFER, [gl.COLOR]);
+  ok(gl.getError() == 0, 'invalidateFramebuffer');
+  gl.invalidateSubFramebuffer(gl.FRAMEBUFFER, [gl.COLOR], 0, 0, 64, 64);
+  ok(gl.getError() == 0, 'invalidateSubFramebuffer');
+  gl.invalidateFramebuffer(gl.FRAMEBUFFER, [gl.GL_COLOR_ATTACHMENT0]);
+  ok(gl.getError() == gl.INVALID_ENUM, 'invalidateFrameBuffer should fail with GL_COLOR_ATTACHMENT on the default framebuffer');
+}, function () {
+  SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
--- a/dom/canvas/test/webgl-mochitest/webgl-util.js
+++ b/dom/canvas/test/webgl-mochitest/webgl-util.js
@@ -53,16 +53,54 @@ WebGLUtil = (function() {
     if (!gl) {
       error('WebGL context could not be retrieved from \'' + canvasId + '\'.');
       return null;
     }
 
     return gl;
   }
 
+  function withWebGL2(canvasId, callback, onFinished) {
+    var prefArrArr = [
+      ['webgl.force-enabled', true],
+      ['webgl.disable-angle', true],
+      ['webgl.enable-prototype-webgl2', true],
+    ];
+    var prefEnv = {'set': prefArrArr};
+    SpecialPowers.pushPrefEnv(prefEnv, function() {
+      var canvas = document.getElementById(canvasId);
+
+      var gl = null;
+      try {
+        gl = canvas.getContext('webgl2');
+      } catch(e) {}
+
+      if (!gl) {
+        try {
+          gl = canvas.getContext('experimental-webgl2');
+        } catch(e) {}
+      }
+
+      if (!gl) {
+        todo(false, 'WebGL2 is not supported');
+        onFinished();
+        return;
+      }
+
+      function errorFunc(str) {
+        ok(false, 'Error: ' + str);
+      }
+      setErrorFunc(errorFunc);
+      setWarningFunc(errorFunc);
+
+      callback(gl);
+      onFinished();
+    });
+  }
+
   function getContentFromElem(elem) {
     var str = "";
     var k = elem.firstChild;
     while (k) {
       if (k.nodeType == 3)
         str += k.textContent;
 
       k = k.nextSibling;
@@ -120,12 +158,13 @@ WebGLUtil = (function() {
     return prog;
   }
 
   return {
     setErrorFunc: setErrorFunc,
     setWarningFunc: setWarningFunc,
 
     getWebGL: getWebGL,
+    withWebGL2: withWebGL2,
     createShaderById: createShaderById,
     createProgramByIds: createProgramByIds,
   };
 })();
--- a/dom/events/test/test_bug602962.xul
+++ b/dom/events/test/test_bug602962.xul
@@ -25,36 +25,35 @@ var oldWidth = 0, oldHeight = 0;
 var win = null;
 
 function openWindow() {
   win = window.open("chrome://mochitests/content/chrome/dom/events/test/bug602962.xul", "_blank", "width=600,height=600");
 }
 
 function doTest() {
   scrollbox = win.document.getElementById("page-scrollbox");
-  sbo = scrollbox.boxObject.QueryInterface(Components.interfaces.nsIScrollBoxObject);
+  sbo = scrollbox.boxObject;
   content = win.document.getElementById("page-box");
   content.style.width = 400 + "px";
   
   win.addEventListener("resize", function() {
     win.removeEventListener("resize", arguments.callee, false);
 
     sbo.scrollBy(200, 0);
     setTimeout(function() { resize(); }, 0);
   }, false);
 
   oldWidth = win.outerWidth;
   oldHeight = win.outerHeight;
   win.resizeTo(200, 400);
 }
 
 function resize() {
-  let x = {}, y = {};
-  sbo.getPosition(x, y);
-  scrollX = x.value, scrollY = y.value;
+  scrollX = sbo.positionX;
+  scrollY = sbo.positionY;
 
   win.addEventListener("resize", function() {
     content.style.width = (oldWidth + 400) + "px";
     win.removeEventListener("resize", arguments.callee, true);
     
     setTimeout(function() {
       finish();
     }, 0);
@@ -67,20 +66,18 @@ function finish() {
   if (win.outerWidth != oldWidth ||
       win.outerHeight != oldHeight) {
     // We should eventually get back to the original size.
     setTimeout(finish, 0);
     return;
   }
   sbo.scrollBy(scrollX, scrollY);
 
-  let x = {}, y = {};
-  sbo.getPosition(x, y);
-  is(x.value, 200, "Scroll X should have been restored to the value before the resize");
-  is(y.value, 0, "Scroll Y should have been restored to the value before the resize");
+  is(sbo.positionX, 200, "Scroll X should have been restored to the value before the resize");
+  is(sbo.positionY, 0, "Scroll Y should have been restored to the value before the resize");
 
   is(win.outerWidth, oldWidth, "Width should be resized");
   is(win.outerHeight, oldHeight, "Height should be resized");
   win.close();
   SimpleTest.finish();
 }
 
 SimpleTest.waitForExplicitFinish();
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -3017,16 +3017,17 @@ TabChild::RecvUIResolutionChanged()
   nsRefPtr<nsPresContext> presContext = presShell->GetPresContext();
   presContext->UIResolutionChanged();
   return true;
 }
 
 TabChildGlobal::TabChildGlobal(TabChildBase* aTabChild)
 : mTabChild(aTabChild)
 {
+  SetIsNotDOMBinding();
 }
 
 TabChildGlobal::~TabChildGlobal()
 {
 }
 
 void
 TabChildGlobal::Init()
--- a/dom/locales/en-US/chrome/layout/css.properties
+++ b/dom/locales/en-US/chrome/layout/css.properties
@@ -101,16 +101,19 @@ PEExpectedInt=Expected an integer but fo
 PEColorBadRGBContents=Expected number or percentage in rgb() but found '%1$S'.
 PEColorComponentBadTerm=Expected '%2$S' but found '%1$S'.
 PEColorHueEOF=hue
 PEExpectedComma=Expected ',' but found '%1$S'.
 PEColorSaturationEOF=saturation
 PEColorLightnessEOF=lightness
 PEColorOpacityEOF=opacity in color value
 PEExpectedNumber=Expected a number but found '%1$S'.
+PEPositionEOF=<position>
+PEExpectedPosition=Expected <position> but found '%1$S'.
+PEExpectedRadius=Expected radius but found '%1$S'.
 PEExpectedCloseParen=Expected ')' but found '%1$S'.
 PEDeclEndEOF=';' or '}' to end declaration
 PEParseDeclarationNoColon=Expected ':' but found '%1$S'.
 PEParseDeclarationDeclExpected=Expected declaration but found '%1$S'.
 PEEndOfDeclEOF=end of declaration
 PEImportantEOF=important
 PEExpectedImportant=Expected 'important' but found '%1$S'.
 PEBadDeclEnd=Expected ';' to terminate declaration but found '%1$S'.
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -2434,17 +2434,17 @@ PeerConnectionWrapper.prototype = {
   },
 
   /**
    * Checks that we are getting the media streams we expect.
    *
    * @param {object} stats
    *        The stats to check from this PeerConnectionWrapper
    */
-  checkStats : function PCW_checkStats(stats) {
+  checkStats : function PCW_checkStats(stats, twoMachines) {
     function toNum(obj) {
       return obj? obj : 0;
     }
     function numTracks(streams) {
       var n = 0;
       streams.forEach(function(stream) {
           n += stream.getAudioTracks().length + stream.getVideoTracks().length;
         });
@@ -2456,17 +2456,23 @@ PeerConnectionWrapper.prototype = {
     // Use spec way of enumerating stats
     var counters = {};
     for (var key in stats) {
       if (stats.hasOwnProperty(key)) {
         var res = stats[key];
         // validate stats
         ok(res.id == key, "Coherent stats id");
         var nowish = Date.now() + 1000;        // TODO: clock drift observed
+        if (twoMachines) {
+          nowish += 10000; // let's be very relaxed about clock sync
+        }
         var minimum = this.whenCreated - 1000; // on Windows XP (Bug 979649)
+        if (twoMachines) {
+          minimum -= 10000; // let's be very relaxed about clock sync
+        }
         if (isWinXP) {
           todo(false, "Can't reliably test rtcp timestamps on WinXP (Bug 979649)");
         } else {
           ok(res.timestamp >= minimum,
              "Valid " + (res.isRemote? "rtcp" : "rtp") + " timestamp " +
                  res.timestamp + " >= " + minimum + " (" +
                  (res.timestamp - minimum) + " ms)");
           ok(res.timestamp <= nowish,
--- a/dom/media/tests/mochitest/templates.js
+++ b/dom/media/tests/mochitest/templates.js
@@ -473,26 +473,26 @@ var commandsPeerConnection = [
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_CHECK_STATS',
     function (test) {
       test.pcLocal.getStats(null, function(stats) {
-        test.pcLocal.checkStats(stats);
+        test.pcLocal.checkStats(stats, test.steeplechase);
         test.next();
       });
     }
   ],
   [
     'PC_REMOTE_CHECK_STATS',
     function (test) {
       test.pcRemote.getStats(null, function(stats) {
-        test.pcRemote.checkStats(stats);
+        test.pcRemote.checkStats(stats, test.steeplechase);
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_CHECK_GETSTATS_AUDIOTRACK_OUTBOUND',
     function (test) {
       var pc = test.pcLocal;
--- a/dom/system/gonk/NetworkUtils.cpp
+++ b/dom/system/gonk/NetworkUtils.cpp
@@ -735,18 +735,18 @@ void NetworkUtils::postTetherInterfaceLi
                                            CommandCallback aCallback,
                                            NetworkResultOptions& aResult)
 {
   // Send the dummy command to continue the function chain.
   char command[MAX_COMMAND_SIZE];
   snprintf(command, MAX_COMMAND_SIZE - 1, "%s", DUMMY_COMMAND);
 
   char buf[BUF_SIZE];
-  const char* reason = NS_ConvertUTF16toUTF8(aResult.mResultReason).get();
-  memcpy(buf, reason, strlen(reason));
+  NS_ConvertUTF16toUTF8 reason(aResult.mResultReason);
+  memcpy(buf, reason.get(), reason.Length() + 1);
   split(buf, INTERFACE_DELIMIT, GET_FIELD(mInterfaceList));
 
   doCommand(command, aChain, aCallback);
 }
 
 void NetworkUtils::setIpForwardingEnabled(CommandChain* aChain,
                                           CommandCallback aCallback,
                                           NetworkResultOptions& aResult)
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -668,16 +668,18 @@ var interfaceNamesInGlobalScope =
     "MediaStreamAudioSourceNode",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "MediaStreamEvent", pref: "media.peerconnection.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "MediaStreamTrackEvent", pref: "media.peerconnection.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "MediaStreamTrack",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "MenuBoxObject", xbl: true},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "MessageEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "MessagePort",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "MimeType",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "MimeTypeArray",
 // IMPORTANT: Do not change this list without review from a DOM peer!
@@ -858,16 +860,18 @@ var interfaceNamesInGlobalScope =
     "PluginArray",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PointerEvent", pref: "dom.w3c_pointer_events.enabled"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PopStateEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PopupBlockedEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "PopupBoxObject", xbl: true},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "ProcessingInstruction",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ProgressEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Promise",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PropertyNodeList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
new file mode 100644
--- /dev/null
+++ b/dom/webidl/BoxObject.webidl
@@ -0,0 +1,33 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[Func="IsChromeOrXBL"]
+interface BoxObject {
+  readonly attribute Element? element;
+
+  readonly attribute long x;
+  readonly attribute long y;
+  [Throws]
+  readonly attribute long screenX;
+  [Throws]
+  readonly attribute long screenY;
+  readonly attribute long width;
+  readonly attribute long height;
+
+  nsISupports? getPropertyAsSupports(DOMString propertyName);
+  void setPropertyAsSupports(DOMString propertyName, nsISupports value);
+  [Throws]
+  DOMString? getProperty(DOMString propertyName);
+  void setProperty(DOMString propertyName, DOMString propertyValue);
+  void removeProperty(DOMString propertyName);
+
+  // for stepping through content in the expanded dom with box-ordinal-group order
+  readonly attribute Element? parentBox;
+  readonly attribute Element? firstChild;
+  readonly attribute Element? lastChild;
+  readonly attribute Element? nextSibling;
+  readonly attribute Element? previousSibling;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/ContainerBoxObject.webidl
@@ -0,0 +1,12 @@
+
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[NoInterfaceObject]
+interface ContainerBoxObject : BoxObject {
+    [ChromeOnly]
+    readonly attribute nsIDocShell? docShell;
+};
--- a/dom/webidl/LegacyQueryInterface.webidl
+++ b/dom/webidl/LegacyQueryInterface.webidl
@@ -15,16 +15,17 @@ interface LegacyQueryInterface {
   // Legacy QueryInterface, only exposed to chrome or XBL code on the
   // main thread.
   [Exposed=Window]
   nsISupports queryInterface(IID iid);
 };
 
 Attr implements LegacyQueryInterface;
 BarProp implements LegacyQueryInterface;
+BoxObject implements LegacyQueryInterface;
 CaretPosition implements LegacyQueryInterface;
 Comment implements LegacyQueryInterface;
 Crypto implements LegacyQueryInterface;
 CSSPrimitiveValue implements LegacyQueryInterface;
 CSSStyleDeclaration implements LegacyQueryInterface;
 CSSValueList implements LegacyQueryInterface;
 DOMImplementation implements LegacyQueryInterface;
 DOMParser implements LegacyQueryInterface;
new file mode 100644
--- /dev/null
+++ b/dom/webidl/ListBoxObject.webidl
@@ -0,0 +1,21 @@
+
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[NoInterfaceObject]
+interface ListBoxObject : BoxObject {
+
+  long getRowCount();
+  long getNumberOfVisibleRows();
+  long getIndexOfFirstVisibleRow();
+
+  void ensureIndexIsVisible(long rowIndex);
+  void scrollToIndex(long rowIndex);
+  void scrollByLines(long numLines);
+
+  Element? getItemAtIndex(long index);
+  long getIndexOfItem(Element item);
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MenuBoxObject.webidl
@@ -0,0 +1,19 @@
+
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[Func="IsChromeOrXBL"]
+interface MenuBoxObject : BoxObject {
+
+  void openMenu(boolean openFlag);
+
+  attribute Element? activeChild;
+
+  boolean handleKeyPress(KeyboardEvent keyEvent);
+
+  readonly attribute boolean openedWithKey;
+
+};
rename from layout/xul/nsIPopupBoxObject.idl
rename to dom/webidl/PopupBoxObject.webidl
--- a/layout/xul/nsIPopupBoxObject.idl
+++ b/dom/webidl/PopupBoxObject.webidl
@@ -1,84 +1,77 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#include "nsISupports.idl"
-
-interface nsIDOMElement;
-interface nsIDOMNode;
-interface nsIDOMEvent;
-interface nsIDOMClientRect;
-
-[scriptable, uuid(492861e3-d168-410f-b2bb-6eb8ce503d4a)]
-interface nsIPopupBoxObject : nsISupports
+[Func="IsChromeOrXBL"]
+interface PopupBoxObject : BoxObject
 {
   /**
    *  This method is deprecated. Use openPopup or openPopupAtScreen instead.
    */
-  void showPopup(in nsIDOMElement srcContent, in nsIDOMElement popupContent,
-                 in long xpos, in long ypos,
-                 in wstring popupType, in wstring anchorAlignment, 
-                 in wstring popupAlignment);
+  void showPopup(Element? srcContent, Element popupContent,
+                 long xpos, long ypos,
+                 DOMString popupType, DOMString anchorAlignment,
+                 DOMString popupAlignment);
 
   /**
    *  Hide the popup if it is open. The cancel argument is used as a hint that
    *  the popup is being closed because it has been cancelled, rather than
    *  something being selected within the panel.
    *
    * @param cancel if true, then the popup is being cancelled.
    */
-  void hidePopup([optional] in bool cancel);
+  void hidePopup(optional boolean cancel = false);
 
-  /** 
+  /**
    * Allow the popup to automatically position itself.
    */
   attribute boolean autoPosition;
 
   /**
    * If keyboard navigation is enabled, the keyboard may be used to navigate
    * the menuitems on the popup. Enabling keyboard navigation is the default
    * behaviour and will install capturing key event listeners on the popup
    * that do not propagate key events to the contents. If you wish to place
    * elements in a popup which accept key events, such as textboxes, keyboard
    * navigation should be disabled.
    *
    * Setting ignorekeys="true" on the popup element also disables keyboard
    * navigation, and is recommended over calling this method.
    */
-  void enableKeyboardNavigator(in boolean enableKeyboardNavigator);
+  void enableKeyboardNavigator(boolean enableKeyboardNavigator);
 
-  /** 
+  /**
    * Enable automatic popup dismissal. This only has effect when called
    * on an open popup.
    */
-  void enableRollup(in boolean enableRollup);
+  void enableRollup(boolean enableRollup);
 
   /**
    * Control whether the event that caused the popup to be automatically
    * dismissed ("rolled up") should be consumed, or dispatched as a
    * normal event.  This should be set immediately before calling showPopup()
    * if non-default behavior is desired.
    */
-  const uint32_t ROLLUP_DEFAULT = 0;   /* widget/platform default */
-  const uint32_t ROLLUP_CONSUME = 1;   /* consume the rollup event */
-  const uint32_t ROLLUP_NO_CONSUME = 2; /* don't consume the rollup event */
-  void setConsumeRollupEvent(in uint32_t consume);
+  const unsigned long ROLLUP_DEFAULT = 0;   /* widget/platform default */
+  const unsigned long ROLLUP_CONSUME = 1;   /* consume the rollup event */
+  const unsigned long ROLLUP_NO_CONSUME = 2; /* don't consume the rollup event */
+  void setConsumeRollupEvent(unsigned long consume);
 
-  /** 
+  /**
    * Size the popup to the given dimensions
    */
-  void sizeTo(in long width, in long height);
+  void sizeTo(long width, long height);
 
   /**
    * Move the popup to a point on screen in CSS pixels.
    */
-  void moveTo(in long left, in long top);
+  void moveTo(long left, long top);
 
   /**
    * Open the popup relative to a specified node at a specific location.
    *
    * The popup may be either anchored to another node or opened freely.
    * To anchor a popup to a node, supply an anchor node and set the position
    * to a string indicating the manner in which the popup should be anchored.
    * Possible values for position are:
@@ -88,17 +81,17 @@ interface nsIPopupBoxObject : nsISupport
    *
    * The anchor node does not need to be in the same document as the popup.
    *
    * If the attributesOverride argument is true, the popupanchor, popupalign
    * and position attributes on the popup node override the position value
    * argument. If attributesOverride is false, the attributes are only used
    * if position is empty.
    *
-   * For an anchored popup, the x and y arguments may be used to offset the 
+   * For an anchored popup, the x and y arguments may be used to offset the
    * popup from its anchored position by some distance, measured in CSS pixels.
    * x increases to the right and y increases down. Negative values may also
    * be used to move to the left and upwards respectively.
    *
    * Unanchored popups may be created by supplying null as the anchor node.
    * An unanchored popup appears at the position specified by x and y,
    * relative to the viewport of the document containing the popup node. In
    * this case, position and attributesOverride are ignored.
@@ -106,82 +99,75 @@ interface nsIPopupBoxObject : nsISupport
    * @param anchorElement the node to anchor the popup to, may be null
    * @param position manner is which to anchor the popup to node
    * @param x horizontal offset
    * @param y vertical offset
    * @param isContextMenu true for context menus, false for other popups
    * @param attributesOverride true if popup node attributes override position
    * @param triggerEvent the event that triggered this popup (mouse click for example)
    */
-  void openPopup(in nsIDOMElement anchorElement,
-                 in AString position,
-                 in long x, in long y,
-                 in boolean isContextMenu,
-                 in boolean attributesOverride,
-                 in nsIDOMEvent triggerEvent);
+  void openPopup(Element? anchorElement,
+                 DOMString position,
+                 long x, long y,
+                 boolean isContextMenu,
+                 boolean attributesOverride,
+                 Event? triggerEvent);
 
   /**
    * Open the popup at a specific screen position specified by x and y. This
    * position may be adjusted if it would cause the popup to be off of the
    * screen. The x and y coordinates are measured in CSS pixels, and like all
    * screen coordinates, are given relative to the top left of the primary
    * screen.
    *
    * @param isContextMenu true for context menus, false for other popups
    * @param x horizontal screen position
    * @param y vertical screen position
    * @param triggerEvent the event that triggered this popup (mouse click for example)
    */
-  void openPopupAtScreen(in long x, in long y,
-                         in boolean isContextMenu,
-                         in nsIDOMEvent triggerEvent);
+  void openPopupAtScreen(long x, long y,
+                         boolean isContextMenu,
+                         Event? triggerEvent);
 
   /**
    * Returns the state of the popup:
    *   closed - the popup is closed
    *   open - the popup is open
    *   showing - the popup is in the process of being shown
    *   hiding - the popup is in the process of being hidden
    */
-  readonly attribute AString popupState;
+  readonly attribute DOMString popupState;
 
   /**
    * The node that triggered the popup. If the popup is not open, will return
    * null.
    */
-  readonly attribute nsIDOMNode triggerNode;
+  readonly attribute Node? triggerNode;
 
   /**
    * Retrieve the anchor that was specified to openPopup or for menupopups in a
    * menu, the parent menu.
    */
-  readonly attribute nsIDOMElement anchorNode;
+  readonly attribute Element? anchorNode;
 
   /**
    * Retrieve the screen rectangle of the popup, including the area occupied by
    * any titlebar or borders present.
    */
-  nsIDOMClientRect getOuterScreenRect();
+  DOMRect getOuterScreenRect();
 
   /**
    * Move an open popup to the given anchor position. The arguments have the same
    * meaning as the corresponding argument to openPopup. This method has no effect
    * on popups that are not open.
    */
-  void moveToAnchor(in nsIDOMElement anchorElement,
-                    in AString position,
-                    in long x, in long y,
-                    in boolean attributesOverride);
+  void moveToAnchor(Element? anchorElement,
+                    DOMString position,
+                    long x, long y,
+                    boolean attributesOverride);
 
   /** Returns the alignment position where the popup has appeared relative to its
    *  anchor node or point, accounting for any flipping that occurred.
    */
-  readonly attribute AString alignmentPosition;
+  readonly attribute DOMString alignmentPosition;
   readonly attribute long alignmentOffset;
-};
 
-%{C++
-class nsIBoxObject;
-
-nsresult
-NS_NewPopupBoxObject(nsIBoxObject** aResult);
-
-%}
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/ScrollBoxObject.webidl
@@ -0,0 +1,70 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+[NoInterfaceObject]
+interface ScrollBoxObject : BoxObject {
+
+  /**
+   * Scroll to the given coordinates, in css pixels.
+   * (0,0) will put the top left corner of the scrolled element's padding-box
+   * at the top left corner of the scrollport (which is its inner-border-box).
+   * Values will be clamped to legal values.
+   */
+  [Throws]
+  void scrollTo(long x, long y);
+
+  /**
+   * Scroll the given amount of device pixels to the right and down.
+   * Values will be clamped to make the resuling position legal.
+   */
+  [Throws]
+  void scrollBy(long dx, long dy);
+  [Throws]
+  void scrollByLine(long dlines);
+  [Throws]
+  void scrollByIndex(long dindexes);
+  [Throws]
+  void scrollToLine(long line);
+  [Throws]
+  void scrollToElement(Element child);
+  [Throws]
+  void scrollToIndex(long index);
+
+  /**
+   * Get the current scroll position in css pixels.
+   * @see scrollTo for the definition of x and y.
+   */
+  [Pure, Throws]
+  readonly attribute long positionX;
+  [Pure, Throws]
+  readonly attribute long positionY;
+  [Pure, Throws]
+  readonly attribute long scrolledWidth;
+  [Pure, Throws]
+  readonly attribute long scrolledHeight;
+
+  /**
+   * DEPRECATED: Please use positionX and positionY
+   *
+   * Get the current scroll position in css pixels.
+   * @see scrollTo for the definition of x and y.
+   */
+  [Throws]
+  void getPosition(object x, object y);
+
+  /**
+   * DEPRECATED: Please use scrolledWidth and scrolledHeight
+   */
+  [Throws]
+  void getScrolledSize(object width, object height);
+
+  [Throws]
+  void ensureElementIsVisible(Element child);
+  [Throws]
+  void ensureIndexIsVisible(long index);
+  [Throws]
+  void ensureLineIsVisible(long line);
+};
copy from layout/xul/tree/nsITreeBoxObject.idl
copy to dom/webidl/TreeBoxObject.webidl
--- a/layout/xul/tree/nsITreeBoxObject.idl
+++ b/dom/webidl/TreeBoxObject.webidl
@@ -1,64 +1,68 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#include "nsISupports.idl"
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
 
-interface nsIDOMElement;
-interface nsITreeView;
-interface nsITreeSelection;
-interface nsITreeColumn;
-interface nsITreeColumns;
+interface MozTreeView;
+interface MozTreeColumn;
 interface nsIScriptableRegion;
 
-[scriptable, uuid(64BA5199-C4F4-4498-BBDC-F8E4C369086C)]
-interface nsITreeBoxObject : nsISupports
-{
+dictionary TreeCellInfo {
+    long row = 0;
+    MozTreeColumn? col = null;
+    DOMString childElt = "";
+};
+
+[NoInterfaceObject]
+interface TreeBoxObject : BoxObject {
+
   /**
    * Obtain the columns.
    */
-  readonly attribute nsITreeColumns columns;
+  readonly attribute TreeColumns? columns;
 
   /**
    * The view that backs the tree and that supplies it with its data.
    * It is dynamically settable, either using a view attribute on the
    * tree tag or by setting this attribute to a new value.
    */
-  attribute nsITreeView view;
+  [SetterThrows]
+  attribute MozTreeView? view;
 
   /**
    * Whether or not we are currently focused.
    */
   attribute boolean focused;
 
   /**
    * Obtain the treebody content node
    */
-  readonly attribute nsIDOMElement treeBody;
+  readonly attribute Element? treeBody;
 
   /**
    * Obtain the height of a row.
    */
   readonly attribute long rowHeight;
 
   /**
    * Obtain the width of a row.
    */
   readonly attribute long rowWidth;
 
   /**
-   * Get the pixel position of the horizontal scrollbar. 
+   * Get the pixel position of the horizontal scrollbar.
    */
   readonly attribute long horizontalPosition;
 
   /**
-   * Return the region for the visible parts of the selection, in device pixels.
+   * Return the region for the visible parts of the selection, in device pixels
    */
   readonly attribute nsIScriptableRegion selectionRegion;
 
   /**
    * Get the index of the first visible row.
    */
   long getFirstVisibleRow();
 
@@ -70,110 +74,125 @@ interface nsITreeBoxObject : nsISupports
   /**
    * Gets the number of possible visible rows.
    */
   long getPageLength();
 
   /**
    * Ensures that a row at a given index is visible.
    */
-  void ensureRowIsVisible(in long index);
+  void ensureRowIsVisible(long index);
 
   /**
    * Ensures that a given cell in the tree is visible.
    */
-  void ensureCellIsVisible(in long row, in nsITreeColumn col);
+  void ensureCellIsVisible(long row, MozTreeColumn? col);
 
   /**
    * Scrolls such that the row at index is at the top of the visible view.
    */
-  void scrollToRow(in long index);
+  void scrollToRow(long index);
 
   /**
    * Scroll the tree up or down by numLines lines. Positive
    * values move down in the tree. Prevents scrolling off the
-   * end of the tree. 
+   * end of the tree.
    */
-  void scrollByLines(in long numLines);
+  void scrollByLines(long numLines);
 
   /**
    * Scroll the tree up or down by numPages pages. A page
    * is considered to be the amount displayed by the tree.
    * Positive values move down in the tree. Prevents scrolling
    * off the end of the tree.
    */
-  void scrollByPages(in long numPages);
-  
-  /**
-   * Scrolls such that a given cell is visible (if possible) 
-   * at the top left corner of the visible view. 
-   */
-  void scrollToCell(in long row, in nsITreeColumn col);
+  void scrollByPages(long numPages);
 
   /**
-   * Scrolls horizontally so that the specified column is 
+   * Scrolls such that a given cell is visible (if possible)
+   * at the top left corner of the visible view.
+   */
+  void scrollToCell(long row, MozTreeColumn? col);
+
+  /**
+   * Scrolls horizontally so that the specified column is
    * at the left of the view (if possible).
    */
-  void scrollToColumn(in nsITreeColumn col);
+  void scrollToColumn(MozTreeColumn? col);
 
   /**
    * Scroll to a specific horizontal pixel position.
    */
-  void scrollToHorizontalPosition(in long horizontalPosition);
+  void scrollToHorizontalPosition(long horizontalPosition);
 
   /**
    * Invalidation methods for fine-grained painting control.
    */
   void invalidate();
-  void invalidateColumn(in nsITreeColumn col);
-  void invalidateRow(in long index);
-  void invalidateCell(in long row, in nsITreeColumn col);
-  void invalidateRange(in long startIndex, in long endIndex);
-  void invalidateColumnRange(in long startIndex, in long endIndex,
-                             in nsITreeColumn col);
+  void invalidateColumn(MozTreeColumn? col);
+  void invalidateRow(long index);
+  void invalidateCell(long row, MozTreeColumn? col);
+  void invalidateRange(long startIndex, long endIndex);
+  void invalidateColumnRange(long startIndex, long endIndex, MozTreeColumn? col);
 
   /**
    * A hit test that can tell you what row the mouse is over.
    * returns -1 for invalid mouse coordinates.
    *
    * The coordinate system is the client coordinate system for the
    * document this boxObject lives in, and the units are CSS pixels.
    */
-  long getRowAt(in long x, in long y);
+  long getRowAt(long x, long y);
 
   /**
-   * A hit test that can tell you what cell the mouse is over.  Row is the row index
-   * hit,  returns -1 for invalid mouse coordinates.  ColID is the column hit.
-   * ChildElt is the pseudoelement hit: this can have values of
+   * A hit test that can tell you what cell the mouse is over.
+   * TreeCellInfo.row is the row index hit,  returns -1 for invalid mouse
+   * coordinates.  TreeCellInfo.col is the column hit.
+   * TreeCellInfo.childElt is the pseudoelement hit: this can have values of
    * "cell", "twisty", "image", and "text".
    *
    * The coordinate system is the client coordinate system for the
    * document this boxObject lives in, and the units are CSS pixels.
    */
-  void getCellAt(in long x, in long y, out long row, out nsITreeColumn col, out ACString childElt);
+  [Throws]
+  TreeCellInfo getCellAt(long x, long y);
 
-  /** 
-   * Find the coordinates of an element within a specific cell. 
+  /**
+   * DEPRECATED: please use above version
+   */
+  [Throws]
+  void getCellAt(long x, long y, object row, object column, object childElt);
+
+  /**
+   * Find the coordinates of an element within a specific cell.
    */
-  void getCoordsForCellItem(in long row, in nsITreeColumn col, in ACString element, 
-                            out long x, out long y, out long width, out long height);
+  [Throws]
+  DOMRect? getCoordsForCellItem(long row, MozTreeColumn col, DOMString element);
 
-  /** 
+  /**
+   * DEPRECATED: Please use above version
+   */
+  [Throws]
+  void getCoordsForCellItem(long row, MozTreeColumn col, DOMString element,
+                            object x, object y, object width, object height);
+
+  /**
    * Determine if the text of a cell is being cropped or not.
    */
-  boolean isCellCropped(in long row, in nsITreeColumn col);
+  [Throws]
+  boolean isCellCropped(long row, MozTreeColumn? col);
 
   /**
    * The view is responsible for calling these notification methods when
    * rows are added or removed.  Index is the position at which the new
    * rows were added or at which rows were removed.  For
    * non-contiguous additions/removals, this method should be called multiple times.
    */
-  void rowCountChanged(in long index, in long count);
-  
+  void rowCountChanged(long index, long count);
+
   /**
    * Notify the tree that the view is about to perform a batch
    * update, that is, add, remove or invalidate several rows at once.
    * This must be followed by calling endUpdateBatch(), otherwise the tree
    * will get out of sync.
    */
   void beginUpdateBatch();
 
--- a/dom/webidl/TreeColumns.webidl
+++ b/dom/webidl/TreeColumns.webidl
@@ -1,21 +1,20 @@
 /* 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/. */
 
-interface MozTreeBoxObject;
 interface MozTreeColumn;
 
 [Func="IsChromeOrXBL"]
 interface TreeColumns {
   /**
    * The tree widget for these columns.
    */
-  readonly attribute MozTreeBoxObject? tree;
+  readonly attribute TreeBoxObject? tree;
 
   /**
    * The number of columns.
    */
   readonly attribute unsigned long count;
 
   /**
    * An alias for count (for the benefit of scripts which treat this as an
--- a/dom/webidl/XULDocument.webidl
+++ b/dom/webidl/XULDocument.webidl
@@ -3,17 +3,16 @@
  * 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/.
  *
  * The origin of this IDL file is:
  * dom/interfaces/xul/nsIDOMXULDocument.idl
  */
 
 interface XULCommandDispatcher;
-interface MozBoxObject;
 interface MozObserver;
 
 [Func="IsChromeOrXBL"]
 interface XULDocument : Document {
            attribute Node? popupNode;
 
   /**
    * These attributes correspond to trustedGetPopupNode().rangeOffset and
@@ -46,13 +45,13 @@ interface XULDocument : Document {
                                DOMString attr);
   void removeBroadcastListenerFor(Element broadcaster, Element observer,
                                   DOMString attr);
 
   [Throws]
   void persist([TreatNullAs=EmptyString] DOMString id, DOMString attr);
 
   [Throws]
-  MozBoxObject? getBoxObjectFor(Element? element);
+  BoxObject? getBoxObjectFor(Element? element);
 
   [Throws]
   void loadOverlay(DOMString url, MozObserver? observer);
 };
--- a/dom/webidl/XULElement.webidl
+++ b/dom/webidl/XULElement.webidl
@@ -1,15 +1,14 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/.
  */
 
-interface MozBoxObject;
 interface MozControllers;
 interface MozFrameLoader;
 interface MozRDFCompositeDataSource;
 interface MozRDFResource;
 interface MozXULTemplateBuilder;
 
 [Func="IsChromeOrXBL"]
 interface XULElement : Element {
@@ -90,17 +89,17 @@ interface XULElement : Element {
 
   readonly attribute MozRDFCompositeDataSource? database;
   readonly attribute MozXULTemplateBuilder?     builder;
   [Throws]
   readonly attribute MozRDFResource?            resource;
   [Throws]
   readonly attribute MozControllers             controllers;
   [Throws]
-  readonly attribute MozBoxObject?              boxObject;
+  readonly attribute BoxObject?                 boxObject;
 
   [Throws]
   void                      focus();
   [Throws]
   void                      blur();
   [Throws]
   void                      click();
   void                      doCommand();
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -48,16 +48,17 @@ WEBIDL_FILES = [
     'AudioTrackList.webidl',
     'AutocompleteInfo.webidl',
     'BarProp.webidl',
     'BatteryManager.webidl',
     'BeforeAfterKeyboardEvent.webidl',
     'BeforeUnloadEvent.webidl',
     'BiquadFilterNode.webidl',
     'Blob.webidl',
+    'BoxObject.webidl',
     'BrowserElementDictionaries.webidl',
     'CallsList.webidl',
     'CameraCapabilities.webidl',
     'CameraControl.webidl',
     'CameraManager.webidl',
     'CameraUtil.webidl',
     'CanvasRenderingContext2D.webidl',
     'CaretPosition.webidl',
@@ -69,16 +70,17 @@ WEBIDL_FILES = [
     'ChromeNotifications.webidl',
     'ClipboardEvent.webidl',
     'CommandEvent.webidl',
     'Comment.webidl',
     'CompositionEvent.webidl',
     'Console.webidl',
     'Constraints.webidl',
     'Contacts.webidl',
+    'ContainerBoxObject.webidl',
     'ConvolverNode.webidl',
     'Coordinates.webidl',
     'CSPReport.webidl',
     'CSS.webidl',
     'CSSPrimitiveValue.webidl',
     'CSSRuleList.webidl',
     'CSSStyleDeclaration.webidl',
     'CSSStyleSheet.webidl',
@@ -245,29 +247,31 @@ WEBIDL_FILES = [
     'InterAppConnection.webidl',
     'InterAppConnectionRequest.webidl',
     'InterAppMessagePort.webidl',
     'KeyAlgorithm.webidl',
     'KeyboardEvent.webidl',
     'KeyEvent.webidl',
     'LegacyQueryInterface.webidl',
     'LinkStyle.webidl',
+    'ListBoxObject.webidl',
     'LocalMediaStream.webidl',
     'Location.webidl',
     'MediaElementAudioSourceNode.webidl',
     'MediaError.webidl',
     'MediaList.webidl',
     'MediaQueryList.webidl',
     'MediaRecorder.webidl',
     'MediaSource.webidl',
     'MediaStream.webidl',
     'MediaStreamAudioDestinationNode.webidl',
     'MediaStreamAudioSourceNode.webidl',
     'MediaStreamTrack.webidl',
     'MediaTrackConstraintSet.webidl',
+    'MenuBoxObject.webidl',
     'MessageChannel.webidl',
     'MessageEvent.webidl',
     'MessagePort.webidl',
     'MessagePortList.webidl',
     'MimeType.webidl',
     'MimeTypeArray.webidl',
     'MouseEvent.webidl',
     'MouseScrollEvent.webidl',
@@ -315,16 +319,17 @@ WEBIDL_FILES = [
     'PerformanceResourceTiming.webidl',
     'PerformanceTiming.webidl',
     'PeriodicWave.webidl',
     'PermissionSettings.webidl',
     'PhoneNumberService.webidl',
     'Plugin.webidl',
     'PluginArray.webidl',
     'PointerEvent.webidl',
+    'PopupBoxObject.webidl',
     'Position.webidl',
     'PositionError.webidl',
     'ProcessingInstruction.webidl',
     'ProfileTimelineMarker.webidl',
     'Promise.webidl',
     'PromiseDebugging.webidl',
     'PushManager.webidl',
     'RadioNodeList.webidl',
@@ -342,16 +347,17 @@ WEBIDL_FILES = [
     'RTCPeerConnectionStatic.webidl',
     'RTCRtpReceiver.webidl',
     'RTCRtpSender.webidl',
     'RTCSessionDescription.webidl',
     'RTCStatsReport.webidl',
     'Screen.webidl',
     'ScriptProcessorNode.webidl',
     'ScrollAreaEvent.webidl',
+    'ScrollBoxObject.webidl',
     'Selection.webidl',
     'ServiceWorker.webidl',
     'ServiceWorkerContainer.webidl',
     'ServiceWorkerGlobalScope.webidl',
     'ServiceWorkerRegistration.webidl',
     'SettingChangeNotification.webidl',
     'SettingsManager.webidl',
     'ShadowRoot.webidl',
@@ -487,16 +493,17 @@ WEBIDL_FILES = [
     'TextTrackCueList.webidl',
     'TextTrackList.webidl',
     'TimeEvent.webidl',
     'TimeRanges.webidl',
     'Touch.webidl',
     'TouchEvent.webidl',
     'TouchList.webidl',
     'TransitionEvent.webidl',
+    'TreeBoxObject.webidl',
     'TreeColumns.webidl',
     'TreeWalker.webidl',
     'UDPMessageEvent.webidl',
     'UDPSocket.webidl',
     'UIEvent.webidl',
     'UndoManager.webidl',
     'URL.webidl',
     'URLSearchParams.webidl',
--- a/dom/xslt/tests/buster/buster-handlers.js
+++ b/dom/xslt/tests/buster/buster-handlers.js
@@ -3,17 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var xalan_field;
 
 function onLoad()
 {
     view.tree = document.getElementById('out');
-    view.boxObject = view.tree.boxObject.QueryInterface(Components.interfaces.nsITreeBoxObject);
+    view.boxObject = view.tree.boxObject;
     {  
         view.mIframe = document.getElementById('hiddenHtml');
         view.mIframe.webNavigation.allowPlugins = false;
         view.mIframe.webNavigation.allowJavascript = false;
         view.mIframe.webNavigation.allowMetaRedirects = false;
         view.mIframe.webNavigation.allowImages = false;
     }
     view.database = view.tree.database;
--- a/editor/composer/test/test_bug434998.xul
+++ b/editor/composer/test/test_bug434998.xul
@@ -93,17 +93,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     mEditor: null
   };
 
   var progress, progressListener;
 
   function runTest() {
     var newEditorElement = document.getElementById("editor");
     newEditorElement.makeEditable("html", true);
-    var docShell = newEditorElement.boxObject.QueryInterface(Components.interfaces.nsIEditorBoxObject).docShell;
+    var docShell = newEditorElement.boxObject.docShell;
     progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
     progressListener = new EditorContentListener(newEditorElement);
     progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
     newEditorElement.setAttribute("src", "data:text/html,");
   }
 ]]>
 </script>
 </window>
--- a/editor/libeditor/tests/test_bug607584.xul
+++ b/editor/libeditor/tests/test_bug607584.xul
@@ -99,17 +99,17 @@ https://bugzilla.mozilla.org/show_bug.cg
       mEditor: null
   };
 
   var progress, progressListener;
 
   function runTest() {
     var newEditorElement = document.getElementById("editor");
     newEditorElement.makeEditable("html", true);
-    var docShell = newEditorElement.boxObject.QueryInterface(Components.interfaces.nsIEditorBoxObject).docShell;
+    var docShell = newEditorElement.boxObject.docShell;
     progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
     progressListener = new EditorContentListener(newEditorElement);
     progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
     newEditorElement.setAttribute("src", "data:text/html,");
   }
 ]]>
 </script>
 </window>
--- a/editor/libeditor/tests/test_bug616590.xul
+++ b/editor/libeditor/tests/test_bug616590.xul
@@ -89,17 +89,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     mEditor: null
   };
 
   var progress, progressListener;
 
   function runTest() {
     var editorElement = document.getElementById("editor");
     editorElement.makeEditable("htmlmail", true);
-    var docShell = editorElement.boxObject.QueryInterface(Components.interfaces.nsIEditorBoxObject).docShell;
+    var docShell = editorElement.boxObject.docShell;
     progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
     progressListener = new EditorContentListener(editorElement);
     progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
     editorElement.setAttribute("src", "data:text/html,");
   }
 ]]>
 </script>
 </window>
--- a/editor/libeditor/tests/test_bug780908.xul
+++ b/editor/libeditor/tests/test_bug780908.xul
@@ -97,17 +97,17 @@ adapted from test_bug607584.xul by Kent 
       mEditor: null
   };
 
   var progress, progressListener;
 
   function runTest() {
     var newEditorElement = document.getElementById("editor");
     newEditorElement.makeEditable("html", true);
-    var docShell = newEditorElement.boxObject.QueryInterface(Components.interfaces.nsIEditorBoxObject).docShell;
+    var docShell = newEditorElement.boxObject.docShell;
     progress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);
     progressListener = new EditorContentListener(newEditorElement);
     progress.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
     newEditorElement.setAttribute("src", "data:text/html,");
   }
 ]]>
 </script>
 </window>
--- a/extensions/cookie/test/unit/test_permmanager_defaults.js
+++ b/extensions/cookie/test/unit/test_permmanager_defaults.js
@@ -1,15 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // The origin we use in most of the tests.
 const TEST_ORIGIN = "example.org";
 const TEST_ORIGIN_2 = "example.com";
 const TEST_PERMISSION = "test-permission";
+Components.utils.import("resource://gre/modules/Promise.jsm");
 
 function promiseTimeout(delay) {
   let deferred = Promise.defer();
   do_timeout(delay, deferred.resolve);
   return deferred.promise;
 }
 
 function run_test() {
--- a/gfx/gl/GLContext.cpp
+++ b/gfx/gl/GLContext.cpp
@@ -79,16 +79,17 @@ static const char *sExtensionNames[] = {
     "GL_ARB_copy_buffer",
     "GL_ARB_depth_texture",
     "GL_ARB_draw_buffers",
     "GL_ARB_draw_instanced",
     "GL_ARB_framebuffer_object",
     "GL_ARB_framebuffer_sRGB",
     "GL_ARB_half_float_pixel",
     "GL_ARB_instanced_arrays",
+    "GL_ARB_invalidate_subdata",
     "GL_ARB_map_buffer_range",
     "GL_ARB_occlusion_query2",
     "GL_ARB_pixel_buffer_object",
     "GL_ARB_robustness",
     "GL_ARB_sampler_objects",
     "GL_ARB_sync",
     "GL_ARB_texture_compression",
     "GL_ARB_texture_float",
@@ -1351,16 +1352,31 @@ GLContext::InitWithPrefix(const char *pr
             if (!LoadSymbols(&umnSymbols[0], trygl, prefix)) {
                 NS_ERROR("GL supports uniform matrix with non-square dim without supplying its functions.");
 
                 MarkUnsupported(GLFeature::uniform_matrix_nonsquare);
                 ClearSymbols(umnSymbols);
             }
         }
 
+        if (IsSupported(GLFeature::invalidate_framebuffer)) {
+            SymLoadStruct invSymbols[] = {
+                { (PRFuncPtr *) &mSymbols.fInvalidateFramebuffer,    { "InvalidateFramebuffer", nullptr } },
+                { (PRFuncPtr *) &mSymbols.fInvalidateSubFramebuffer, { "InvalidateSubFramebuffer", nullptr } },
+                END_SYMBOLS
+            };
+
+            if (!LoadSymbols(&invSymbols[0], trygl, prefix)) {
+                NS_ERROR("GL supports framebuffer invalidation without supplying its functions.");
+
+                MarkUnsupported(GLFeature::invalidate_framebuffer);
+                ClearSymbols(invSymbols);
+            }
+        }
+
         if (IsExtensionSupported(KHR_debug)) {
             SymLoadStruct extSymbols[] = {
                 { (PRFuncPtr*) &mSymbols.fDebugMessageControl,  { "DebugMessageControl",  "DebugMessageControlKHR",  nullptr } },
                 { (PRFuncPtr*) &mSymbols.fDebugMessageInsert,   { "DebugMessageInsert",   "DebugMessageInsertKHR",   nullptr } },
                 { (PRFuncPtr*) &mSymbols.fDebugMessageCallback, { "DebugMessageCallback", "DebugMessageCallbackKHR", nullptr } },
                 { (PRFuncPtr*) &mSymbols.fGetDebugMessageLog,   { "GetDebugMessageLog",   "GetDebugMessageLogKHR",   nullptr } },
                 { (PRFuncPtr*) &mSymbols.fGetPointerv,          { "GetPointerv",          "GetPointervKHR",          nullptr } },
                 { (PRFuncPtr*) &mSymbols.fPushDebugGroup,       { "PushDebugGroup",       "PushDebugGroupKHR",       nullptr } },
--- a/gfx/gl/GLContext.h
+++ b/gfx/gl/GLContext.h
@@ -38,16 +38,17 @@
 #include "GLContextTypes.h"
 #include "GLTextureImage.h"
 #include "SurfaceTypes.h"
 #include "GLScreenBuffer.h"
 #include "GLContextSymbols.h"
 #include "base/platform_thread.h"       // for PlatformThreadId
 #include "mozilla/GenericRefCounted.h"
 #include "gfx2DGlue.h"
+#include "GeckoProfiler.h"
 
 class nsIntRegion;
 class nsIRunnable;
 class nsIThread;
 
 namespace android {
     class GraphicBuffer;
 }
@@ -95,16 +96,17 @@ MOZ_BEGIN_ENUM_CLASS(GLFeature)
     framebuffer_multisample,
     framebuffer_object,
     get_integer_indexed,
     get_integer64_indexed,
     get_query_object_iv,
     gpu_shader4,
     instanced_arrays,
     instanced_non_arrays,
+    invalidate_framebuffer,
     map_buffer_range,
     occlusion_query,
     occlusion_query_boolean,
     occlusion_query2,
     packed_depth_stencil,
     query_objects,
     renderbuffer_color_float,
     renderbuffer_color_half_float,
@@ -362,16 +364,17 @@ public:
         ARB_copy_buffer,
         ARB_depth_texture,
         ARB_draw_buffers,
         ARB_draw_instanced,
         ARB_framebuffer_object,
         ARB_framebuffer_sRGB,
         ARB_half_float_pixel,
         ARB_instanced_arrays,
+        ARB_invalidate_subdata,
         ARB_map_buffer_range,
         ARB_occlusion_query2,
         ARB_pixel_buffer_object,
         ARB_robustness,
         ARB_sampler_objects,
         ARB_sync,
         ARB_texture_compression,
         ARB_texture_float,
@@ -759,36 +762,49 @@ private:
         GLContext *tip = this;
         while (tip->mSharedContext)
             tip = tip->mSharedContext;
         return tip;
     }
 
     static void AssertNotPassingStackBufferToTheGL(const void* ptr);
 
+#ifdef MOZ_WIDGET_ANDROID
+// Record the name of the GL call for better hang stacks on Android.
+#define BEFORE_GL_CALL                              \
+            PROFILER_LABEL_FUNC(                    \
+              js::ProfileEntry::Category::GRAPHICS);\
+            BeforeGLCall(MOZ_FUNCTION_NAME)
+#else
 #define BEFORE_GL_CALL                              \
             do {                                    \
                 BeforeGLCall(MOZ_FUNCTION_NAME);    \
             } while (0)
+#endif
 
 #define AFTER_GL_CALL                               \
             do {                                    \
                 AfterGLCall(MOZ_FUNCTION_NAME);     \
             } while (0)
 
 #define TRACKING_CONTEXT(a)                         \
             do {                                    \
                 TrackingContext()->a;               \
             } while (0)
 
 #define ASSERT_NOT_PASSING_STACK_BUFFER_TO_GL(ptr) AssertNotPassingStackBufferToTheGL(ptr)
 
 #else // ifdef DEBUG
 
+#ifdef MOZ_WIDGET_ANDROID
+// Record the name of the GL call for better hang stacks on Android.
+#define BEFORE_GL_CALL PROFILER_LABEL_FUNC(js::ProfileEntry::Category::GRAPHICS)
+#else
 #define BEFORE_GL_CALL do { } while (0)
+#endif
 #define AFTER_GL_CALL do { } while (0)
 #define TRACKING_CONTEXT(a) do {} while (0)
 #define ASSERT_NOT_PASSING_STACK_BUFFER_TO_GL(ptr) do {} while (0)
 
 #endif // ifdef DEBUG
 
 #define ASSERT_SYMBOL_PRESENT(func) \
             do {\
@@ -885,16 +901,30 @@ public:
             default:
                 // Nothing we care about, likely an error.
                 break;
         }
 
         raw_fBindFramebuffer(target, framebuffer);
     }
 
+    void fInvalidateFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments) {
+        BEFORE_GL_CALL;
+        ASSERT_SYMBOL_PRESENT(fInvalidateFramebuffer);
+        mSymbols.fInvalidateFramebuffer(target, numAttachments, attachments);
+        AFTER_GL_CALL;
+    }
+
+    void fInvalidateSubFramebuffer(GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height) {
+        BEFORE_GL_CALL;
+        ASSERT_SYMBOL_PRESENT(fInvalidateSubFramebuffer);
+        mSymbols.fInvalidateSubFramebuffer(target, numAttachments, attachments, x, y, width, height);
+        AFTER_GL_CALL;
+    }
+
     void fBindTexture(GLenum target, GLuint texture) {
         BEFORE_GL_CALL;
         mSymbols.fBindTexture(target, texture);
         AFTER_GL_CALL;
     }
 
     void fBlendColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) {
         BEFORE_GL_CALL;
@@ -3532,16 +3562,20 @@ public:
     /* Clear to transparent black, with 0 depth and stencil,
      * while preserving current ClearColor etc. values.
      * Useful for resizing offscreen buffers.
      */
     void ClearSafely();
 
     bool WorkAroundDriverBugs() const { return mWorkAroundDriverBugs; }
 
+    bool IsDrawingToDefaultFramebuffer() {
+        return Screen()->IsDrawFramebufferDefault();
+    }
+
 protected:
     nsRefPtr<TextureGarbageBin> mTexGarbageBin;
 
 public:
     TextureGarbageBin* TexGarbageBin() {
         MOZ_ASSERT(mTexGarbageBin);
         return mTexGarbageBin;
     }
--- a/gfx/gl/GLContextFeatures.cpp
+++ b/gfx/gl/GLContextFeatures.cpp
@@ -284,16 +284,25 @@ static const FeatureInfo sFeatureInfoArr
         }
         /* This is an expanded version of `instanced_arrays` that allows for all
          * enabled active attrib arrays to have non-zero divisors.
          * ANGLE_instanced_arrays and NV_instanced_arrays forbid this, but GLES3
          * has no such restriction.
          */
     },
     {
+        "invalidate_framebuffer",
+        430, // OpenGL version
+        300, // OpenGL ES version
+        GLContext::ARB_invalidate_subdata,
+        {
+            GLContext::Extensions_End
+        }
+    },
+    {
         "map_buffer_range",
         300, // OpenGL version
         300, // OpenGL ES version
         GLContext::ARB_map_buffer_range,
         {
             GLContext::Extensions_End
         }
     },
--- a/gfx/gl/GLContextSymbols.h
+++ b/gfx/gl/GLContextSymbols.h
@@ -329,16 +329,21 @@ struct GLContextSymbols
     PFNGLISFRAMEBUFFER fIsFramebuffer;
     typedef realGLboolean (GLAPIENTRY * PFNGLISRENDERBUFFER) (GLuint renderbuffer);
     PFNGLISRENDERBUFFER fIsRenderbuffer;
     typedef realGLboolean (GLAPIENTRY * PFNGLISVERTEXARRAY) (GLuint array);
     PFNGLISVERTEXARRAY fIsVertexArray;
     typedef void (GLAPIENTRY * PFNGLRENDERBUFFERSTORAGE) (GLenum target, GLenum internalFormat, GLsizei width, GLsizei height);
     PFNGLRENDERBUFFERSTORAGE fRenderbufferStorage;
 
+    typedef void (GLAPIENTRY * PFNINVALIDATEFRAMEBUFFER) (GLenum target, GLsizei numAttachments, const GLenum* attachments);
+    PFNINVALIDATEFRAMEBUFFER fInvalidateFramebuffer;
+    typedef void (GLAPIENTRY * PFNINVALIDATESUBFRAMEBUFFER) (GLenum target, GLsizei numAttachments, const GLenum* attachments, GLint x, GLint y, GLsizei width, GLsizei height);
+    PFNINVALIDATESUBFRAMEBUFFER fInvalidateSubFramebuffer;
+
         // These functions are only used by Skia/GL in desktop mode.
         // Other parts of Gecko should avoid using these
         typedef void (GLAPIENTRY * PFNGLCLIENTACTIVETEXTURE) (GLenum texture);
         PFNGLCLIENTACTIVETEXTURE fClientActiveTexture;
         typedef void (GLAPIENTRY * PFNDISABLECLIENTSTATE) (GLenum capability);
         PFNDISABLECLIENTSTATE fDisableClientState;
         typedef void (GLAPIENTRY * PFNENABLECLIENTSTATE) (GLenum capability);
         PFNENABLECLIENTSTATE fEnableClientState;
--- a/gfx/gl/GLLibraryEGL.h
+++ b/gfx/gl/GLLibraryEGL.h
@@ -7,16 +7,17 @@
 
 #if defined(MOZ_X11)
 #include "mozilla/X11Util.h"
 #endif
 
 #include "GLLibraryLoader.h"
 #include "mozilla/ThreadLocal.h"
 #include "nsIFile.h"
+#include "GeckoProfiler.h"
 
 #include <bitset>
 
 #if defined(XP_WIN)
 
 #ifndef WIN32_LEAN_AND_MEAN
 #define WIN32_LEAN_AND_MEAN 1
 #endif
@@ -70,27 +71,38 @@ namespace gl {
 #  define MOZ_FUNCTION_NAME __PRETTY_FUNCTION__
 # elif defined(_MSC_VER)
 #  define MOZ_FUNCTION_NAME __FUNCTION__
 # else
 #  define MOZ_FUNCTION_NAME __func__  // defined in C99, supported in various C++ compilers. Just raw function name.
 # endif
 #endif
 
+#ifdef MOZ_WIDGET_ANDROID
+// Record the name of the GL call for better hang stacks on Android.
+#define BEFORE_GL_CALL                      \
+    PROFILER_LABEL_FUNC(                    \
+      js::ProfileEntry::Category::GRAPHICS);\
+    BeforeGLCall(MOZ_FUNCTION_NAME)
+#else
 #define BEFORE_GL_CALL do {          \
     BeforeGLCall(MOZ_FUNCTION_NAME); \
 } while (0)
+#endif
 
 #define AFTER_GL_CALL do {           \
     AfterGLCall(MOZ_FUNCTION_NAME);  \
 } while (0)
-// We rely on the fact that GLLibraryEGL.h #defines BEFORE_GL_CALL and
-// AFTER_GL_CALL to nothing if !defined(DEBUG).
+#else
+#ifdef MOZ_WIDGET_ANDROID
+// Record the name of the GL call for better hang stacks on Android.
+#define BEFORE_GL_CALL PROFILER_LABEL_FUNC(js::ProfileEntry::Category::GRAPHICS)
 #else
 #define BEFORE_GL_CALL
+#endif
 #define AFTER_GL_CALL
 #endif
 
 class GLLibraryEGL
 {
 public:
     GLLibraryEGL() 
         : mInitialized(false),
--- a/gfx/gl/GLScreenBuffer.cpp
+++ b/gfx/gl/GLScreenBuffer.cpp
@@ -524,16 +524,30 @@ GLScreenBuffer::Readback(SharedSurface* 
   }
 
   if (needsSwap) {
       src->UnlockProd();
       SharedSurf()->LockProd();
   }
 }
 
+bool
+GLScreenBuffer::IsDrawFramebufferDefault() const
+{
+    if (!mDraw)
+        return IsReadFramebufferDefault();
+    return mDraw->mFB == 0;
+}
+
+bool
+GLScreenBuffer::IsReadFramebufferDefault() const
+{
+    return SharedSurf()->mAttachType == AttachmentType::Screen;
+}
+
 ////////////////////////////////////////////////////////////////////////
 // DrawBuffer
 
 bool
 DrawBuffer::Create(GLContext* const gl,
                    const SurfaceCaps& caps,
                    const GLFormats& formats,
                    const gfx::IntSize& size,
--- a/gfx/gl/GLScreenBuffer.h
+++ b/gfx/gl/GLScreenBuffer.h
@@ -256,14 +256,17 @@ public:
     GLuint GetDrawFB() const;
     GLuint GetReadFB() const;
 
     // Here `fb` is the actual framebuffer you want bound. Binding 0 will
     // bind the (generally useless) default framebuffer.
     void BindFB_Internal(GLuint fb);
     void BindDrawFB_Internal(GLuint fb);
     void BindReadFB_Internal(GLuint fb);
+
+    bool IsDrawFramebufferDefault() const;
+    bool IsReadFramebufferDefault() const;
 };
 
 }   // namespace gl
 }   // namespace mozilla
 
 #endif  // SCREEN_BUFFER_H_
--- a/gfx/gl/SharedSurface.cpp
+++ b/gfx/gl/SharedSurface.cpp
@@ -1,17 +1,19 @@
 /* -*- Mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40; -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "SharedSurface.h"
 
+#include "../2d/2D.h"
 #include "GLBlitHelper.h"
 #include "GLContext.h"
+#include "GLReadTexImageHelper.h"
 #include "nsThreadUtils.h"
 #include "ScopedGLHelpers.h"
 #include "SharedSurfaceGL.h"
 
 namespace mozilla {
 namespace gl {
 
 /*static*/ void
@@ -442,10 +444,111 @@ ScopedReadbackFB::~ScopedReadbackFB()
     if (mSurfToUnlock) {
         mSurfToUnlock->UnlockProd();
     }
     if (mSurfToLock) {
         mSurfToLock->LockProd();
     }
 }
 
+////////////////////////////////////////////////////////////////////////////////
+
+class AutoLockBits
+{
+    gfx::DrawTarget* mDT;
+    uint8_t* mLockedBits;
+
+public:
+    AutoLockBits(gfx::DrawTarget* dt)
+        : mDT(dt)
+        , mLockedBits(nullptr)
+    {
+        MOZ_ASSERT(mDT);
+    }
+
+    bool Lock(uint8_t** data, gfx::IntSize* size, int32_t* stride,
+              gfx::SurfaceFormat* format)
+    {
+        bool success = mDT->LockBits(data, size, stride, format);
+        if (success)
+            mLockedBits = *data;
+        return success;
+    }
+
+    ~AutoLockBits() {
+        if (mLockedBits)
+            mDT->ReleaseBits(mLockedBits);
+    }
+};
+
+bool
+ReadbackSharedSurface(SharedSurface* src, gfx::DrawTarget* dst)
+{
+    AutoLockBits lock(dst);
+
+    uint8_t* dstBytes;
+    gfx::IntSize dstSize;
+    int32_t dstStride;
+    gfx::SurfaceFormat dstFormat;
+    if (!dst->LockBits(&dstBytes, &dstSize, &dstStride, &dstFormat))
+        return false;
+
+    const bool isDstRGBA = (dstFormat == gfx::SurfaceFormat::R8G8B8A8 ||
+                            dstFormat == gfx::SurfaceFormat::R8G8B8X8);
+    MOZ_ASSERT_IF(!isDstRGBA, dstFormat == gfx::SurfaceFormat::B8G8R8A8 ||
+                              dstFormat == gfx::SurfaceFormat::B8G8R8X8);
+
+    size_t width = src->mSize.width;
+    size_t height = src->mSize.height;
+    MOZ_ASSERT(width == (size_t)dstSize.width);
+    MOZ_ASSERT(height == (size_t)dstSize.height);
+
+    GLenum readGLFormat;
+    GLenum readType;
+
+    {
+        ScopedReadbackFB autoReadback(src);
+
+
+        // We have a source FB, now we need a format.
+        GLenum dstGLFormat = isDstRGBA ? LOCAL_GL_BGRA : LOCAL_GL_RGBA;
+        GLenum dstType = LOCAL_GL_UNSIGNED_BYTE;
+
+        // We actually don't care if they match, since we can handle
+        // any read{Format,Type} we get.
+        GLContext* gl = src->mGL;
+        GetActualReadFormats(gl, dstGLFormat, dstType, &readGLFormat,
+                             &readType);
+
+        MOZ_ASSERT(readGLFormat == LOCAL_GL_RGBA ||
+                   readGLFormat == LOCAL_GL_BGRA);
+        MOZ_ASSERT(readType == LOCAL_GL_UNSIGNED_BYTE);
+
+        // ReadPixels from the current FB into lockedBits.
+        {
+            size_t alignment = 8;
+            if (dstStride % 4 == 0)
+                alignment = 4;
+            ScopedPackAlignment autoAlign(gl, alignment);
+
+            gl->raw_fReadPixels(0, 0, width, height, readGLFormat, readType,
+                                dstBytes);
+        }
+    }
+
+    const bool isReadRGBA = readGLFormat == LOCAL_GL_RGBA;
+
+    if (isReadRGBA != isDstRGBA) {
+        for (size_t j = 0; j < height; ++j) {
+            uint8_t* rowItr = dstBytes + j*dstStride;
+            uint8_t* rowEnd = rowItr + 4*width;
+            while (rowItr != rowEnd) {
+                Swap(rowItr[0], rowItr[2]);
+                rowItr += 4;
+            }
+        }
+    }
+
+    return true;
+}
+
 } /* namespace gfx */
 } /* namespace mozilla */
--- a/gfx/gl/SharedSurface.h
+++ b/gfx/gl/SharedSurface.h
@@ -26,16 +26,19 @@
 #include "mozilla/UniquePtr.h"
 #include "mozilla/WeakPtr.h"
 #include "ScopedGLHelpers.h"
 #include "SurfaceTypes.h"
 
 class nsIThread;
 
 namespace mozilla {
+namespace gfx {
+class DrawTarget;
+}
 namespace gl {
 
 class GLContext;
 class SurfaceFactory;
 class ShSurfHandle;
 
 class SharedSurface
 {
@@ -248,12 +251,14 @@ class ScopedReadbackFB
     SharedSurface* mSurfToUnlock;
     SharedSurface* mSurfToLock;
 
 public:
     ScopedReadbackFB(SharedSurface* src);
     ~ScopedReadbackFB();
 };
 
+bool ReadbackSharedSurface(SharedSurface* src, gfx::DrawTarget* dst);
+
 } // namespace gl
 } // namespace mozilla
 
 #endif // SHARED_SURFACE_H_
--- a/gfx/gl/SharedSurfaceGL.cpp
+++ b/gfx/gl/SharedSurfaceGL.cpp
@@ -78,85 +78,29 @@ SharedSurface_Basic::SharedSurface_Basic
     mGL->fFramebufferTexture2D(LOCAL_GL_FRAMEBUFFER,
                               LOCAL_GL_COLOR_ATTACHMENT0,
                               LOCAL_GL_TEXTURE_2D,
                               mTex,
                               0);
 
     DebugOnly<GLenum> status = mGL->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER);
     MOZ_ASSERT(status == LOCAL_GL_FRAMEBUFFER_COMPLETE);
-
-    int32_t stride = gfx::GetAlignedStride<4>(size.width * BytesPerPixel(format));
-    mData = gfx::Factory::CreateDataSourceSurfaceWithStride(size, format, stride);
-    // Leave the extra return for clarity, in case we decide more code should
-    // be added after this check, that should run even if mData is null.
-    if (NS_WARN_IF(!mData)) {
-        return;
-    }
 }
 
 SharedSurface_Basic::~SharedSurface_Basic()
 {
     if (!mGL->MakeCurrent())
         return;
 
     if (mFB)
         mGL->fDeleteFramebuffers(1, &mFB);
 
     mGL->fDeleteTextures(1, &mTex);
 }
 
-void
-SharedSurface_Basic::Fence()
-{
-    // The constructor can fail to get us mData, we should deal with it:
-    if (NS_WARN_IF(!mData)) {
-        return;
-    }
-
-    mGL->MakeCurrent();
-    ScopedBindFramebuffer autoFB(mGL, mFB);
-    ReadPixelsIntoDataSurface(mGL, mData);
-}
-
-bool
-SharedSurface_Basic::WaitSync()
-{
-    return true;
-}
-
-bool
-SharedSurface_Basic::PollSync()
-{
-    return true;
-}
-
-void
-SharedSurface_Basic::Fence_ContentThread_Impl()
-{
-}
-
-bool
-SharedSurface_Basic::WaitSync_ContentThread_Impl()
-{
-    mGL->MakeCurrent();
-    ScopedBindFramebuffer autoFB(mGL, mFB);
-    ReadPixelsIntoDataSurface(mGL, mData);
-    return true;
-}
-
-bool