[webview_flutter] Add download listener to Android webview (#4322)

diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md
index 361bfd2..1e1d5aa 100644
--- a/packages/webview_flutter/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 2.0.13
 
+* Send URL of File to download to the NavigationDelegate on Android just like it is already done on iOS.
 * Updated Android lint settings.
 
 ## 2.0.12
diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java
new file mode 100644
index 0000000..cfad4e3
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java
@@ -0,0 +1,33 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import android.webkit.DownloadListener;
+import android.webkit.WebView;
+
+/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */
+public class FlutterDownloadListener implements DownloadListener {
+  private final FlutterWebViewClient webViewClient;
+  private WebView webView;
+
+  public FlutterDownloadListener(FlutterWebViewClient webViewClient) {
+    this.webViewClient = webViewClient;
+  }
+
+  /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */
+  public void setWebView(WebView webView) {
+    this.webView = webView;
+  }
+
+  @Override
+  public void onDownloadStart(
+      String url,
+      String userAgent,
+      String contentDisposition,
+      String mimetype,
+      long contentLength) {
+    webViewClient.notifyDownload(webView, url);
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
index a3b681f..4651a5f 100644
--- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
+++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
@@ -11,12 +11,14 @@
 import android.os.Handler;
 import android.os.Message;
 import android.view.View;
+import android.webkit.DownloadListener;
 import android.webkit.WebChromeClient;
 import android.webkit.WebResourceRequest;
 import android.webkit.WebStorage;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
@@ -94,18 +96,25 @@
         (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
     displayListenerProxy.onPreWebViewInitialization(displayManager);
 
+    this.methodChannel = methodChannel;
+    this.methodChannel.setMethodCallHandler(this);
+
+    flutterWebViewClient = new FlutterWebViewClient(methodChannel);
+
+    FlutterDownloadListener flutterDownloadListener =
+        new FlutterDownloadListener(flutterWebViewClient);
     webView =
         createWebView(
-            new WebViewBuilder(context, containerView), params, new FlutterWebChromeClient());
+            new WebViewBuilder(context, containerView),
+            params,
+            new FlutterWebChromeClient(),
+            flutterDownloadListener);
+    flutterDownloadListener.setWebView(webView);
 
     displayListenerProxy.onPostWebViewInitialization(displayManager);
 
     platformThreadHandler = new Handler(context.getMainLooper());
 
-    this.methodChannel = methodChannel;
-    this.methodChannel.setMethodCallHandler(this);
-
-    flutterWebViewClient = new FlutterWebViewClient(methodChannel);
     Map<String, Object> settings = (Map<String, Object>) params.get("settings");
     if (settings != null) {
       applySettings(settings);
@@ -156,7 +165,10 @@
    */
   @VisibleForTesting
   static WebView createWebView(
-      WebViewBuilder webViewBuilder, Map<String, Object> params, WebChromeClient webChromeClient) {
+      WebViewBuilder webViewBuilder,
+      Map<String, Object> params,
+      WebChromeClient webChromeClient,
+      @Nullable DownloadListener downloadListener) {
     boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition"));
     webViewBuilder
         .setUsesHybridComposition(usesHybridComposition)
@@ -164,8 +176,9 @@
         .setJavaScriptCanOpenWindowsAutomatically(
             true) // Always allow automatically opening of windows.
         .setSupportMultipleWindows(true) // Always support multiple windows.
-        .setWebChromeClient(
-            webChromeClient); // Always use {@link FlutterWebChromeClient} as web Chrome client.
+        .setWebChromeClient(webChromeClient)
+        .setDownloadListener(
+            downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client.
 
     return webViewBuilder.build();
   }
diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java
index adc8467..260ef8e 100644
--- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java
+++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java
@@ -115,6 +115,22 @@
     return true;
   }
 
+  /**
+   * Notifies the Flutter code that a download should start when a navigation delegate is set.
+   *
+   * @param view the webView the result of the navigation delegate will be send to.
+   * @param url the download url
+   * @return A boolean whether or not the request is forwarded to the Flutter code.
+   */
+  boolean notifyDownload(WebView view, String url) {
+    if (!hasNavigationDelegate) {
+      return false;
+    }
+
+    notifyOnNavigationRequest(url, null, view, true);
+    return true;
+  }
+
   private void onPageStarted(WebView view, String url) {
     Map<String, Object> args = new HashMap<>();
     args.put("url", url);
diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java
index 6b8cc51..d3cd1d5 100644
--- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java
+++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java
@@ -6,6 +6,7 @@
 
 import android.content.Context;
 import android.view.View;
+import android.webkit.DownloadListener;
 import android.webkit.WebChromeClient;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
@@ -44,6 +45,7 @@
   private boolean supportMultipleWindows;
   private boolean usesHybridComposition;
   private WebChromeClient webChromeClient;
+  private DownloadListener downloadListener;
 
   /**
    * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link
@@ -123,6 +125,18 @@
   }
 
   /**
+   * Registers the interface to be used when content can not be handled by the rendering engine, and
+   * should be downloaded instead. This will replace the current handler.
+   *
+   * @param downloadListener an implementation of DownloadListener This value may be null.
+   * @return This builder. This value cannot be {@code null}.
+   */
+  public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) {
+    this.downloadListener = downloadListener;
+    return this;
+  }
+
+  /**
    * Build the {@link android.webkit.WebView} using the current settings.
    *
    * @return The {@link android.webkit.WebView} using the current settings.
@@ -135,7 +149,7 @@
     webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically);
     webSettings.setSupportMultipleWindows(supportMultipleWindows);
     webView.setWebChromeClient(webChromeClient);
-
+    webView.setDownloadListener(downloadListener);
     return webView;
   }
 }
diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java
new file mode 100644
index 0000000..2c91858
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java
@@ -0,0 +1,42 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.webkit.WebView;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FlutterDownloadListenerTest {
+  private FlutterWebViewClient webViewClient;
+  private WebView webView;
+
+  @Before
+  public void before() {
+    webViewClient = mock(FlutterWebViewClient.class);
+    webView = mock(WebView.class);
+  }
+
+  @Test
+  public void onDownloadStart_should_notify_webViewClient() {
+    String url = "testurl.com";
+    FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient);
+    downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0);
+    verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url));
+  }
+
+  @Test
+  public void onDownloadStart_should_pass_webView() {
+    FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient);
+    downloadListener.setWebView(webView);
+    downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0);
+    verify(webViewClient).notifyDownload(eq(webView), anyString());
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java
new file mode 100644
index 0000000..86346ac
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java
@@ -0,0 +1,60 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import android.webkit.WebView;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class FlutterWebViewClientTest {
+
+  MethodChannel mockMethodChannel;
+  WebView mockWebView;
+
+  @Before
+  public void before() {
+    mockMethodChannel = mock(MethodChannel.class);
+    mockWebView = mock(WebView.class);
+  }
+
+  @Test
+  public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() {
+    final String url = "testurl.com";
+
+    FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel);
+    client.createWebViewClient(true);
+
+    client.notifyDownload(mockWebView, url);
+    ArgumentCaptor<Object> argumentCaptor = ArgumentCaptor.forClass(Object.class);
+    verify(mockMethodChannel)
+        .invokeMethod(
+            eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class));
+    HashMap<String, Object> map = (HashMap<String, Object>) argumentCaptor.getValue();
+    assertEquals(map.get("url"), url);
+    assertEquals(map.get("isForMainFrame"), true);
+  }
+
+  @Test
+  public void
+      notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() {
+    final String url = "testurl.com";
+
+    FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel);
+    client.createWebViewClient(false);
+
+    client.notifyDownload(mockWebView, url);
+    verifyNoInteractions(mockMethodChannel);
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java
index 96cbdec..56d9db1 100644
--- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java
+++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java
@@ -11,6 +11,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.webkit.DownloadListener;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import java.util.HashMap;
@@ -20,6 +21,7 @@
 
 public class FlutterWebViewTest {
   private WebChromeClient mockWebChromeClient;
+  private DownloadListener mockDownloadListener;
   private WebViewBuilder mockWebViewBuilder;
   private WebView mockWebView;
 
@@ -28,6 +30,7 @@
     mockWebChromeClient = mock(WebChromeClient.class);
     mockWebViewBuilder = mock(WebViewBuilder.class);
     mockWebView = mock(WebView.class);
+    mockDownloadListener = mock(DownloadListener.class);
 
     when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder);
     when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean()))
@@ -36,6 +39,8 @@
     when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder);
     when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class)))
         .thenReturn(mockWebViewBuilder);
+    when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class)))
+        .thenReturn(mockWebViewBuilder);
 
     when(mockWebViewBuilder.build()).thenReturn(mockWebView);
   }
@@ -43,7 +48,7 @@
   @Test
   public void createWebView_should_create_webview_with_default_configuration() {
     FlutterWebView.createWebView(
-        mockWebViewBuilder, createParameterMap(false), mockWebChromeClient);
+        mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener);
 
     verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true);
     verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true);
diff --git a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java
index 48fbce2..423cb21 100644
--- a/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java
+++ b/packages/webview_flutter/webview_flutter/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java
@@ -9,6 +9,7 @@
 
 import android.content.Context;
 import android.view.View;
+import android.webkit.DownloadListener;
 import android.webkit.WebChromeClient;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
@@ -60,6 +61,7 @@
   public void build_should_set_values() throws IOException {
     WebSettings mockWebSettings = mock(WebSettings.class);
     WebChromeClient mockWebChromeClient = mock(WebChromeClient.class);
+    DownloadListener mockDownloadListener = mock(DownloadListener.class);
 
     when(mockWebView.getSettings()).thenReturn(mockWebSettings);
 
@@ -68,7 +70,8 @@
             .setDomStorageEnabled(true)
             .setJavaScriptCanOpenWindowsAutomatically(true)
             .setSupportMultipleWindows(true)
-            .setWebChromeClient(mockWebChromeClient);
+            .setWebChromeClient(mockWebChromeClient)
+            .setDownloadListener(mockDownloadListener);
 
     WebView webView = builder.build();
 
@@ -77,6 +80,7 @@
     verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true);
     verify(mockWebSettings).setSupportMultipleWindows(true);
     verify(mockWebView).setWebChromeClient(mockWebChromeClient);
+    verify(mockWebView).setDownloadListener(mockDownloadListener);
   }
 
   @Test
@@ -95,5 +99,6 @@
     verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false);
     verify(mockWebSettings).setSupportMultipleWindows(false);
     verify(mockWebView).setWebChromeClient(null);
+    verify(mockWebView).setDownloadListener(null);
   }
 }
diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml
index cc5d9cd..3976ff7 100644
--- a/packages/webview_flutter/webview_flutter/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Flutter plugin that provides a WebView widget on Android and iOS.
 repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
-version: 2.0.12
+version: 2.0.13
 
 environment:
   sdk: ">=2.12.0 <3.0.0"