Bug 1441810 - Add session save/restore code to GeckoViewContent r=snorp,jchen
authorDylan Roeh <droeh@mozilla.com>
Tue, 17 Apr 2018 14:13:10 -0500
changeset 468694 ef8ba68c5138499ba8d39f874945270b9c121706
parent 468693 fbbae23a7d87d279116150ba5a958997aa5140dd
child 468695 2ff4e915c7223b3663e2c2d684c1dc2eb10b043c
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, jchen
bugs1441810
milestone61.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 1441810 - Add session save/restore code to GeckoViewContent r=snorp,jchen
mobile/android/chrome/geckoview/GeckoViewContent.js
mobile/android/modules/geckoview/GeckoViewContent.jsm
--- a/mobile/android/chrome/geckoview/GeckoViewContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContent.js
@@ -3,19 +3,31 @@
  * 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/. */
 
 ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
+  SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.jsm",
+  FormData: "resource://gre/modules/FormData.jsm",
+  ScrollPosition: "resource://gre/modules/ScrollPosition.jsm",
 });
 
 class GeckoViewContent extends GeckoViewContentModule {
+  onInit() {
+    debug `onInit`;
+
+    this.messageManager.addMessageListener("GeckoView:SaveState",
+                                           this);
+    this.messageManager.addMessageListener("GeckoView:RestoreState",
+                                           this);
+  }
+
   onEnable() {
     debug `onEnable`;
 
     addEventListener("DOMTitleChanged", this, false);
     addEventListener("DOMWindowFocus", this, false);
     addEventListener("DOMWindowClose", this, false);
     addEventListener("MozDOMFullscreen:Entered", this, false);
     addEventListener("MozDOMFullscreen:Exit", this, false);
@@ -24,16 +36,21 @@ class GeckoViewContent extends GeckoView
     addEventListener("contextmenu", this, { capture: true });
 
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenEntered",
                                            this);
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenExited",
                                            this);
     this.messageManager.addMessageListener("GeckoView:ZoomToInput",
                                            this);
+
+    this.progressFilter =
+      Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+      .createInstance(Ci.nsIWebProgress);
+    this.flags = Ci.nsIWebProgress.NOTIFY_LOCATION;
   }
 
   onDisable() {
     debug `onDisable`;
 
     removeEventListener("DOMTitleChanged", this);
     removeEventListener("DOMWindowFocus", this);
     removeEventListener("DOMWindowClose", this);
@@ -46,16 +63,72 @@ class GeckoViewContent extends GeckoView
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenEntered",
                                               this);
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExited",
                                               this);
     this.messageManager.removeMessageListener("GeckoView:ZoomToInput",
                                               this);
   }
 
+  /**
+   * A function that will recursively call |cb| to collected data for all
+   * non-dynamic frames in the current frame/docShell tree.
+   */
+  mapFrameTree(frame, cb) {
+    // Collect data for the current frame.
+    let callbacks = Array.isArray(cb) ? cb : [cb];
+    let objs = callbacks.map((callback) => callback(frame) || {});
+    let children = callbacks.map(() => []);
+
+    // Recurse into child frames.
+    for (let i = 0; i < frame.frames.length; i++) {
+      let subframe = frame.frames[i];
+      let results = this.mapFrameTree(subframe, callbacks);
+      results.forEach((result, j) => {
+        if (result && Object.keys(result).length) {
+          children[j][i] = result;
+        }
+      });
+    }
+
+    objs.forEach((obj, i) => {
+      if (children[i].length) {
+        obj.children = children[i];
+      }
+    });
+
+    let res = objs.map((obj) => Object.keys(obj).length ? obj : null);
+    return Array.isArray(cb) ? res : res[0];
+  }
+
+  collectSessionState() {
+    let history = SessionHistory.collect(docShell);
+    let [formdata, scrolldata] = this.mapFrameTree(content, [FormData.collect, ScrollPosition.collect]);
+
+    // Save the current document resolution.
+    let zoom = { value: 1 };
+    let domWindowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+    domWindowUtils.getResolution(zoom);
+    scrolldata.zoom = {};
+    scrolldata.zoom.resolution = zoom.value;
+
+    // Save some data that'll help in adjusting the zoom level
+    // when restoring in a different screen orientation.
+    let displaySize = {};
+    let width = {}, height = {};
+    domWindowUtils.getContentViewerSize(width, height);
+
+    displaySize.width = width.value;
+    displaySize.height = height.value;
+
+    scrolldata.zoom.displaySize = displaySize;
+
+    return {history, formdata, scrolldata};
+  }
+
   receiveMessage(aMsg) {
     debug `receiveMessage: ${aMsg.name}`;
 
     switch (aMsg.name) {
       case "GeckoView:DOMFullscreenEntered":
         if (content) {
           content.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIDOMWindowUtils)
@@ -106,18 +179,46 @@ class GeckoViewContent extends GeckoView
         // cases by allowing resizing within a set interval, and still zoom to
         // input if there is no resize event at the end of the interval.
         content.setTimeout(() => {
           removeEventListener("resize", onResize, { capture: true });
           if (!gotResize) {
             onResize();
           }
         }, 500);
+        break;
       }
-      break;
+
+      case "GeckoView:SaveState":
+        if (this._savedState) {
+          // Short circuit and return the pending state if we're in the process of restoring
+          sendAsyncMessage("GeckoView:SaveStateFinish", {state: JSON.stringify(this._savedState)});
+        } else {
+          let state = this.collectSessionState();
+          sendAsyncMessage("GeckoView:SaveStateFinish", {state: JSON.stringify(state)});
+        }
+        break;
+
+      case "GeckoView:RestoreState":
+        this._savedState = JSON.parse(aMsg.data.state);
+
+        if (this._savedState.history) {
+          let restoredHistory = SessionHistory.restore(docShell, this._savedState.history);
+
+          addEventListener("load", this, {capture: true, mozSystemGroup: true, once: true});
+          addEventListener("pageshow", this, {capture: true, mozSystemGroup: true, once: true});
+
+          this.progressFilter.addProgressListener(this, this.flags);
+          let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                    .getInterface(Ci.nsIWebProgress);
+          webProgress.addProgressListener(this.progressFilter, this.flags);
+
+          restoredHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
+        }
+        break;
     }
   }
 
   handleEvent(aEvent) {
     debug `handleEvent: ${aEvent.type}`;
 
     switch (aEvent.type) {
       case "contextmenu":
@@ -180,14 +281,50 @@ class GeckoViewContent extends GeckoView
           return;
         }
 
         aEvent.preventDefault();
         this.eventDispatcher.sendRequest({
           type: "GeckoView:DOMWindowClose"
         });
         break;
+      case "load": {
+        const formdata = this._savedState.formdata;
+        if (formdata) {
+          FormData.restoreTree(content, formdata);
+        }
+        break;
+      }
+      case "pageshow": {
+        const scrolldata = this._savedState.scrolldata;
+        if (scrolldata) {
+          ScrollPosition.restoreTree(content, scrolldata);
+        }
+        delete this._savedState;
+        break;
+      }
     }
   }
