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

[webview_flutter] Refactored creation of Android WebView for testability. #4178

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
2 changes: 2 additions & 0 deletions packages/webview_flutter/webview_flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,7 @@ android {
implementation 'androidx.annotation:annotation:1.0.0'
implementation 'androidx.webkit:webkit:1.0.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-inline:3.11.1'
testImplementation 'androidx.test:core:1.3.0'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import io.flutter.plugin.common.BinaryMessenger;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
Expand All @@ -28,6 +28,7 @@
import java.util.Map;

public class FlutterWebView implements PlatformView, MethodCallHandler {

private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames";
private final WebView webView;
private final MethodChannel methodChannel;
Expand All @@ -36,6 +37,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler {

// Verifies that a url opened by `Window.open` has a secure url.
private class FlutterWebChromeClient extends WebChromeClient {

@Override
public boolean onCreateWindow(
final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
Expand Down Expand Up @@ -83,8 +85,7 @@ public void onProgressChanged(WebView view, int progress) {
@SuppressWarnings("unchecked")
FlutterWebView(
final Context context,
BinaryMessenger messenger,
int id,
MethodChannel methodChannel,
Map<String, Object> params,
View containerView) {

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

Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition");
webView =
(usesHybridComposition)
? new WebView(context)
: new InputAwareWebView(context, containerView);
createWebView(
new WebViewBuilder(context, containerView), params, new FlutterWebChromeClient());

displayListenerProxy.onPostWebViewInitialization(displayManager);

platformThreadHandler = new Handler(context.getMainLooper());
// Allow local storage.
webView.getSettings().setDomStorageEnabled(true);
webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);

// Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679.
webView.getSettings().setSupportMultipleWindows(true);
webView.setWebChromeClient(new FlutterWebChromeClient());

methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
methodChannel.setMethodCallHandler(this);
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);
if (settings != null) {
applySettings(settings);
}

if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) {
List<String> names = (List<String>) params.get(JS_CHANNEL_NAMES_FIELD);
if (names != null) registerJavaScriptChannelNames(names);
if (names != null) {
registerJavaScriptChannelNames(names);
}
}

Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy");
if (autoMediaPlaybackPolicy != null) updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy);
if (autoMediaPlaybackPolicy != null) {
updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy);
}
if (params.containsKey("userAgent")) {
String userAgent = (String) params.get("userAgent");
updateUserAgent(userAgent);
Expand All @@ -134,6 +132,44 @@ public void onProgressChanged(WebView view, int progress) {
}
}

/**
* Creates a {@link android.webkit.WebView} and configures it according to the supplied
* parameters.
*
* <p>The {@link WebView} is configured with the following predefined settings:
*
* <ul>
* <li>always enable the DOM storage API;
* <li>always allow JavaScript to automatically open windows;
* <li>always allow support for multiple windows;
* <li>always use the {@link FlutterWebChromeClient} as web Chrome client.
* </ul>
*
* <p><strong>Important:</strong> This method is visible for testing purposes only and should
* never be called from outside this class.
*
* @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link
* WebView}.
* @param params creation parameters received over the method channel.
* @param webChromeClient an implementation of WebChromeClient This value may be null.
* @return The new {@link android.webkit.WebView} object.
*/
@VisibleForTesting
static WebView createWebView(
WebViewBuilder webViewBuilder, Map<String, Object> params, WebChromeClient webChromeClient) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should pass a Map<String, Object> object here. I think a configuration object is better and we can make a static method to map the Map to a configuration object. Might be a good idea to already pass a configuration object to the FlutterWebView constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but then we should directly pass it into the FlutterWebView constructor. I can make that happen.

Copy link
Contributor Author

@mvanbeusekom mvanbeusekom Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking into this more detailed, changing this to a configuration object requires quite a large refactor as the params also contain a second Map<String, Object> collection containing several web settings.

This means we need to change a lot of code that currently validates how to handle different situations (bases on if a key is part of the params or web settings hashmap or not). These changes are not relevant for the problem this PR is trying to solve. So for now I think it would be better to leave it as is and maybe do a separate PR on updating this if needed.

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.

return webViewBuilder.build();
}

@Override
public View getView() {
return webView;
Expand Down Expand Up @@ -369,7 +405,9 @@ private void applySettings(Map<String, Object> settings) {
switch (key) {
case "jsMode":
Integer mode = (Integer) settings.get(key);
if (mode != null) updateJsMode(mode);
if (mode != null) {
updateJsMode(mode);
}
break;
case "hasNavigationDelegate":
final boolean hasNavigationDelegate = (boolean) settings.get(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@
import android.content.Context;
import android.view.View;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.util.Map;

public final class WebViewFactory extends PlatformViewFactory {
public final class FlutterWebViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
private final View containerView;

WebViewFactory(BinaryMessenger messenger, View containerView) {
FlutterWebViewFactory(BinaryMessenger messenger, View containerView) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
this.containerView = containerView;
Expand All @@ -26,6 +27,7 @@ public final class WebViewFactory extends PlatformViewFactory {
@Override
public PlatformView create(Context context, int id, Object args) {
Map<String, Object> params = (Map<String, Object>) args;
return new FlutterWebView(context, messenger, id, params, containerView);
MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
return new FlutterWebView(context, methodChannel, params, containerView);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// 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.content.Context;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/** Builder used to create {@link android.webkit.WebView} objects. */
public class WebViewBuilder {

/** Factory used to create a new {@link android.webkit.WebView} instance. */
static class WebViewFactory {

/**
* Creates a new {@link android.webkit.WebView} instance.
*
* @param context an Activity Context to access application assets. This value cannot be null.
* @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is
* returned.
* @param containerView must be supplied when the {@code useHybridComposition} parameter is set
* to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or
* IME, thread (see also {@link InputAwareWebView})
* @return A new instance of the {@link android.webkit.WebView} object.
*/
static WebView create(Context context, boolean usesHybridComposition, View containerView) {
return usesHybridComposition
? new WebView(context)
: new InputAwareWebView(context, containerView);
}
}

private final Context context;
private final View containerView;

private boolean enableDomStorage;
private boolean javaScriptCanOpenWindowsAutomatically;
private boolean supportMultipleWindows;
private boolean usesHybridComposition;
private WebChromeClient webChromeClient;

/**
* Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link
* WebViewFactory} object.
*
* @param context an Activity Context to access application assets. This value cannot be null.
* @param containerView must be supplied when the {@code useHybridComposition} parameter is set to
* {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME,
* thread (see also {@link InputAwareWebView})
*/
WebViewBuilder(@NonNull final Context context, View containerView) {
this.context = context;
this.containerView = containerView;
}

/**
* Sets whether the DOM storage API is enabled. The default value is {@code false}.
*
* @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API.
* @return This builder. This value cannot be {@code null}.
*/
public WebViewBuilder setDomStorageEnabled(boolean flag) {
this.enableDomStorage = flag;
return this;
}

/**
* Sets whether JavaScript is allowed to open windows automatically. This applies to the
* JavaScript function {@code window.open()}. The default value is {@code false}.
*
* @param flag {@code true} if JavaScript is allowed to open windows automatically.
* @return This builder. This value cannot be {@code null}.
*/
public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) {
this.javaScriptCanOpenWindowsAutomatically = flag;
return this;
}

/**
* Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link
* WebChromeClient#onCreateWindow} must be implemented by the host application. The default is
* {@code false}.
*
* @param flag {@code true} if multiple windows are supported.
* @return This builder. This value cannot be {@code null}.
*/
public WebViewBuilder setSupportMultipleWindows(boolean flag) {
this.supportMultipleWindows = flag;
return this;
}

/**
* Sets whether the hybrid composition should be used.
*
* <p>If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the
* {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the
* {@link WebView} on Android versions below N.
*
* @param flag {@code true} if uses hybrid composition. The default is {@code false}.
* @return This builder. This value cannot be {@code null}
*/
public WebViewBuilder setUsesHybridComposition(boolean flag) {
this.usesHybridComposition = flag;
return this;
}

/**
* Sets the chrome handler. This is an implementation of WebChromeClient for use in handling
* JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler.
*
* @param webChromeClient an implementation of WebChromeClient This value may be null.
* @return This builder. This value cannot be {@code null}.
*/
public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) {
this.webChromeClient = webChromeClient;
return this;
}

/**
* Build the {@link android.webkit.WebView} using the current settings.
*
* @return The {@link android.webkit.WebView} using the current settings.
*/
public WebView build() {
WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView);

WebSettings webSettings = webView.getSettings();
webSettings.setDomStorageEnabled(enableDomStorage);
webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically);
webSettings.setSupportMultipleWindows(supportMultipleWindows);
webView.setWebChromeClient(webChromeClient);

return webView;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra
.platformViewRegistry()
.registerViewFactory(
"plugins.flutter.io/webview",
new WebViewFactory(registrar.messenger(), registrar.view()));
new FlutterWebViewFactory(registrar.messenger(), registrar.view()));
new FlutterCookieManager(registrar.messenger());
}

Expand All @@ -56,7 +56,8 @@ public void onAttachedToEngine(FlutterPluginBinding binding) {
binding
.getPlatformViewRegistry()
.registerViewFactory(
"plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null));
"plugins.flutter.io/webview",
new FlutterWebViewFactory(messenger, /*containerView=*/ null));
flutterCookieManager = new FlutterCookieManager(messenger);
}

Expand Down
Loading