Bug 1161802 part 7 - Put document into fullscreen state after window becomes fullscreen. r=smaug,dao
authorXidorn Quan <quanxunzhen@gmail.com>
Wed, 10 Jun 2015 23:13:12 +1200
changeset 248043 10d47ad00e7a97160d74c5682605c7c82de606d0
parent 248042 3c9900eeadaff920a3ef8c0122a20e5edd5beb4f
child 248044 7f0b45781f24778f7152d72a1f284e0f3e089809
push id28888
push userkwierso@gmail.com
push dateThu, 11 Jun 2015 01:29:45 +0000
treeherdermozilla-central@04c057942da4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, dao
bugs1161802
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1161802 part 7 - Put document into fullscreen state after window becomes fullscreen. r=smaug,dao
browser/base/content/browser-fullScreen.js
browser/base/content/tab-content.js
dom/base/nsDOMWindowUtils.cpp
dom/base/nsDocument.cpp
dom/base/nsGlobalWindow.cpp
dom/base/nsIDocument.h
dom/browser-element/BrowserElementChildPreload.js
dom/browser-element/BrowserElementParent.js
dom/interfaces/base/nsIDOMWindowUtils.idl
--- a/browser/base/content/browser-fullScreen.js
+++ b/browser/base/content/browser-fullScreen.js
@@ -2,24 +2,27 @@
 # 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/.
 
 var FullScreen = {
   _XULNS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
 
   _MESSAGES: [
-    "DOMFullscreen:Entered",
+    "DOMFullscreen:Request",
     "DOMFullscreen:NewOrigin",
-    "DOMFullscreen:Exited"
+    "DOMFullscreen:Exited",
   ],
 
   init: function() {
     // called when we go into full screen, even if initiated by a web page script
     window.addEventListener("fullscreen", this, true);
+    window.addEventListener("MozDOMFullscreen:Entered", this,
+                            /* useCapture */ true,
+                            /* wantsUntrusted */ false);
     window.addEventListener("MozDOMFullscreen:Exited", this,
                             /* useCapture */ true,
                             /* wantsUntrusted */ false);
     for (let type of this._MESSAGES) {
       window.messageManager.addMessageListener(type, this);
     }
 
     if (window.fullScreen)
@@ -103,35 +106,56 @@ var FullScreen = {
         break;
       case "fullscreen":
         this.toggle();
         break;
       case "transitionend":
         if (event.propertyName == "opacity")
           this.cancelWarning();
         break;
+      case "MozDOMFullscreen:Entered": {
+        // The original target is the element which requested the DOM
+        // fullscreen. If we were entering DOM fullscreen for a remote
+        // browser, this element would be that browser element, which
+        // was the parameter of `remoteFrameFullscreenChanged` call.
+        // If the fullscreen request was initiated from an in-process
+        // browser, we need to get its corresponding browser element.
+        let originalTarget = event.originalTarget;
+        let browser;
+        if (this._isBrowser(originalTarget)) {
+          browser = originalTarget;
+        } else {
+          let topWin = originalTarget.ownerDocument.defaultView.top;
+          browser = gBrowser.getBrowserForContentWindow(topWin);
+          if (!browser) {
+            document.mozCancelFullScreen();
+            break;
+          }
+        }
+        this.enterDomFullscreen(browser);
+        // If it is a remote browser, send a message to ask the content
+        // to enter fullscreen state. We don't need to do so if it is an
+        // in-process browser, since all related document should have
+        // entered fullscreen state at this point.
+        if (this._isRemoteBrowser(browser)) {
+          browser.messageManager.sendAsyncMessage("DOMFullscreen:Entered");
+        }
+        break;
+      }
       case "MozDOMFullscreen:Exited":
         this.cleanupDomFullscreen();
         break;
     }
   },
 
   receiveMessage: function(aMessage) {
     let browser = aMessage.target;
     switch (aMessage.name) {
-      case "DOMFullscreen:Entered": {
-        // If we're a multiprocess browser, then the request to enter
-        // fullscreen did not bubble up to the root browser document -
-        // it stopped at the root of the content document. That means
-        // we have to kick off the switch to fullscreen here at the
-        // operating system level in the parent process ourselves.
-        if (this._isRemoteBrowser(browser)) {
-          this._windowUtils.remoteFrameFullscreenChanged(browser);
-        }
-        this.enterDomFullscreen(browser);
+      case "DOMFullscreen:Request": {
+        this._windowUtils.remoteFrameFullscreenChanged(browser);
         break;
       }
       case "DOMFullscreen:NewOrigin": {
         this.showWarning(aMessage.data.originNoSuffix);
         break;
       }
       case "DOMFullscreen:Exited": {
         // Like entering DOM fullscreen, we also need to exit fullscreen
@@ -211,16 +235,23 @@ var FullScreen = {
       this._shouldAnimate = true;
       this.hideNavToolbox();
     }
 
     window.messageManager
           .broadcastAsyncMessage("DOMFullscreen:CleanUp");
   },
 
+  _isBrowser: function (aNode) {
+    if (aNode.tagName != "xul:browser") {
+      return false;
+    }
+    let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+    return aNode.nodePrincipal == systemPrincipal;
+  },
   _isRemoteBrowser: function (aBrowser) {
     return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true";
   },
 
   get _windowUtils() {
     return window.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIDOMWindowUtils);
   },
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -584,47 +584,61 @@ if (Services.appinfo.processType == Serv
   tabchild.webBrowserChrome = WebBrowserChrome;
 }
 
 
 let DOMFullscreenHandler = {
   _fullscreenDoc: null,
 
   init: function() {
+    addMessageListener("DOMFullscreen:Entered", this);
     addMessageListener("DOMFullscreen:Approved", this);
     addMessageListener("DOMFullscreen:CleanUp", this);
-    addEventListener("MozDOMFullscreen:Entered", this);
+    addEventListener("MozDOMFullscreen:Request", this);
     addEventListener("MozDOMFullscreen:NewOrigin", this);
     addEventListener("MozDOMFullscreen:Exited", this);
   },
 
+  get _windowUtils() {
+    return content.QueryInterface(Ci.nsIInterfaceRequestor)
+                  .getInterface(Ci.nsIDOMWindowUtils);
+  },
+
   receiveMessage: function(aMessage) {
     switch(aMessage.name) {
+      case "DOMFullscreen:Entered": {
+        if (!this._windowUtils.handleFullscreenRequests() &&
+            !content.document.mozFullScreen) {
+          // If we don't actually have any pending fullscreen request
+          // to handle, neither we have been in fullscreen, tell the
+          // parent to just exit.
+          sendAsyncMessage("DOMFullscreen:Exited");
+        }
+        break;
+      }
       case "DOMFullscreen:Approved": {
         if (this._fullscreenDoc) {
           Services.obs.notifyObservers(this._fullscreenDoc,
                                        "fullscreen-approved",
                                        "");
         }
         break;
       }
       case "DOMFullscreen:CleanUp": {
-        let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDOMWindowUtils);
-        utils.exitFullscreen();
+        this._windowUtils.exitFullscreen();
         this._fullscreenDoc = null;
         break;
       }
     }
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
-      case "MozDOMFullscreen:Entered": {
-        sendAsyncMessage("DOMFullscreen:Entered");
+      case "MozDOMFullscreen:Request": {
+        sendAsyncMessage("DOMFullscreen:Request");
         break;
       }
       case "MozDOMFullscreen:NewOrigin": {
         this._fullscreenDoc = aEvent.target;
         sendAsyncMessage("DOMFullscreen:NewOrigin", {
           originNoSuffix: this._fullscreenDoc.nodePrincipal.originNoSuffix,
         });
         break;
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -3122,16 +3122,28 @@ nsDOMWindowUtils::RemoteFrameFullscreenR
 
   nsCOMPtr<nsIDocument> doc = GetDocument();
   NS_ENSURE_STATE(doc);
 
   doc->RemoteFrameFullscreenReverted();
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDOMWindowUtils::HandleFullscreenRequests(bool* aRetVal)
+{
+  MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome());
+
+  nsCOMPtr<nsIDocument> doc = GetDocument();
+  NS_ENSURE_STATE(doc);
+
+  *aRetVal = nsIDocument::HandlePendingFullscreenRequests(doc);
+  return NS_OK;
+}
+
 nsresult
 nsDOMWindowUtils::ExitFullscreen()
 {
   MOZ_RELEASE_ASSERT(nsContentUtils::IsCallerChrome());
 
   nsIDocument::ExitFullscreen(nullptr, /* async */ false);
   return NS_OK;
 }
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -11,16 +11,18 @@
 #include "nsDocument.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/BinarySearch.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/Likely.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/UniquePtr.h"
 #include <algorithm>
 
 #include "mozilla/Logging.h"
 #include "plstr.h"
 #include "prprf.h"
 
 #include "mozilla/Telemetry.h"
 #include "nsIInterfaceRequestor.h"
@@ -11628,54 +11630,142 @@ nsDocument::FullscreenElementReadyCheck(
     if (nsContentUtils::HasPluginWithUncontrolledEventDispatch(content)) {
       LogFullScreenDenied(true, "FullScreenDeniedFocusedPlugin", this);
       return false;
     }
   }
   return true;
 }
 
+struct FullscreenRequest : public LinkedListElement<FullscreenRequest>
+{
+  FullscreenRequest(Element* aElement, const FullScreenOptions& aOptions)
+    : mElement(aElement), mOptions(aOptions)
+    { MOZ_COUNT_CTOR(FullscreenRequest); }
+  ~FullscreenRequest() { MOZ_COUNT_DTOR(FullscreenRequest); }
+
+  nsRefPtr<Element> mElement;
+  FullScreenOptions mOptions;
+};
+
+// Any fullscreen request waiting for the widget to finish being full-
+// screen is queued here. This is declared static instead of a member
+// of nsDocument because in the majority of time, there would be at most
+// one document requesting fullscreen. We shouldn't waste the space to
+// hold for it in every document.
+static LinkedList<FullscreenRequest> sPendingFullscreenRequests;
+
+static nsCOMPtr<nsPIDOMWindow>
+GetRootWindow(nsIDocument* aDoc)
+{
+  nsIDocShell* docShell = aDoc->GetDocShell();
+  if (!docShell) {
+    return nullptr;
+  }
+  nsCOMPtr<nsIDocShellTreeItem> rootItem;
+  docShell->GetRootTreeItem(getter_AddRefs(rootItem));
+  return rootItem ? rootItem->GetWindow() : nullptr;
+}
+
 void
 nsDocument::RequestFullScreen(Element* aElement,
                               const FullScreenOptions& aOptions)
 {
+  nsCOMPtr<nsPIDOMWindow> rootWin = GetRootWindow(this);
+  if (!rootWin) {
+    return;
+  }
+
+  // If we have been in fullscreen, apply the new state directly.
+  // Note that we should check both condition, because if we are in
+  // child process, our window may not report to be in fullscreen.
+  if (static_cast<nsGlobalWindow*>(rootWin.get())->FullScreen() ||
+      nsContentUtils::GetRootDocument(this)->IsFullScreenDoc()) {
+    ApplyFullscreen(aElement, aOptions);
+    return;
+  }
+
+  // We don't need to check element ready before this point, because
+  // if we called ApplyFullscreen, it would check that for us.
   if (!FullscreenElementReadyCheck(aElement, aOptions.mIsCallerChrome)) {
     return;
   }
 
-  ApplyFullscreen(aElement, aOptions);
-
-#ifdef DEBUG
-  // Note assertions must run before SetWindowFullScreen() as that does
-  // synchronous event dispatch which can run script which exits full-screen!
-  NS_ASSERTION(GetFullScreenElement() == aElement,
-               "Full-screen element should be the requested element!");
-  NS_ASSERTION(IsFullScreenDoc(), "Should be full-screen doc");
-  nsCOMPtr<nsIDOMElement> fse;
-  GetMozFullScreenElement(getter_AddRefs(fse));
-  nsCOMPtr<nsIContent> c(do_QueryInterface(fse));
-  NS_ASSERTION(c->AsElement() == aElement,
-    "GetMozFullScreenElement should match GetFullScreenElement()");
-#endif
-
-  // Make the window full-screen. Note we must make the state changes above
-  // before making the window full-screen, as then the document reports as
-  // being in full-screen mode when the chrome "fullscreen" event fires,
-  // enabling chrome to distinguish between browser and dom full-screen
-  // modes. Also note that nsGlobalWindow::SetFullScreen() (which
-  // SetWindowFullScreen() calls) proxies to the root window in its hierarchy,
-  // and does not operate on the a per-nsIDOMWindow basis.
-  FullscreenRoots::Add(this);
-  SetWindowFullScreen(this, true, aOptions.mVRHMDDevice);
+  sPendingFullscreenRequests.insertBack(
+    new FullscreenRequest(aElement, aOptions));
+  if (XRE_GetProcessType() == GeckoProcessType_Content) {
+    // If we are not the top level process, dispatch an event to make
+    // our parent process go fullscreen first.
+    (new AsyncEventDispatcher(
+       this, NS_LITERAL_STRING("MozDOMFullscreen:Request"),
+       /* Bubbles */ true, /* ChromeOnly */ true))->PostDOMEvent();
+  } else {
+    // Make the window fullscreen.
+    SetWindowFullScreen(this, true, aOptions.mVRHMDDevice);
+  }
+}
+
+/* static */ bool
+nsIDocument::HandlePendingFullscreenRequest(const FullscreenRequest& aRequest,
+                                            nsIDocShellTreeItem* aRootShell,
+                                            bool* aHandled)
+{
+  nsRefPtr<nsDocument> doc =
+    static_cast<nsDocument*>(aRequest.mElement->OwnerDoc());
+  nsIDocShellTreeItem* shell = doc->GetDocShell();
+  if (!shell) {
+    return true;
+  }
+  nsCOMPtr<nsIDocShellTreeItem> rootShell;
+  shell->GetRootTreeItem(getter_AddRefs(rootShell));
+  if (rootShell != aRootShell) {
+    return false;
+  }
+
+  doc->ApplyFullscreen(aRequest.mElement, aRequest.mOptions);
+  *aHandled = true;
+  return true;
+}
+
+/* static */ bool
+nsIDocument::HandlePendingFullscreenRequests(nsIDocument* aDoc)
+{
+  if (sPendingFullscreenRequests.isEmpty()) {
+    return false;
+  }
+
+  bool handled = false;
+  nsIDocShellTreeItem* shell = aDoc->GetDocShell();
+  nsCOMPtr<nsIDocShellTreeItem> rootShell;
+  if (shell) {
+    shell->GetRootTreeItem(getter_AddRefs(rootShell));
+  }
+  FullscreenRequest* request = sPendingFullscreenRequests.getFirst();
+  while (request) {
+    if (HandlePendingFullscreenRequest(*request, rootShell, &handled)) {
+      // Drop requests, which either have been detached from document/
+      // document shell, or are handled by HandleFullscreenRequest.
+      FullscreenRequest* thisRequest = request;
+      request = request->getNext();
+      delete thisRequest;
+    } else {
+      request = request->getNext();
+    }
+  }
+  return handled;
 }
 
 void
 nsDocument::ApplyFullscreen(Element* aElement,
                             const FullScreenOptions& aOptions)
 {
+  if (!FullscreenElementReadyCheck(aElement, aOptions.mIsCallerChrome)) {
+    return;
+  }
+
   // Stash a reference to any existing fullscreen doc, we'll use this later
   // to detect if the origin which is fullscreen has changed.
   nsCOMPtr<nsIDocument> previousFullscreenDoc = GetFullscreenLeaf(this);
 
   AddFullscreenApprovedObserver();
 
   // Stores a list of documents which we must dispatch "mozfullscreenchange"
   // too. We're required by the spec to dispatch the events in root-to-leaf
@@ -11756,17 +11846,17 @@ nsDocument::ApplyFullscreen(Element* aEl
   // If it is the first entry of the fullscreen, trigger an event so
   // that the UI can response to this change, e.g. hide chrome, or
   // notifying parent process to enter fullscreen. Note that chrome
   // code may also want to listen to MozDOMFullscreen:NewOrigin event
   // to pop up warning/approval UI.
   if (!previousFullscreenDoc) {
     nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
       new AsyncEventDispatcher(
-        this, NS_LITERAL_STRING("MozDOMFullscreen:Entered"),
+        aElement, NS_LITERAL_STRING("MozDOMFullscreen:Entered"),
         /* Bubbles */ true, /* ChromeOnly */ true);
     asyncDispatcher->PostDOMEvent();
   }
 
   // The origin which is fullscreen gets changed. Trigger an event so
   // that the chrome knows to pop up a warning/approval UI. Note that
   // previousFullscreenDoc == nullptr upon first entry, so we always
   // take this path on the first entry. Also note that, in a multi-
@@ -11776,16 +11866,18 @@ nsDocument::ApplyFullscreen(Element* aEl
   if (aOptions.mShouldNotifyNewOrigin &&
       !nsContentUtils::HaveEqualPrincipals(previousFullscreenDoc, this)) {
     nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
       new AsyncEventDispatcher(
         this, NS_LITERAL_STRING("MozDOMFullscreen:NewOrigin"),
         /* Bubbles */ true, /* ChromeOnly */ true);
     asyncDispatcher->PostDOMEvent();
   }
+
+  FullscreenRoots::Add(this);
 }
 
 NS_IMETHODIMP
 nsDocument::GetMozFullScreenElement(nsIDOMElement **aFullScreenElement)
 {
   ErrorResult rv;
   Element* el = GetMozFullScreenElement(rv);
   if (rv.Failed()) {
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -6174,16 +6174,24 @@ nsGlobalWindow::FinishFullscreenChange(b
       mFullScreen = true;
       // We don't know how code can reach here. Not sure
       // what value should be set for fullscreen mode.
       mFullscreenMode = false;
     }
     return;
   }
 
+  // Ask the document to handle any pending DOM fullscreen change. Note
+  // we must make the state changes before dispatching the "fullscreen"
+  // event below, so that the chrome can distinguish between browser
+  // fullscreen mode and DOM fullscreen.
+  if (mFullScreen) {
+    nsIDocument::HandlePendingFullscreenRequests(mDoc);
+  }
+
   // dispatch a "fullscreen" DOM event so that XUL apps can
   // respond visually if we are kicked into full screen mode
   DispatchCustomEvent(NS_LITERAL_STRING("fullscreen"));
 
   if (!mFullScreen) {
     // Force exit from DOM full-screen mode. This is so that if we're in
     // DOM full-screen mode and the user exits full-screen mode with
     // the browser full-screen mode toggle keyboard-shortcut, we'll detect
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -43,16 +43,17 @@ class nsHTMLCSSStyleSheet;
 class nsHTMLDocument;
 class nsHTMLStyleSheet;
 class nsIAtom;
 class nsIBFCacheEntry;
 class nsIChannel;
 class nsIContent;
 class nsIContentSink;
 class nsIDocShell;
+class nsIDocShellTreeItem;
 class nsIDocumentEncoder;
 class nsIDocumentObserver;
 class nsIDOMDocument;
 class nsIDOMDocumentType;
 class nsIDOMElement;
 class nsIDOMNodeFilter;
 class nsIDOMNodeList;
 class nsIHTMLCollection;
@@ -78,16 +79,17 @@ class nsScriptLoader;
 class nsSMILAnimationController;
 class nsStyleSet;
 class nsTextNode;
 class nsWindowSizes;
 class nsDOMCaretPosition;
 class nsViewportInfo;
 class nsIGlobalObject;
 struct nsCSSSelectorList;
+struct FullscreenRequest; // For nsIDocument::HandlePendingFullscreenRequest
 
 namespace mozilla {
 class CSSStyleSheet;
 class ErrorResult;
 class EventStates;
 class PendingAnimationTracker;
 class SVGAttrAnimationRuleProcessor;
 
@@ -1175,16 +1177,32 @@ public:
    *
    * If aRunAsync is true, fullscreen is executed asynchronously.
    *
    * Note if aDocument is not fullscreen this function has no effect, even if
    * aDocument has fullscreen ancestors.
    */
   static void ExitFullscreen(nsIDocument* aDocument, bool aRunAsync);
 
+  /**
+   * Handles one single fullscreen request, updates `aHandled` if the request
+   * is handled, and returns whether this request should be removed from the
+   * request queue.
+   */
+  static bool HandlePendingFullscreenRequest(const FullscreenRequest& aRequest,
+                                             nsIDocShellTreeItem* aRootShell,
+                                             bool* aHandled);
+
+  /**
+   * Handles any pending fullscreen in aDocument or its subdocuments.
+   *
+   * Returns whether there is any fullscreen request handled.
+   */
+  static bool HandlePendingFullscreenRequests(nsIDocument* aDocument);
+
   virtual void RequestPointerLock(Element* aElement) = 0;
 
   static void UnlockPointer(nsIDocument* aDoc = nullptr);
 
 
   //----------------------------------------------------------------------
 
   // Document notification API's
--- a/dom/browser-element/BrowserElementChildPreload.js
+++ b/dom/browser-element/BrowserElementChildPreload.js
@@ -139,18 +139,18 @@ BrowserElementChild.prototype = {
                      /* useCapture = */ true,
                      /* wantsUntrusted = */ false);
 
     addEventListener('MozScrolledAreaChanged',
                      this._mozScrollAreaChanged.bind(this),
                      /* useCapture = */ true,
                      /* wantsUntrusted = */ false);
 
-    addEventListener("MozDOMFullscreen:Entered",
-                     this._mozEnteredDomFullscreen.bind(this),
+    addEventListener("MozDOMFullscreen:Request",
+                     this._mozRequestedDOMFullscreen.bind(this),
                      /* useCapture = */ true,
                      /* wantsUntrusted = */ false);
 
     addEventListener("MozDOMFullscreen:NewOrigin",
                      this._mozFullscreenOriginChange.bind(this),
                      /* useCapture = */ true,
                      /* wantsUntrusted = */ false);
 
@@ -219,16 +219,17 @@ BrowserElementChild.prototype = {
       "go-back": this._recvGoBack,
       "go-forward": this._recvGoForward,
       "reload": this._recvReload,
       "stop": this._recvStop,
       "zoom": this._recvZoom,
       "unblock-modal-prompt": this._recvStopWaiting,
       "fire-ctx-callback": this._recvFireCtxCallback,
       "owner-visibility-change": this._recvOwnerVisibilityChange,
+      "entered-fullscreen": this._recvEnteredFullscreen,
       "exit-fullscreen": this._recvExitFullscreen.bind(this),
       "activate-next-paint-listener": this._activateNextPaintListener.bind(this),
       "set-input-method-active": this._recvSetInputMethodActive.bind(this),
       "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this),
       "do-command": this._recvDoCommand
     }
 
     addMessageListener("browser-element-api:call", function(aMessage) {
@@ -286,16 +287,22 @@ BrowserElementChild.prototype = {
    */
   _unloadHandler: function() {
     this._shuttingDown = true;
     OBSERVED_EVENTS.forEach((aTopic) => {
       Services.obs.removeObserver(this, aTopic);
     });
   },
 
+  get _windowUtils() {
+    return content.document.defaultView
+                  .QueryInterface(Ci.nsIInterfaceRequestor)
+                  .getInterface(Ci.nsIDOMWindowUtils);
+  },
+
   _tryGetInnerWindowID: function(win) {
     let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils);
     try {
       return utils.currentInnerWindowID;
     }
     catch(e) {
       return null;
@@ -429,21 +436,28 @@ BrowserElementChild.prototype = {
       return;
     }
 
     debug("recvStopWaiting " + win);
     win.modalReturnValue = returnValue;
     win.modalDepth--;
   },
 
+  _recvEnteredFullscreen: function() {
+    if (!this._windowUtils.handleFullscreenRequests() &&
+        !content.document.mozFullScreen) {
+      // If we don't actually have any pending fullscreen request
+      // to handle, neither we have been in fullscreen, tell the
+      // parent to just exit.
+      sendAsyncMsg("exited-dom-fullscreen");
+    }
+  },
+
   _recvExitFullscreen: function() {
-    var utils = content.document.defaultView
-                       .QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDOMWindowUtils);
-    utils.exitFullscreen();
+    this._windowUtils.exitFullscreen();
   },
 
   _titleChangedHandler: function(e) {
     debug("Got titlechanged: (" + e.target.title + ")");
     var win = e.target.defaultView;
 
     // Ignore titlechanges which don't come from the top-level
     // <iframe mozbrowser> window.
@@ -965,18 +979,18 @@ BrowserElementChild.prototype = {
   _mozScrollAreaChanged: function(e) {
     let dimensions = this._getContentDimensions();
     sendAsyncMsg('scrollareachanged', {
       width: dimensions.width,
       height: dimensions.height
     });
   },
 
-  _mozEnteredDomFullscreen: function(e) {
-    sendAsyncMsg("entered-dom-fullscreen");
+  _mozRequestedDOMFullscreen: function(e) {
+    sendAsyncMsg("requested-dom-fullscreen");
   },
 
   _mozFullscreenOriginChange: function(e) {
     sendAsyncMsg("fullscreen-origin-change", {
       originNoSuffix: e.target.nodePrincipal.originNoSuffix
     });
   },
 
--- a/dom/browser-element/BrowserElementParent.js
+++ b/dom/browser-element/BrowserElementParent.js
@@ -79,16 +79,17 @@ function defineDOMRequestMethod(msgName)
 function BrowserElementParent() {
   debug("Creating new BrowserElementParent object");
   this._domRequestCounter = 0;
   this._domRequestReady = false;
   this._pendingAPICalls = [];
   this._pendingDOMRequests = {};
   this._pendingSetInputMethodActive = [];
   this._nextPaintListeners = [];
+  this._pendingDOMFullscreen = false;
 
   Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true);
   Services.obs.addObserver(this, 'copypaste-docommand', /* ownsWeak = */ true);
   Services.obs.addObserver(this, 'ask-children-to-execute-copypaste-command', /* ownsWeak = */ true);
 }
 
 BrowserElementParent.prototype = {
 
@@ -191,17 +192,17 @@ BrowserElementParent.prototype = {
       "firstpaint": this._fireProfiledEventFromMsg,
       "documentfirstpaint": this._fireProfiledEventFromMsg,
       "nextpaint": this._recvNextPaint,
       "got-purge-history": this._gotDOMRequestResult,
       "got-screenshot": this._gotDOMRequestResult,
       "got-contentdimensions": this._gotDOMRequestResult,
       "got-can-go-back": this._gotDOMRequestResult,
       "got-can-go-forward": this._gotDOMRequestResult,
-      "entered-dom-fullscreen": this._enteredDomFullscreen,
+      "requested-dom-fullscreen": this._requestedDOMFullscreen,
       "fullscreen-origin-change": this._fullscreenOriginChange,
       "exited-dom-fullscreen": this._exitedDomFullscreen,
       "got-visible": this._gotDOMRequestResult,
       "visibilitychange": this._childVisibilityChange,
       "got-set-input-method-active": this._gotDOMRequestResult,
       "selectionstatechanged": this._handleSelectionStateChanged,
       "scrollviewchange": this._handleScrollViewChange,
       "caretstatechanged": this._handleCaretStateChanged,
@@ -965,33 +966,37 @@ BrowserElementParent.prototype = {
    */
   _childVisibilityChange: function(data) {
     debug("_childVisibilityChange(" + data.json.visible + ")");
     this._frameLoader.visible = data.json.visible;
 
     this._fireEventFromMsg(data);
   },
 
-  _enteredDomFullscreen: function() {
+  _requestedDOMFullscreen: function() {
+    this._pendingDOMFullscreen = true;
     this._windowUtils.remoteFrameFullscreenChanged(this._frameElement);
   },
 
   _fullscreenOriginChange: function(data) {
     Services.obs.notifyObservers(
       this._frameElement, "fullscreen-origin-change", data.json.originNoSuffix);
   },
 
   _exitedDomFullscreen: function(data) {
     this._windowUtils.remoteFrameFullscreenReverted();
   },
 
   _fullscreenChange: function(evt) {
     if (this._isAlive() && evt.target == this._window.document) {
       if (!this._window.document.mozFullScreen) {
         this._sendAsyncMsg("exit-fullscreen");
+      } else if (this._pendingDOMFullscreen) {
+        this._pendingDOMFullscreen = false;
+        this._sendAsyncMsg("entered-fullscreen");
       }
     }
   },
 
   _fireFatalError: function() {
     let evt = this._createEvent('error', {type: 'fatal'},
                                 /* cancelable = */ false);
     this._frameElement.dispatchEvent(evt);
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -44,17 +44,17 @@ interface nsIDOMClientRect;
 interface nsIURI;
 interface nsIDOMEventTarget;
 interface nsIRunnable;
 interface nsITranslationNodeList;
 interface nsIJSRAIIHelper;
 interface nsIContentPermissionRequest;
 interface nsIObserver;
 
-[scriptable, uuid(ec176f3b-2886-4090-938e-dded103c5f1c)]
+[scriptable, uuid(a01d5bb4-3e81-4529-b139-2f4ebb78f717)]
 interface nsIDOMWindowUtils : nsISupports {
 
   /**
    * Image animation mode of the window. When this attribute's value
    * is changed, the implementation should set all images in the window
    * to the given value. That is, when set to kDontAnimMode, all images
    * will stop animating. The attribute's value must be one of the
    * animationMode values from imgIContainer.
@@ -1189,16 +1189,25 @@ interface nsIDOMWindowUtils : nsISupport
 
   /**
    * Called when the remote frame has popped all fullscreen elements off its
    * stack, so that the operation can complete on the parent side.
    */
   void remoteFrameFullscreenReverted();
 
   /**
+   * Calls the document to handle any pending fullscreen requests.
+   * It is called when the parent document has entered fullscreen, and
+   * we want to put the current document into fullscreen as well.
+   * The return value indicates whether there is any fullscreen request
+   * handled by this call.
+   */
+  boolean handleFullscreenRequests();
+
+  /**
    * Called when the child frame has fully exit fullscreen, so that the parent
    * process can also fully exit.
    */
   void exitFullscreen();
 
   /**
    * If sendQueryContentEvent()'s aAdditionalFlags argument is
    * SELECTION_SET_FLAG_USE_NATIVE_LINE_BREAK, aOffset and aLength are offset