+
+  // WebProgress event handler.
+  onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+    debug `onLocationChange`;
+
+    if (this._savedState) {
+      const scrolldata = this._savedState.scrolldata;
+      if (scrolldata && scrolldata.zoom && scrolldata.zoom.displaySize) {
+        let utils = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+        // Restore zoom level.
+        utils.setRestoreResolution(scrolldata.zoom.resolution,
+                                   scrolldata.zoom.displaySize.width,
+                                   scrolldata.zoom.displaySize.height);
+      }
+    }
+
+    this.progressFilter.removeProgressListener(this);
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.removeProgressListener(this.progressFilter);
+  }
 }
 
 let {debug, warn} = GeckoViewContent.initLogging("GeckoViewContent");
 let module = GeckoViewContent.create(this);
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -7,17 +7,19 @@
 var EXPORTED_SYMBOLS = ["GeckoViewContent"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 class GeckoViewContent extends GeckoViewModule {
   onInit() {
     this.eventDispatcher.registerListener(this, [
-      "GeckoView:SetActive"
+      "GeckoView:SetActive",
+      "GeckoView:SaveState",
+      "GeckoView:RestoreState"
     ]);
   }
 
   onEnable() {
     this.registerContent("chrome://geckoview/content/GeckoViewContent.js");
 
     this.window.addEventListener("MozDOMFullScreen:Entered", this,
                                  /* capture */ true, /* untrusted */ false);
@@ -26,16 +28,17 @@ class GeckoViewContent extends GeckoView
 
     this.registerListener([
         "GeckoViewContent:ExitFullScreen",
         "GeckoView:ZoomToInput",
     ]);
 
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenExit", this);
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenRequest", this);
+    this.messageManager.addMessageListener("GeckoView:SaveStateFinish", this);
   }
 
   onDisable() {
     this.window.removeEventListener("MozDOMFullScreen:Entered", this,
                                     /* capture */ true);
     this.window.removeEventListener("MozDOMFullScreen:Exited", this,
                                     /* capture */ true);
 
@@ -62,16 +65,27 @@ class GeckoViewContent extends GeckoView
           this.browser.focus();
           this.browser.docShellIsActive = true;
         } else {
           this.browser.removeAttribute("primary");
           this.browser.docShellIsActive = false;
           this.browser.blur();
         }
         break;
+      case "GeckoView:SaveState":
+        if (this._saveStateCallback) {
+          aCallback.onError();
+        } else {
+          this.messageManager.sendAsyncMessage("GeckoView:SaveState");
+          this._saveStateCallback = aCallback;
+        }
+        break;
+      case "GeckoView:RestoreState":
+        this.messageManager.sendAsyncMessage("GeckoView:RestoreState", {state: aData.state});
+        break;
     }
   }
 
   // DOM event handler
   handleEvent(aEvent) {
     debug `handleEvent: ${aEvent.type}`;
 
     switch (aEvent.type) {
@@ -97,11 +111,17 @@ class GeckoViewContent extends GeckoView
                    .getInterface(Ci.nsIDOMWindowUtils)
                    .remoteFrameFullscreenReverted();
         break;
       case "GeckoView:DOMFullscreenRequest":
         this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils)
                    .remoteFrameFullscreenChanged(aMsg.target);
         break;
+      case "GeckoView:SaveStateFinish":
+        if (this._saveStateCallback) {
+          this._saveStateCallback.onSuccess(aMsg.data.state);
+          delete this._saveStateCallback;
+        }
+        break;
     }
   }
 }