Skip to content

Commit 6e19b49

Browse files
mvanbeusekomamantoux
authored andcommitted
[webview_flutter] Refactored creation of Android WebView for testability. (flutter#4178)
1 parent 995f075 commit 6e19b49

File tree

7 files changed

+369
-25
lines changed

7 files changed

+369
-25
lines changed

packages/webview_flutter/webview_flutter/android/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,7 @@ android {
3737
implementation 'androidx.annotation:annotation:1.0.0'
3838
implementation 'androidx.webkit:webkit:1.0.0'
3939
testImplementation 'junit:junit:4.12'
40+
testImplementation 'org.mockito:mockito-inline:3.11.1'
41+
testImplementation 'androidx.test:core:1.3.0'
4042
}
4143
}

packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import android.webkit.WebView;
1818
import android.webkit.WebViewClient;
1919
import androidx.annotation.NonNull;
20-
import io.flutter.plugin.common.BinaryMessenger;
20+
import androidx.annotation.VisibleForTesting;
2121
import io.flutter.plugin.common.MethodCall;
2222
import io.flutter.plugin.common.MethodChannel;
2323
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
@@ -28,6 +28,7 @@
2828
import java.util.Map;
2929

3030
public class FlutterWebView implements PlatformView, MethodCallHandler {
31+
3132
private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames";
3233
private final WebView webView;
3334
private final MethodChannel methodChannel;
@@ -36,6 +37,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler {
3637

3738
// Verifies that a url opened by `Window.open` has a secure url.
3839
private class FlutterWebChromeClient extends WebChromeClient {
40+
3941
@Override
4042
public boolean onCreateWindow(
4143
final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
@@ -83,8 +85,7 @@ public void onProgressChanged(WebView view, int progress) {
8385
@SuppressWarnings("unchecked")
8486
FlutterWebView(
8587
final Context context,
86-
BinaryMessenger messenger,
87-
int id,
88+
MethodChannel methodChannel,
8889
Map<String, Object> params,
8990
View containerView) {
9091

@@ -93,37 +94,34 @@ public void onProgressChanged(WebView view, int progress) {
9394
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
9495
displayListenerProxy.onPreWebViewInitialization(displayManager);
9596

96-
Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition");
9797
webView =
98-
(usesHybridComposition)
99-
? new WebView(context)
100-
: new InputAwareWebView(context, containerView);
98+
createWebView(
99+
new WebViewBuilder(context, containerView), params, new FlutterWebChromeClient());
101100

102101
displayListenerProxy.onPostWebViewInitialization(displayManager);
103102

104103
platformThreadHandler = new Handler(context.getMainLooper());
105-
// Allow local storage.
106-
webView.getSettings().setDomStorageEnabled(true);
107-
webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
108-
109-
// Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679.
110-
webView.getSettings().setSupportMultipleWindows(true);
111-
webView.setWebChromeClient(new FlutterWebChromeClient());
112104

113-
methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
114-
methodChannel.setMethodCallHandler(this);
105+
this.methodChannel = methodChannel;
106+
this.methodChannel.setMethodCallHandler(this);
115107

116108
flutterWebViewClient = new FlutterWebViewClient(methodChannel);
117109
Map<String, Object> settings = (Map<String, Object>) params.get("settings");
118-
if (settings != null) applySettings(settings);
110+
if (settings != null) {
111+
applySettings(settings);
112+
}
119113

120114
if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) {
121115
List<String> names = (List<String>) params.get(JS_CHANNEL_NAMES_FIELD);
122-
if (names != null) registerJavaScriptChannelNames(names);
116+
if (names != null) {
117+
registerJavaScriptChannelNames(names);
118+
}
123119
}
124120

125121
Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy");
126-
if (autoMediaPlaybackPolicy != null) updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy);
122+
if (autoMediaPlaybackPolicy != null) {
123+
updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy);
124+
}
127125
if (params.containsKey("userAgent")) {
128126
String userAgent = (String) params.get("userAgent");
129127
updateUserAgent(userAgent);
@@ -134,6 +132,44 @@ public void onProgressChanged(WebView view, int progress) {
134132
}
135133
}
136134

135+
/**
136+
* Creates a {@link android.webkit.WebView} and configures it according to the supplied
137+
* parameters.
138+
*
139+
* <p>The {@link WebView} is configured with the following predefined settings:
140+
*
141+
* <ul>
142+
* <li>always enable the DOM storage API;
143+
* <li>always allow JavaScript to automatically open windows;
144+
* <li>always allow support for multiple windows;
145+
* <li>always use the {@link FlutterWebChromeClient} as web Chrome client.
146+
* </ul>
147+
*
148+
* <p><strong>Important:</strong> This method is visible for testing purposes only and should
149+
* never be called from outside this class.
150+
*
151+
* @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link
152+
* WebView}.
153+
* @param params creation parameters received over the method channel.
154+
* @param webChromeClient an implementation of WebChromeClient This value may be null.
155+
* @return The new {@link android.webkit.WebView} object.
156+
*/
157+
@VisibleForTesting
158+
static WebView createWebView(
159+
WebViewBuilder webViewBuilder, Map<String, Object> params, WebChromeClient webChromeClient) {
160+
boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition"));
161+
webViewBuilder
162+
.setUsesHybridComposition(usesHybridComposition)
163+
.setDomStorageEnabled(true) // Always enable DOM storage API.
164+
.setJavaScriptCanOpenWindowsAutomatically(
165+
true) // Always allow automatically opening of windows.
166+
.setSupportMultipleWindows(true) // Always support multiple windows.
167+
.setWebChromeClient(
168+
webChromeClient); // Always use {@link FlutterWebChromeClient} as web Chrome client.
169+
170+
return webViewBuilder.build();
171+
}
172+
137173
@Override
138174
public View getView() {
139175
return webView;
@@ -369,7 +405,9 @@ private void applySettings(Map<String, Object> settings) {
369405
switch (key) {
370406
case "jsMode":
371407
Integer mode = (Integer) settings.get(key);
372-
if (mode != null) updateJsMode(mode);
408+
if (mode != null) {
409+
updateJsMode(mode);
410+
}
373411
break;
374412
case "hasNavigationDelegate":
375413
final boolean hasNavigationDelegate = (boolean) settings.get(key);
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
import android.content.Context;
88
import android.view.View;
99
import io.flutter.plugin.common.BinaryMessenger;
10+
import io.flutter.plugin.common.MethodChannel;
1011
import io.flutter.plugin.common.StandardMessageCodec;
1112
import io.flutter.plugin.platform.PlatformView;
1213
import io.flutter.plugin.platform.PlatformViewFactory;
1314
import java.util.Map;
1415

15-
public final class WebViewFactory extends PlatformViewFactory {
16+
public final class FlutterWebViewFactory extends PlatformViewFactory {
1617
private final BinaryMessenger messenger;
1718
private final View containerView;
1819

19-
WebViewFactory(BinaryMessenger messenger, View containerView) {
20+
FlutterWebViewFactory(BinaryMessenger messenger, View containerView) {
2021
super(StandardMessageCodec.INSTANCE);
2122
this.messenger = messenger;
2223
this.containerView = containerView;
@@ -26,6 +27,7 @@ public final class WebViewFactory extends PlatformViewFactory {
2627
@Override
2728
public PlatformView create(Context context, int id, Object args) {
2829
Map<String, Object> params = (Map<String, Object>) args;
29-
return new FlutterWebView(context, messenger, id, params, containerView);
30+
MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
31+
return new FlutterWebView(context, methodChannel, params, containerView);
3032
}
3133
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.webviewflutter;
6+
7+
import android.content.Context;
8+
import android.view.View;
9+
import android.webkit.WebChromeClient;
10+
import android.webkit.WebSettings;
11+
import android.webkit.WebView;
12+
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
14+
15+
/** Builder used to create {@link android.webkit.WebView} objects. */
16+
public class WebViewBuilder {
17+
18+
/** Factory used to create a new {@link android.webkit.WebView} instance. */
19+
static class WebViewFactory {
20+
21+
/**
22+
* Creates a new {@link android.webkit.WebView} instance.
23+
*
24+
* @param context an Activity Context to access application assets. This value cannot be null.
25+
* @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is
26+
* returned.
27+
* @param containerView must be supplied when the {@code useHybridComposition} parameter is set
28+
* to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or
29+
* IME, thread (see also {@link InputAwareWebView})
30+
* @return A new instance of the {@link android.webkit.WebView} object.
31+
*/
32+
static WebView create(Context context, boolean usesHybridComposition, View containerView) {
33+
return usesHybridComposition
34+
? new WebView(context)
35+
: new InputAwareWebView(context, containerView);
36+
}
37+
}
38+
39+
private final Context context;
40+
private final View containerView;
41+
42+
private boolean enableDomStorage;
43+
private boolean javaScriptCanOpenWindowsAutomatically;
44+
private boolean supportMultipleWindows;
45+
private boolean usesHybridComposition;
46+
private WebChromeClient webChromeClient;
47+
48+
/**
49+
* Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link
50+
* WebViewFactory} object.
51+
*
52+
* @param context an Activity Context to access application assets. This value cannot be null.
53+
* @param containerView must be supplied when the {@code useHybridComposition} parameter is set to
54+
* {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME,
55+
* thread (see also {@link InputAwareWebView})
56+
*/
57+
WebViewBuilder(@NonNull final Context context, View containerView) {
58+
this.context = context;
59+
this.containerView = containerView;
60+
}
61+
62+
/**
63+
* Sets whether the DOM storage API is enabled. The default value is {@code false}.
64+
*
65+
* @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API.
66+
* @return This builder. This value cannot be {@code null}.
67+
*/
68+
public WebViewBuilder setDomStorageEnabled(boolean flag) {
69+
this.enableDomStorage = flag;
70+
return this;
71+
}
72+
73+
/**
74+
* Sets whether JavaScript is allowed to open windows automatically. This applies to the
75+
* JavaScript function {@code window.open()}. The default value is {@code false}.
76+
*
77+
* @param flag {@code true} if JavaScript is allowed to open windows automatically.
78+
* @return This builder. This value cannot be {@code null}.
79+
*/
80+
public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) {
81+
this.javaScriptCanOpenWindowsAutomatically = flag;
82+
return this;
83+
}
84+
85+
/**
86+
* Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link
87+
* WebChromeClient#onCreateWindow} must be implemented by the host application. The default is
88+
* {@code false}.
89+
*
90+
* @param flag {@code true} if multiple windows are supported.
91+
* @return This builder. This value cannot be {@code null}.
92+
*/
93+
public WebViewBuilder setSupportMultipleWindows(boolean flag) {
94+
this.supportMultipleWindows = flag;
95+
return this;
96+
}
97+
98+
/**
99+
* Sets whether the hybrid composition should be used.
100+
*
101+
* <p>If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the
102+
* {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the
103+
* {@link WebView} on Android versions below N.
104+
*
105+
* @param flag {@code true} if uses hybrid composition. The default is {@code false}.
106+
* @return This builder. This value cannot be {@code null}
107+
*/
108+
public WebViewBuilder setUsesHybridComposition(boolean flag) {
109+
this.usesHybridComposition = flag;
110+
return this;
111+
}
112+
113+
/**
114+
* Sets the chrome handler. This is an implementation of WebChromeClient for use in handling
115+
* JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler.
116+
*
117+
* @param webChromeClient an implementation of WebChromeClient This value may be null.
118+
* @return This builder. This value cannot be {@code null}.
119+
*/
120+
public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) {
121+
this.webChromeClient = webChromeClient;
122+
return this;
123+
}
124+
125+
/**
126+
* Build the {@link android.webkit.WebView} using the current settings.
127+
*
128+
* @return The {@link android.webkit.WebView} using the current settings.
129+
*/
130+
public WebView build() {
131+
WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView);
132+
133+
WebSettings webSettings = webView.getSettings();
134+
webSettings.setDomStorageEnabled(enableDomStorage);
135+
webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically);
136+
webSettings.setSupportMultipleWindows(supportMultipleWindows);
137+
webView.setWebChromeClient(webChromeClient);
138+
139+
return webView;
140+
}
141+
}

packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra
4646
.platformViewRegistry()
4747
.registerViewFactory(
4848
"plugins.flutter.io/webview",
49-
new WebViewFactory(registrar.messenger(), registrar.view()));
49+
new FlutterWebViewFactory(registrar.messenger(), registrar.view()));
5050
new FlutterCookieManager(registrar.messenger());
5151
}
5252

@@ -56,7 +56,8 @@ public void onAttachedToEngine(FlutterPluginBinding binding) {
5656
binding
5757
.getPlatformViewRegistry()
5858
.registerViewFactory(
59-
"plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null));
59+
"plugins.flutter.io/webview",
60+
new FlutterWebViewFactory(messenger, /*containerView=*/ null));
6061
flutterCookieManager = new FlutterCookieManager(messenger);
6162
}
6263

0 commit comments

Comments
 (0)