Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Refactor for testability
  • Loading branch information
mvanbeusekom committed Jul 21, 2021
commit 5d0431106e0682f001dd788d306c10a6b3309eeb
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,6 +17,7 @@
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
Expand All @@ -28,6 +29,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 +38,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 @@ -94,36 +97,32 @@ public void onProgressChanged(WebView view, int progress) {
displayListenerProxy.onPreWebViewInitialization(displayManager);

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

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);

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 +133,48 @@ 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>
* <li>always allow JavaScript to automatically open windows;</li>
* <li>always allow support for multiple windows;</li>
* <li>always use the {@link FlutterWebChromeClient} as web Chrome client.</li>
* </ul>
* </p>
*
* <p>
* <strong>Important:</strong>
* This method is visible for testing purposes only and should never be called from outside this
* class.
* </p>
* @param context an Activity Context to access application assets. This value cannot be
* null.
* @param params creation parameters received over the method channel.
* @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 The new {@link android.webkit.WebView} object.
*/
@VisibleForTesting
WebView createWebView(
Context context,
Map<String, Object> params,
View containerView
) {
Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition");
WebViewBuilder builder = new WebViewBuilder(context, usesHybridComposition, containerView)
.setDomStorageEnabled(true) // Always enable DOM storage API.
.setJavaScriptCanOpenWindowsAutomatically(true) // Always allow automatically opening of windows.
.setSupportMultipleWindows(true) // Always support multiple windows.
.setWebChromeClient(new FlutterWebChromeClient()); // Always use {@link FlutterWebChromeClient} as web Chrome client.

return builder.build();
}

@Override
public View getView() {
return webView;
Expand Down Expand Up @@ -369,7 +410,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 @@ -12,11 +12,11 @@
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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// 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;

/**
* 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.
*/
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 final boolean usesHybridComposition;
private final WebViewFactory webViewFactory;

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

/**
* Constructs a new {@link WebViewBuilder} object.
*
* @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})
*/
public WebViewBuilder(
@NonNull final Context context,
boolean usesHybridComposition,
View containerView) {
this(context, usesHybridComposition, containerView, new WebViewFactory());
}

/**
* 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 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})
* @param webViewFactory custom implementation of the {@link WebViewFactory} object.
*/
WebViewBuilder(
@NonNull final Context context,
boolean usesHybridComposition,
View containerView,
WebViewFactory webViewFactory
) {
this.context = context;
this.usesHybridComposition = usesHybridComposition;
this.containerView = containerView;
this.webViewFactory = webViewFactory;
}

/**
* 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;
}

/**
* Set whether the 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 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(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,7 @@ 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.flutter.plugins.webviewflutter;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.*;

import android.content.Context;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;

public class WebViewBuilderTest {
private Context mockContext;
private View mockContainerView;

@Before
public void before() {
mockContext = mock(Context.class);
mockContainerView = mock(View.class);
}

@Test
public void ctor_test() {
WebViewBuilder builder =
new WebViewBuilder(mockContext, false, mockContainerView);

assertNotNull(builder);
}

@Test
public void build_Should_set_values() throws IOException {
WebViewBuilder.WebViewFactory mockFactory =
mock(WebViewBuilder.WebViewFactory.class);
WebView mockWebView = mock(WebView.class);
WebSettings mockWebSettings = mock(WebSettings.class);
WebChromeClient mockWebChromeClient = mock(WebChromeClient.class);

when(mockWebView.getSettings()).thenReturn(mockWebSettings);

WebViewBuilder builder =
new WebViewBuilder(mockContext, false, mockContainerView, mockFactory)
.setDomStorageEnabled(true)
.setJavaScriptCanOpenWindowsAutomatically(true)
.setSupportMultipleWindows(true)
.setWebChromeClient(mockWebChromeClient);

when(mockFactory.create(mockContext, false, mockContainerView)).thenReturn(mockWebView);

WebView webView = builder.build();

assertNotNull(webView);
verify(mockWebSettings).setDomStorageEnabled(true);
verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true);
verify(mockWebSettings).setSupportMultipleWindows(true);
verify(mockWebView).setWebChromeClient(mockWebChromeClient);
}
}