Bug 1437988 - [1.5] Add progress tracking and expose progress via a delegate callback. r=jchen,snorp,droeh a=ritu
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -659,16 +659,20 @@ public class CustomTabsActivity extends
@Override
public void onPageStop(GeckoSession session, boolean success) {
mCanStop = false;
updateCanStop();
updateProgress(100);
}
@Override
+ public void onProgressChange(GeckoSession session, int progress) {
+ }
+
+ @Override
public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
mSecurityInformation = securityInfo;
updateActionBar();
}
/* GeckoSession.ContentDelegate */
@Override
public void onTitleChange(GeckoSession session, String title) {
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -110,16 +110,21 @@ public class WebAppActivity extends AppC
}
@Override
public void onPageStop(GeckoSession session, boolean success) {
}
@Override
+ public void onProgressChange(GeckoSession session, int progress) {
+
+ }
+
+ @Override
public void onSecurityChange(GeckoSession session, SecurityInformation security) {
// We want to ignore the extraneous first about:blank load
if (mIsFirstLoad && security.origin.startsWith("moz-nullprincipal:")) {
mIsFirstLoad = false;
return;
}
mIsFirstLoad = false;
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/geckoview/GeckoViewProgressContent.js
@@ -0,0 +1,331 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+});
+
+class GeckoViewProgressContent extends GeckoViewContentModule {
+ onInit() {
+ debug `onInit`;
+ }
+
+ onEnable() {
+ debug `onEnable`;
+
+ ProgressTracker.onEnable(this);
+
+ let flags = Ci.nsIWebProgress.NOTIFY_PROGRESS |
+ Ci.nsIWebProgress.NOTIFY_STATE_NETWORK |
+ Ci.nsIWebProgress.NOTIFY_LOCATION;
+ this.progressFilter =
+ Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+ .createInstance(Ci.nsIWebProgress);
+ this.progressFilter.addProgressListener(this, flags);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this.progressFilter, flags);
+ }
+
+ onDisable() {
+ debug `onDisable`;
+
+ if (this.progressFilter) {
+ this.progressFilter.removeProgressListener(this);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(this.progressFilter);
+ }
+ }
+
+ onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
+ debug `onProgressChange ${aCurSelf}/${aMaxSelf} ${aCurTotal}/${aMaxTotal}`;
+
+ ProgressTracker.handleProgress(null, aCurTotal, aMaxTotal);
+ ProgressTracker.updateProgress();
+ }
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ debug `onStateChange: isTopLevel=${aWebProgress.isTopLevel},
+ flags=${aStateFlags}, status=${aStatus}`;
+
+ if (!aWebProgress || !aWebProgress.isTopLevel) {
+ return;
+ }
+
+ const uri = aRequest.QueryInterface(Ci.nsIChannel).URI.displaySpec;
+ debug `onStateChange: uri=${uri}`;
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ ProgressTracker.start(uri);
+ } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
+ !aWebProgress.isLoadingDocument) {
+ ProgressTracker.stop();
+ } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_REDIRECTING) {
+ ProgressTracker.start(uri);
+ }
+ }
+
+ onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+ debug `onLocationChange: location=${aLocationURI.displaySpec},
+ flags=${aFlags}`;
+
+ if (!aWebProgress || !aWebProgress.isTopLevel) {
+ return;
+ }
+
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ ProgressTracker.stop();
+ } else {
+ ProgressTracker.changeLocation(aLocationURI.displaySpec);
+ }
+ }
+}
+
+const ProgressTracker = {
+ onEnable: function(aModule) {
+ this._module = aModule;
+ this.clear();
+ },
+
+ start: function(aUri) {
+ debug `ProgressTracker start ${aUri}`;
+
+ if (this._tracking) {
+ this.stop();
+ }
+
+ addEventListener("MozAfterPaint", this,
+ { capture: false, mozSystemGroup: true });
+ addEventListener("DOMContentLoaded", this,
+ { capture: false, mozSystemGroup: true });
+ addEventListener("pageshow", this,
+ { capture: false, mozSystemGroup: true });
+
+ this._tracking = true;
+ this.clear();
+ let data = this._data;
+
+ if (aUri === "about:blank") {
+ data.uri = null;
+ return;
+ }
+
+ data.uri = aUri;
+ data.pageStart = true;
+ this.updateProgress();
+ },
+
+ changeLocation: function(aUri) {
+ debug `ProgressTracker changeLocation ${aUri}`;
+
+ let data = this._data;
+ data.locationChange = true;
+ data.uri = aUri;
+ },
+
+ stop: function() {
+ debug `ProgressTracker stop`;
+
+ let data = this._data;
+ data.pageStop = true;
+ this.updateProgress();
+ this._tracking = false;
+
+ if (!data.parsed) {
+ removeEventListener("DOMContentLoaded", this,
+ { capture: false, mozSystemGroup: true });
+ }
+ if (!data.firstPaint) {
+ removeEventListener("MozAfterPaint", this,
+ { capture: false, mozSystemGroup: true });
+ }
+ if (!data.pageShow) {
+ removeEventListener("pageshow", this,
+ { capture: false, mozSystemGroup: true });
+ }
+ },
+
+ get messageManager() {
+ return this._module.messageManager;
+ },
+
+ get eventDispatcher() {
+ return this._module.eventDispatcher;
+ },
+
+ handleEvent: function(aEvent) {
+ let data = this._data;
+
+ const target = aEvent.originalTarget;
+ const uri = target && target.location.href;
+
+ if (!data.uri || data.uri !== uri) {
+ return;
+ }
+
+ debug `ProgressTracker handleEvent: ${aEvent.type}`;
+
+ let needsUpdate = false;
+
+ switch (aEvent.type) {
+ case "DOMContentLoaded":
+ needsUpdate = needsUpdate || !data.parsed;
+ data.parsed = true;
+ removeEventListener("DOMContentLoaded", this,
+ { capture: false, mozSystemGroup: true });
+ break;
+ case "MozAfterPaint":
+ needsUpdate = needsUpdate || !data.firstPaint;
+ data.firstPaint = true;
+ removeEventListener("MozAfterPaint", this,
+ { capture: false, mozSystemGroup: true });
+ break;
+ case "pageshow":
+ needsUpdate = needsUpdate || !data.pageShow;
+ data.pageShow = true;
+ removeEventListener("pageshow", this,
+ { capture: false, mozSystemGroup: true });
+ break;
+ }
+
+ if (needsUpdate) {
+ this.updateProgress();
+ }
+ },
+
+ clear: function() {
+ this._data = {
+ prev: 0,
+ uri: null,
+ locationChange: false,
+ pageStart: false,
+ pageStop: false,
+ firstPaint: false,
+ pageShow: false,
+ parsed: false,
+ totalReceived: 1,
+ totalExpected: 1,
+ channels: {},
+ };
+ },
+
+ _debugData: function() {
+ return {
+ prev: this._data.prev,
+ uri: this._data.uri,
+ locationChange: this._data.locationChange,
+ pageStart: this._data.pageStart,
+ pageStop: this._data.pageStop,
+ firstPaint: this._data.firstPaint,
+ pageShow: this._data.pageShow,
+ parsed: this._data.parsed,
+ totalReceived: this._data.totalReceived,
+ totalExpected: this._data.totalExpected,
+ };
+ },
+
+ handleProgress: function(aChannelUri, aProgress, aMax) {
+ debug `ProgressTracker handleProgress ${aChannelUri} ${aProgress}/${aMax}`;
+
+ let data = this._data;
+
+ if (!data.uri) {
+ return;
+ }
+
+ aChannelUri = aChannelUri || data.uri;
+
+ const now = content.performance.now();
+
+ if (!data.channels[aChannelUri]) {
+ data.channels[aChannelUri] = {
+ received: aProgress,
+ max: aMax,
+ expected: (aMax > 0 ? aMax : aProgress * 2),
+ lastUpdate: now,
+ };
+ } else {
+ let channelProgress = data.channels[aChannelUri];
+ channelProgress.received = Math.max(channelProgress.received, aProgress);
+ channelProgress.expected = Math.max(channelProgress.expected, aMax);
+ channelProgress.lastUpdate = now;
+ }
+ },
+
+ updateProgress: function() {
+ debug `ProgressTracker updateProgress`;
+
+ let data = this._data;
+
+ if (!this._tracking || !data.uri) {
+ return;
+ }
+
+ let progress = 0;
+
+ if (data.pageStart) {
+ progress += 10;
+ }
+ if (data.firstPaint) {
+ progress += 15;
+ }
+ if (data.parsed) {
+ progress += 15;
+ }
+ if (data.pageShow) {
+ progress += 15;
+ }
+ if (data.locationChange) {
+ progress += 10;
+ }
+
+ data.totalReceived = 1;
+ data.totalExpected = 1;
+ const channelOverdue = content.performance.now() - 300;
+
+ for (let channel in data.channels) {
+ if (data.channels[channel].max < 1 &&
+ channelOverdue > data.channels[channel].lastUpdate) {
+ data.channels[channel].expected = data.channels[channel].received;
+ }
+ data.totalReceived += data.channels[channel].received;
+ data.totalExpected += data.channels[channel].expected;
+ }
+
+ const minExpected = 1024 * 1;
+ const maxExpected = 1024 * 1024 * 0.5;
+
+ if (data.pageStop ||
+ (data.pageStart && data.firstPaint && data.parsed && data.pageShow &&
+ data.totalReceived > minExpected &&
+ data.totalReceived >= data.totalExpected)) {
+ progress = 100;
+ } else {
+ const a = Math.min(1, (data.totalExpected / maxExpected)) * 30;
+ progress += data.totalReceived / data.totalExpected * a;
+ }
+
+ debug `ProgressTracker onProgressChangeUpdate ${this._debugData()} ${data.totalReceived}/${data.totalExpected} progress=${progress}`;
+
+ if (data.prev >= progress) {
+ return;
+ }
+
+ this.eventDispatcher.sendRequest({
+ type: "GeckoView:ProgressChanged",
+ progress,
+ });
+
+ data.prev = progress;
+ },
+};
+
+
+let {debug, warn} = GeckoViewProgressContent.initLogging("GeckoViewProgress");
+let module = GeckoViewProgressContent.create(this);
--- a/mobile/android/chrome/geckoview/geckoview.js
+++ b/mobile/android/chrome/geckoview/geckoview.js
@@ -329,16 +329,17 @@ function startup() {
},
onEnable: {
frameScript: "chrome://geckoview/content/GeckoViewNavigationContent.js",
},
}, {
name: "GeckoViewProgress",
onEnable: {
resource: "resource://gre/modules/GeckoViewProgress.jsm",
+ frameScript: "chrome://geckoview/content/GeckoViewProgressContent.js",
},
}, {
name: "GeckoViewScroll",
onEnable: {
frameScript: "chrome://geckoview/content/GeckoViewScrollContent.js",
},
}, {
name: "GeckoViewSelectionAction",
--- a/mobile/android/chrome/geckoview/jar.mn
+++ b/mobile/android/chrome/geckoview/jar.mn
@@ -6,11 +6,12 @@ geckoview.jar:
% content geckoview %content/
content/ErrorPageEventHandler.js
content/geckoview.xul
content/geckoview.js
content/GeckoViewContent.js
content/GeckoViewContentSettings.js
content/GeckoViewNavigationContent.js
+ content/GeckoViewProgressContent.js
content/GeckoViewPromptContent.js
content/GeckoViewScrollContent.js
content/GeckoViewSelectionActionContent.js
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
@@ -169,16 +169,20 @@ class GeckoSessionTestRuleTest : BaseSes
override fun onPageStop(session: GeckoSession, success: Boolean) {
counter++
}
override fun onSecurityChange(session: GeckoSession,
securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
counter++
}
+
+ override fun onProgressChange(session: GeckoSession, progress: Int) {
+ counter++
+ }
})
assertThat("Callback count should be correct", counter, equalTo(1))
}
@Test fun waitUntilCalled_specificInterfaceMethod() {
sessionRule.session.loadTestPath(HELLO_HTML_PATH)
sessionRule.waitUntilCalled(GeckoSession.ProgressDelegate::class,
@@ -236,16 +240,20 @@ class GeckoSessionTestRuleTest : BaseSes
override fun onPageStop(session: GeckoSession, success: Boolean) {
counter++
}
override fun onSecurityChange(session: GeckoSession,
securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
counter++
}
+
+ override fun onProgressChange(session: GeckoSession, progress: Int) {
+ counter++
+ }
})
assertThat("Callback count should be correct", counter, equalTo(1))
}
@Test fun waitUntilCalled_specificObjectMethod() {
sessionRule.session.loadTestPath(HELLO_HTML_PATH)
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
@@ -79,16 +79,19 @@ class Callbacks private constructor() {
interface ProgressDelegate : GeckoSession.ProgressDelegate {
override fun onPageStart(session: GeckoSession, url: String) {
}
override fun onPageStop(session: GeckoSession, success: Boolean) {
}
+ override fun onProgressChange(session: GeckoSession, progress: Int) {
+ }
+
override fun onSecurityChange(session: GeckoSession, securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
}
}
interface PromptDelegate : GeckoSession.PromptDelegate {
override fun onAlert(session: GeckoSession, title: String, msg: String, callback: GeckoSession.PromptDelegate.AlertCallback) {
callback.dismiss()
}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -266,30 +266,34 @@ public class GeckoSession extends LayerS
};
private final GeckoSessionHandler<ProgressDelegate> mProgressHandler =
new GeckoSessionHandler<ProgressDelegate>(
"GeckoViewProgress", this,
new String[]{
"GeckoView:PageStart",
"GeckoView:PageStop",
+ "GeckoView:ProgressChanged",
"GeckoView:SecurityChanged"
}
) {
@Override
public void handleMessage(final ProgressDelegate delegate,
final String event,
final GeckoBundle message,
final EventCallback callback) {
if ("GeckoView:PageStart".equals(event)) {
delegate.onPageStart(GeckoSession.this,
message.getString("uri"));
} else if ("GeckoView:PageStop".equals(event)) {
delegate.onPageStop(GeckoSession.this,
message.getBoolean("success"));
+ } else if ("GeckoView:ProgressChanged".equals(event)) {
+ delegate.onProgressChange(GeckoSession.this,
+ message.getInt("progress"));
} else if ("GeckoView:SecurityChanged".equals(event)) {
final GeckoBundle identity = message.getBundle("identity");
delegate.onSecurityChange(GeckoSession.this, new ProgressDelegate.SecurityInformation(identity));
}
}
};
private final GeckoSessionHandler<ScrollDelegate> mScrollHandler =
@@ -1882,16 +1886,23 @@ public class GeckoSession extends LayerS
/**
* A View has finished loading content from the network.
* @param session GeckoSession that initiated the callback.
* @param success Whether the page loaded successfully or an error occurred.
*/
void onPageStop(GeckoSession session, boolean success);
/**
+ * Page loading has progressed.
+ * @param session GeckoSession that initiated the callback.
+ * @param progress Current page load progress value [0, 100].
+ */
+ void onProgressChange(GeckoSession session, int progress);
+
+ /**
* The security status has been updated.
* @param session GeckoSession that initiated the callback.
* @param securityInfo The new security information.
*/
void onSecurityChange(GeckoSession session, SecurityInformation securityInfo);
}
private static int getContentElementType(final String name) {
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -427,16 +427,21 @@ public class GeckoViewActivity extends A
public void onPageStop(GeckoSession session, boolean success) {
Log.i(LOGTAG, "Stopping page load " + (success ? "successfully" : "unsuccessfully"));
Log.i(LOGTAG, "zerdatime " + SystemClock.elapsedRealtime() +
" - page load stop");
mTp.logCounters();
}
@Override
+ public void onProgressChange(GeckoSession session, int progress) {
+ Log.i(LOGTAG, "onProgressChange " + progress);
+ }
+
+ @Override
public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
Log.i(LOGTAG, "Security status changed to " + securityInfo.securityMode);
}
}
private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate {
public int androidPermissionRequestCode = 1;