--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -10,16 +10,17 @@ const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", "resource://gre/modules/ScrollPosition.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm");
function dump(a) {
Services.console.logStringMessage(a);
}
@@ -56,16 +57,17 @@ SessionStore.prototype = {
Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
_windows: {},
_lastSaveTime: 0,
_interval: 10000,
_maxTabsUndo: 5,
_pendingWrite: 0,
+ _scrollSavePending: null,
// The index where the most recently closed tab was in the tabs array
// when it was closed.
_lastClosedTabIndex: -1,
// Whether or not to send notifications for changes to the closed tabs.
_notifyClosedTabs: false,
@@ -270,34 +272,60 @@ SessionStore.prototype = {
// yet; pageshow happens too late, so we could lose session data if the
// browser were killed.
let browser = aEvent.currentTarget;
log("DOMTitleChanged for tab " + window.BrowserApp.getTabForBrowser(browser).id);
this.onTabLoad(window, browser);
break;
}
case "load": {
- // Handle restoring the text data into the content and frames. We wait
- // until the main content and all frames are loaded before trying to
- // restore the text data.
let browser = aEvent.currentTarget;
+
+ // Skip subframe loads.
+ if (browser.contentDocument !== aEvent.originalTarget) {
+ return;
+ }
+
+ // Handle restoring the scroll position and text data into the content
+ // and frames. We wait until the main content and all frames are loaded
+ // before trying to restore this data.
log("load for tab " + window.BrowserApp.getTabForBrowser(browser).id);
- if (browser.__SS_restore_text_data) {
+ if (browser.__SS_restoreDataOnLoad) {
+ delete browser.__SS_restoreDataOnLoad;
this._restoreTextData(browser.__SS_data.formdata, browser);
+ this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
+ } else {
+ // We're not restoring, capture the initial scroll position on load.
+ this.onTabScroll(window, browser);
}
break;
}
case "change":
case "input":
case "DOMAutoComplete": {
let browser = aEvent.currentTarget;
log("TabInput for tab " + window.BrowserApp.getTabForBrowser(browser).id);
this.onTabInput(window, browser);
break;
}
+ case "scroll": {
+ let browser = aEvent.currentTarget;
+ // Duplicated logging check to avoid calling getTabForBrowser on each scroll event.
+ if (loggingEnabled) {
+ log("scroll for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ }
+ if (!this._scrollSavePending) {
+ this._scrollSavePending =
+ window.setTimeout(() => {
+ this._scrollSavePending = null;
+ this.onTabScroll(window, browser);
+ }, 500);
+ }
+ break;
+ }
}
},
onWindowOpen: function ss_onWindowOpen(aWindow) {
// Return if window has already been initialized
if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) {
return;
}
@@ -369,31 +397,35 @@ SessionStore.prototype = {
// Use load to restore text data
aBrowser.addEventListener("load", this, true);
// Use a combination of events to watch for text data changes
aBrowser.addEventListener("change", this, true);
aBrowser.addEventListener("input", this, true);
aBrowser.addEventListener("DOMAutoComplete", this, true);
+ // Record the current scroll position
+ aBrowser.addEventListener("scroll", this, true);
+
log("onTabAdd() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
", aNoNotification = " + aNoNotification);
if (!aNoNotification) {
this.saveStateDelayed();
}
this._updateCrashReportURL(aWindow);
},
onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
// Cleanup event listeners
aBrowser.removeEventListener("DOMTitleChanged", this, true);
aBrowser.removeEventListener("load", this, true);
aBrowser.removeEventListener("change", this, true);
aBrowser.removeEventListener("input", this, true);
aBrowser.removeEventListener("DOMAutoComplete", this, true);
+ aBrowser.removeEventListener("scroll", this, true);
let tabId = aWindow.BrowserApp.getTabForBrowser(aBrowser).id;
// If this browser is being restored, skip any session save activity
if (aBrowser.__SS_restore) {
log("onTabRemove() ran for zombie tab " + tabId + ", aNoNotification = " + aNoNotification);
return;
}
@@ -462,27 +494,30 @@ SessionStore.prototype = {
continue;
}
let entry = this._serializeHistoryEntry(historyEntry);
entries.push(entry);
}
let data = { entries: entries, index: index };
let formdata;
+ let scrolldata;
if (aBrowser.__SS_data) {
formdata = aBrowser.__SS_data.formdata;
+ scrolldata = aBrowser.__SS_data.scrolldata;
}
delete aBrowser.__SS_data;
this._collectTabData(aWindow, aBrowser, data);
- if (aBrowser.__SS_restore_text_data) {
+ if (aBrowser.__SS_restoreDataOnLoad) {
// If the tab has been freshly restored and the "load" event
- // hasn't yet fired, we need to restore any form data that
- // might have been present.
+ // hasn't yet fired, we need to restore any form data and
+ // scroll positions that might have been present.
aBrowser.__SS_data.formdata = formdata;
+ aBrowser.__SS_data.scrolldata = scrolldata;
} else {
// When navigating via the forward/back buttons, Gecko restores
// the form data all by itself and doesn't invoke any input events.
// As _collectTabData() doesn't save any form data, we need to manually
// capture it to bridge the time until the next input event arrives.
this.onTabInput(aWindow, aBrowser);
}
@@ -574,16 +609,70 @@ SessionStore.prototype = {
// If we found any form data, main content or frames, let's save it
if (Object.keys(formdata).length) {
data.formdata = formdata;
log("onTabInput() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
this.saveStateDelayed();
}
},
+ onTabScroll: function ss_onTabScroll(aWindow, aBrowser) {
+ // If we've been called directly, cancel any pending timeouts.
+ if (this._scrollSavePending) {
+ aWindow.clearTimeout(this._scrollSavePending);
+ this._scrollSavePending = null;
+ log("onTabScroll() clearing pending timeout");
+ }
+
+ // If this browser is being restored, skip any session save activity.
+ if (aBrowser.__SS_restore) {
+ return;
+ }
+
+ // Don't bother trying to save scroll positions if we don't have history yet.
+ let data = aBrowser.__SS_data;
+ if (!data || data.entries.length == 0) {
+ return;
+ }
+
+ // Neither bother if we're yet to restore the previous scroll position.
+ if (aBrowser.__SS_restoreDataOnLoad) {
+ return;
+ }
+
+ // Start with storing the main content.
+ let content = aBrowser.contentWindow;
+
+ // Store the main content.
+ let scrolldata = ScrollPosition.collect(content) || {};
+
+ // Loop over direct child frames, and store the scroll positions.
+ let children = [];
+ for (let i = 0; i < content.frames.length; i++) {
+ let frame = content.frames[i];
+
+ let result = ScrollPosition.collect(frame);
+ if (result && Object.keys(result).length) {
+ children[i] = result;
+ }
+ }
+
+ // If any frame had scroll positions, add them to the main scroll data.
+ if (children.length) {
+ scrolldata.children = children;
+ }
+
+ // If we found any scroll positions, main content or frames, let's save them.
+ if (Object.keys(scrolldata).length) {
+ data.scrolldata = scrolldata;
+ log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ this.saveStateDelayed();
+ }
+ },
+
saveStateDelayed: function ss_saveStateDelayed() {
if (!this._saveTimer) {
// Interval until the next disk operation is allowed
let minimalDelay = this._lastSaveTime + this._interval - Date.now();
// If we have to wait, set a timer, otherwise saveState directly
let delay = Math.max(minimalDelay, 2000);
if (delay > 0) {
@@ -1115,19 +1204,19 @@ SessionStore.prototype = {
// aTabData shouldn't be empty here, but if it is,
// _restoreHistory() will crash otherwise.
if (!aTabData || aTabData.entries.length == 0) {
Cu.reportError("SessionStore.js: Error trying to restore tab with empty tabdata");
return;
}
this._restoreHistory(aTabData, aBrowser.sessionHistory);
- // Restoring the text data requires waiting for the content to load. So
- // we set a flag and delay this until the "load" event.
- aBrowser.__SS_restore_text_data = true;
+ // Restoring text data and scroll position requires waiting for the content
+ // to load. So we set a flag and delay this until the appropriate event.
+ aBrowser.__SS_restoreDataOnLoad = true;
},
/**
* Takes serialized history data and create news entries into the given
* nsISessionHistory object.
*/
_restoreHistory: function ss_restoreHistory(aTabData, aHistory) {
if (aHistory.count > 0) {
@@ -1162,17 +1251,26 @@ SessionStore.prototype = {
/**
* Takes serialized form text data and restores it into the given browser.
*/
_restoreTextData: function ss_restoreTextData(aFormData, aBrowser) {
if (aFormData) {
log("_restoreTextData()");
FormData.restoreTree(aBrowser.contentWindow, aFormData);
}
- delete aBrowser.__SS_restore_text_data;
+ },
+
+ /**
+ * Takes serialized scroll positions and restores them into the given browser.
+ */
+ _restoreScrollPosition: function ss_restoreScrollPosition(aScrollData, aBrowser) {
+ if (aScrollData) {
+ log("_restoreScrollPosition()");
+ ScrollPosition.restoreTree(aBrowser.contentWindow, aScrollData);
+ }
},
getBrowserState: function ss_getBrowserState() {
return this._getCurrentState();
},
_restoreWindow: function ss_restoreWindow(aData) {
let state;