Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[webview_flutter] Add download listener to Android webview #4322

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/webview_flutter/webview_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,18 +96,25 @@ public void onProgressChanged(WebView view, int progress) {
(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);
Expand Down Expand Up @@ -156,16 +165,20 @@ public void onProgressChanged(WebView view, int progress) {
*/
@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)
.setDomStorageEnabled(true) // Always enable DOM storage API.
.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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,22 @@ boolean shouldOverrideUrlLoading(WebView view, String url) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,6 +45,7 @@ static WebView create(Context context, boolean usesHybridComposition, View conta
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
Expand Down Expand Up @@ -122,6 +124,18 @@ public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClie
return this;
}

/**
* 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.
*
Expand All @@ -135,7 +149,7 @@ public WebView build() {
webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically);
webSettings.setSupportMultipleWindows(supportMultipleWindows);
webView.setWebChromeClient(webChromeClient);

webView.setDownloadListener(downloadListener);
return webView;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,7 @@

public class FlutterWebViewTest {
private WebChromeClient mockWebChromeClient;
private DownloadListener mockDownloadListener;
private WebViewBuilder mockWebViewBuilder;
private WebView mockWebView;

Expand All @@ -28,6 +30,7 @@ public void before() {
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()))
Expand All @@ -36,14 +39,16 @@ public void before() {
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);
}

@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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,6 +61,7 @@ public void ctor_test() {
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);

Expand All @@ -68,7 +70,8 @@ public void build_should_set_values() throws IOException {
.setDomStorageEnabled(true)
.setJavaScriptCanOpenWindowsAutomatically(true)
.setSupportMultipleWindows(true)
.setWebChromeClient(mockWebChromeClient);
.setWebChromeClient(mockWebChromeClient)
.setDownloadListener(mockDownloadListener);

WebView webView = builder.build();

Expand All @@ -77,6 +80,7 @@ public void build_should_set_values() throws IOException {
verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true);
verify(mockWebSettings).setSupportMultipleWindows(true);
verify(mockWebView).setWebChromeClient(mockWebChromeClient);
verify(mockWebView).setDownloadListener(mockDownloadListener);
}

@Test
Expand All @@ -95,5 +99,6 @@ public void build_should_use_default_values() throws IOException {
verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false);
verify(mockWebSettings).setSupportMultipleWindows(false);
verify(mockWebView).setWebChromeClient(null);
verify(mockWebView).setDownloadListener(null);
}
}
2 changes: 1 addition & 1 deletion packages/webview_flutter/webview_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter
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"
Expand Down