Bug 1437701 - Add GeckoSession.ContentDelegate.onExternalResponse() r=esawin,droeh
authorJames Willcox <snorp@snorp.net>
Fri, 23 Mar 2018 08:58:23 -0500
changeset 410809 769685eb371b12394dc8ef04c6365fbd2646afa6
parent 410808 7de2ec5e9b047e2e0b27a650a4903f5b98a9284a
child 410810 7bbada38055fa1d1ae40a78d0650014a9db0c14d
push id33740
push usernbeleuzu@mozilla.com
push dateFri, 30 Mar 2018 21:49:44 +0000
treeherdermozilla-central@f4fcdaef6168 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersesawin, droeh
bugs1437701
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 1437701 - Add GeckoSession.ContentDelegate.onExternalResponse() r=esawin,droeh This can be used to allow the app to handle downloads. MozReview-Commit-ID: DlCNcP3quoO
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/customtabs/CustomTabsActivity.java
mobile/android/base/java/org/mozilla/gecko/webapps/WebAppActivity.java
mobile/android/components/geckoview/GeckoView.manifest
mobile/android/components/geckoview/GeckoViewExternalAppService.js
mobile/android/components/geckoview/moz.build
mobile/android/geckoview/src/androidTest/assets/www/download.html
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/BaseSessionTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/util/Callbacks.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/installer/package-manifest.in
--- 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