Bug 1437701 - Add GeckoSession.ContentDelegate.onExternalResponse() r=esawin,droeh draft
authorJames Willcox <snorp@snorp.net>
Fri, 23 Mar 2018 08:58:23 -0500
changeset 774438 efdc4d6f10238cdb5794d0b0df3f88b098c3415c
parent 774437 6a76caab52b3680dbf4c7ca431ec315ae60ddb86
push id104401
push userbmo:snorp@snorp.net
push dateWed, 28 Mar 2018 22:12:07 +0000
reviewersesawin, droeh
bugs1437701
milestone61.0a1
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