Bug 1437701 - Add GeckoSession.ContentDelegate.onExternalResponse() r=esawin,droeh
This can be used to allow the app to handle downloads.
MozReview-Commit-ID: DlCNcP3quoO
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -897,16 +897,20 @@ public abstract class GeckoApp extends G
}
@Override
public void onContextMenu(final GeckoSession session, final int screenX,
final int screenY, final String uri,
int elementType, final String elementSrc) {
}
+ @Override
+ public void onExternalResponse(final GeckoSession session, final GeckoSession.WebResponseInfo request) {
+ }
+
protected void setFullScreen(final boolean fullscreen) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
onFullScreen(mLayerView.getSession(), fullscreen);
}
});
}
--- a/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
@@ -719,16 +719,19 @@ public class CustomTabsActivity extends
runOnUiThread(new Runnable() {
@Override
public void run() {
WebApps.openInFennec(validUri, CustomTabsActivity.this);
}
});
}
+ @Override
+ public void onExternalResponse(final GeckoSession session, final GeckoSession.WebResponseInfo request) {
+ }
@Override // ActionModePresenter
public void startActionMode(final ActionMode.Callback callback) {
endActionMode();
mActionMode = startSupportActionMode(callback);
}
@Override // ActionModePresenter
--- a/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
@@ -360,16 +360,20 @@ public class WebAppActivity extends AppC
if (validUri == null) {
return;
}
WebApps.openInFennec(validUri, WebAppActivity.this);
}
@Override // GeckoSession.ContentDelegate
+ public void onExternalResponse(final GeckoSession session, final GeckoSession.WebResponseInfo request) {
+ }
+
+ @Override // GeckoSession.ContentDelegate
public void onFullScreen(GeckoSession session, boolean fullScreen) {
updateFullScreenContent(fullScreen);
}
@Override
public void onLoadRequest(final GeckoSession session, final String urlStr,
final int target,
final GeckoSession.Response<Boolean> response) {
--- a/mobile/android/components/geckoview/GeckoView.manifest
+++ b/mobile/android/components/geckoview/GeckoView.manifest
@@ -14,8 +14,12 @@ contract @mozilla.org/content-permission
# GeckoViewPrompt.js
component {076ac188-23c1-4390-aa08-7ef1f78ca5d9} GeckoViewPrompt.js
contract @mozilla.org/embedcomp/prompt-service;1 {076ac188-23c1-4390-aa08-7ef1f78ca5d9}
contract @mozilla.org/prompter;1 {076ac188-23c1-4390-aa08-7ef1f78ca5d9}
component {aa0dd6fc-73dd-4621-8385-c0b377e02cee} GeckoViewPrompt.js process=main
contract @mozilla.org/colorpicker;1 {aa0dd6fc-73dd-4621-8385-c0b377e02cee} process=main
component {e4565e36-f101-4bf5-950b-4be0887785a9} GeckoViewPrompt.js process=main
contract @mozilla.org/filepicker;1 {e4565e36-f101-4bf5-950b-4be0887785a9} process=main
+
+# GeckoViewExternalAppService.js
+component {a89eeec6-6608-42ee-a4f8-04d425992f45} GeckoViewExternalAppService.js
+contract @mozilla.org/uriloader/external-helper-app-service;1 {a89eeec6-6608-42ee-a4f8-04d425992f45}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ChromeUtils.defineModuleGetter(this, "EventDispatcher",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "dump", () =>
+ ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
+ {}).AndroidLog.d.bind(null, "ViewContent"));
+
+function debug(aMsg) {
+ // dump(aMsg);
+}
+
+function ExternalAppService() {
+ this.wrappedJSObject = this;
+}
+
+ExternalAppService.prototype = {
+ classID: Components.ID("{a89eeec6-6608-42ee-a4f8-04d425992f45}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIExternalHelperAppService]),
+
+ doContent(mimeType, request, context, forceSave) {
+ const channel = request.QueryInterface(Ci.nsIChannel);
+ const mm = context.QueryInterface(Ci.nsIDocShell).tabChild.messageManager;
+
+ debug(`doContent() URI=${channel.URI.displaySpec}, contentType=${channel.contentType}`);
+
+ EventDispatcher.forMessageManager(mm).sendRequest({
+ type: "GeckoView:ExternalResponse",
+ uri: channel.URI.displaySpec,
+ contentType: channel.contentType,
+ contentLength: channel.contentLength,
+ filename: channel.contentDispositionFilename
+ });
+
+ request.cancel(Cr.NS_ERROR_ABORT);
+ Components.returnCode = Cr.NS_ERROR_ABORT;
+ },
+
+ applyDecodingForExtension(ext, encoding) {
+ debug(`applyDecodingForExtension() extension=${ext}, encoding=${encoding}`);
+
+ // This doesn't matter for us right now because
+ // we shouldn't end up reading the stream.
+ return true;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ExternalAppService]);
--- a/mobile/android/components/geckoview/moz.build
+++ b/mobile/android/components/geckoview/moz.build
@@ -1,12 +1,13 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
EXTRA_COMPONENTS += [
'GeckoView.manifest',
+ 'GeckoViewExternalAppService.js',
'GeckoViewPermission.js',
'GeckoViewPrompt.js',
'GeckoViewStartup.js',
]
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/www/download.html
@@ -0,0 +1,13 @@
+<html>
+<body>
+ <script>
+ const data = "Downloaded Data";
+ const element = document.createElement("a");
+ element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(data));
+ element.setAttribute("download", "download.txt");
+ element.style.display = "none";
+ document.body.appendChild(element);
+ element.click();
+ </script>
+</body>
+</html>
\ No newline at end of file
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
@@ -25,16 +25,17 @@ open class BaseSessionTest(noErrorCollec
companion object {
const val INVALID_URI = "http://www.test.invalid/"
const val HELLO_HTML_PATH = "/assets/www/hello.html"
const val HELLO2_HTML_PATH = "/assets/www/hello2.html"
const val NEW_SESSION_HTML_PATH = "/assets/www/newSession.html";
const val NEW_SESSION_CHILD_HTML_PATH = "/assets/www/newSession_child.html"
const val CLICK_TO_RELOAD_HTML_PATH = "/assets/www/clickToReload.html"
const val TITLE_CHANGE_HTML_PATH = "/assets/www/titleChange.html"
+ const val DOWNLOAD_HTML_PATH = "/assets/www/download.html"
}
@get:Rule val sessionRule = GeckoSessionTestRule()
@get:Rule val errors = ErrorCollector()
fun <T> assertThat(reason: String, v: T, m: Matcher<T>) = sessionRule.assertThat(reason, v, m)
init {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
@@ -1,19 +1,17 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.geckoview.test
-import android.support.test.InstrumentationRegistry
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
-import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
import org.mozilla.geckoview.test.util.Callbacks
import android.support.test.filters.MediumTest
import android.support.test.runner.AndroidJUnit4
import org.hamcrest.Matchers.*
import org.junit.Test
import org.junit.runner.RunWith
@@ -29,9 +27,33 @@ class ContentDelegateTest : BaseSessionT
@AssertCalled(count = 2)
override fun onTitleChange(session: GeckoSession, title: String) {
assertThat("Title should match", title,
equalTo(titles.removeAt(0)))
}
})
}
+ @Test fun download() {
+ sessionRule.session.loadTestPath(DOWNLOAD_HTML_PATH)
+
+ sessionRule.waitUntilCalled(object : Callbacks.NavigationDelegate, Callbacks.ContentDelegate {
+
+ @AssertCalled(count = 2)
+ override fun onLoadRequest(session: GeckoSession, uri: String, where: Int, response: GeckoSession.Response<Boolean>) {
+ response.respond(false)
+ }
+
+ @AssertCalled(false)
+ override fun onNewSession(session: GeckoSession, uri: String, response: GeckoSession.Response<GeckoSession>) {
+ }
+
+ @AssertCalled(count = 1)
+ override fun onExternalResponse(session: GeckoSession, response: GeckoSession.WebResponseInfo) {
+ assertThat("Uri should start with data:", response.uri, startsWith("data:"))
+ assertThat("Content type should match", response.contentType, equalTo("text/plain"))
+ assertThat("Content length should be non-zero", response.contentLength, greaterThan(0L))
+ assertThat("Filename should match", response.filename, equalTo("download.txt"))
+ }
+ })
+ }
+
}
\ No newline at end of file
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
@@ -1,17 +1,15 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.geckoview.test;
-import org.mozilla.gecko.mozglue.GeckoLoader;
-import org.mozilla.gecko.mozglue.SafeIntent;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoSessionSettings;
import org.mozilla.geckoview.GeckoView;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -71,16 +69,20 @@ public class TestRunnerActivity extends
public void onFullScreen(GeckoSession session, boolean fullScreen) {
}
@Override
public void onContextMenu(GeckoSession session, int screenX, int screenY, String uri, int elementType, String elementSrc) {
}
+
+ @Override
+ public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo request) {
+ }
};
private GeckoSession createSession() {
return createSession(null);
}
private GeckoSession createSession(GeckoSessionSettings settings) {
if (settings == null) {
--- 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
@@ -23,16 +23,19 @@ class Callbacks private constructor() {
override fun onCloseRequest(session: GeckoSession) {
}
override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
}
override fun onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, uri: String, elementType: Int, elementSrc: String) {
}
+
+ override fun onExternalResponse(session: GeckoSession, response: GeckoSession.WebResponseInfo) {
+ }
}
interface NavigationDelegate : GeckoSession.NavigationDelegate {
override fun onLocationChange(session: GeckoSession, url: String) {
}
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -94,16 +94,17 @@ public class GeckoSession extends LayerS
private final GeckoSessionHandler<ContentDelegate> mContentHandler =
new GeckoSessionHandler<ContentDelegate>(
"GeckoViewContent", this,
new String[]{
"GeckoView:ContextMenu",
"GeckoView:DOMTitleChanged",
"GeckoView:DOMWindowFocus",
"GeckoView:DOMWindowClose",
+ "GeckoView:ExternalResponse",
"GeckoView:FullScreenEnter",
"GeckoView:FullScreenExit"
}
) {
@Override
public void handleMessage(final ContentDelegate delegate,
final String event,
final GeckoBundle message,
@@ -125,16 +126,18 @@ public class GeckoSession extends LayerS
} else if ("GeckoView:DOMWindowFocus".equals(event)) {
delegate.onFocusRequest(GeckoSession.this);
} else if ("GeckoView:DOMWindowClose".equals(event)) {
delegate.onCloseRequest(GeckoSession.this);
} else if ("GeckoView:FullScreenEnter".equals(event)) {
delegate.onFullScreen(GeckoSession.this, true);
} else if ("GeckoView:FullScreenExit".equals(event)) {
delegate.onFullScreen(GeckoSession.this, false);
+ } else if ("GeckoView:ExternalResponse".equals(event)) {
+ delegate.onExternalResponse(GeckoSession.this, new WebResponseInfo(message));
}
}
};
private final GeckoSessionHandler<NavigationDelegate> mNavigationHandler =
new GeckoSessionHandler<NavigationDelegate>(
"GeckoViewNavigation", this,
new String[]{
@@ -1609,16 +1612,53 @@ public class GeckoSession extends LayerS
} else if ("HTMLVideoElement".equals(name)) {
return ContentDelegate.ELEMENT_TYPE_VIDEO;
} else if ("HTMLAudioElement".equals(name)) {
return ContentDelegate.ELEMENT_TYPE_AUDIO;
}
return ContentDelegate.ELEMENT_TYPE_NONE;
}
+ /**
+ * WebResponseInfo contains information about a single web response.
+ */
+ public class WebResponseInfo {
+ /**
+ * The URI of the response. Cannot be null.
+ */
+ public final String uri;
+
+ /**
+ * The content type (mime type) of the response. May be null.
+ */
+ public final String contentType;
+
+ /**
+ * The content length of the response. May be 0 if unknokwn.
+ */
+ public final long contentLength;
+
+ /**
+ * The filename obtained from the content disposition, if any.
+ * May be null.
+ */
+ public final String filename;
+
+ /* package */ WebResponseInfo(GeckoBundle message) {
+ uri = message.getString("uri");
+ if (uri == null) {
+ throw new IllegalArgumentException("URI cannot be null");
+ }
+
+ contentType = message.getString("contentType");
+ contentLength = message.getLong("contentLength");
+ filename = message.getString("filename");
+ }
+ }
+
public interface ContentDelegate {
@IntDef({ELEMENT_TYPE_NONE, ELEMENT_TYPE_IMAGE, ELEMENT_TYPE_VIDEO,
ELEMENT_TYPE_AUDIO})
public @interface ElementType {}
static final int ELEMENT_TYPE_NONE = 0;
static final int ELEMENT_TYPE_IMAGE = 1;
static final int ELEMENT_TYPE_VIDEO = 2;
static final int ELEMENT_TYPE_AUDIO = 3;
@@ -1668,16 +1708,25 @@ public class GeckoSession extends LayerS
* @param elementType The type of the pressed element.
* One of the {@link ContentDelegate#ELEMENT_TYPE_NONE} flags.
* @param elementSrc The source URI of the pressed element, set for
* (nested) images and media elements.
*/
void onContextMenu(GeckoSession session, int screenX, int screenY,
String uri, @ElementType int elementType,
String elementSrc);
+
+ /**
+ * This is fired when there is a response that cannot be handled
+ * by Gecko (e.g., a download).
+ *
+ * @param session the GeckoSession that received the external response.
+ * @param response the WebResponseInfo for the external response
+ */
+ void onExternalResponse(GeckoSession session, WebResponseInfo response);
}
/**
* This is used to send responses in delegate methods that have asynchronous responses.
*/
public interface Response<T> {
/**
* @param val The value contained in the response
--- 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
@@ -7,17 +7,16 @@ package org.mozilla.geckoview_example;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
-import android.text.TextUtils;
import android.util.Log;
import android.view.WindowManager;
import java.util.Locale;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoSessionSettings;
@@ -191,16 +190,20 @@ public class GeckoViewActivity extends A
@Override
public void onContextMenu(GeckoSession session, int screenX, int screenY,
String uri, int elementType, String elementSrc) {
Log.d(LOGTAG, "onContextMenu screenX=" + screenX +
" screenY=" + screenY + " uri=" + uri +
" elementType=" + elementType +
" elementSrc=" + elementSrc);
}
+
+ @Override
+ public void onExternalResponse(GeckoSession session, GeckoSession.WebResponseInfo request) {
+ }
}
private class MyGeckoViewProgress implements GeckoSession.ProgressDelegate {
private MyTrackingProtection mTp;
private MyGeckoViewProgress(final MyTrackingProtection tp) {
mTp = tp;
}
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -505,16 +505,17 @@
@BINPATH@/components/remotebrowserutils.manifest
[mobile]
@BINPATH@/chrome/geckoview@JAREXT@
@BINPATH@/chrome/geckoview.manifest
#ifdef MOZ_GECKOVIEW_JAR
@BINPATH@/components/GeckoView.manifest
+@BINPATH@/components/GeckoViewExternalAppService.js
@BINPATH@/components/GeckoViewPrompt.js
@BINPATH@/components/GeckoViewPermission.js
@BINPATH@/components/GeckoViewStartup.js
#else
@BINPATH@/chrome/chrome@JAREXT@
@BINPATH@/chrome/chrome.manifest
@BINPATH@/components/AboutRedirector.js
@BINPATH@/components/AddonUpdateService.js