From 760d0a5b8baa49f7e4aeabf8c185bb821760fe7e Mon Sep 17 00:00:00 2001 From: Dot Cink Date: Mon, 13 Jan 2020 12:01:28 +0800 Subject: [PATCH] [webview_flutter] configure `mixedContentMode` for Android WebView --- packages/webview_flutter/CHANGELOG.md | 4 ++ packages/webview_flutter/README.md | 4 ++ .../webviewflutter/FlutterWebView.java | 27 ++++++++++++ .../android/app/src/main/AndroidManifest.xml | 3 +- .../webview_flutter_test.dart | 42 +++++++++++++++++++ .../webview_flutter/example/lib/main.dart | 1 + .../lib/platform_interface.dart | 8 +++- .../lib/src/webview_method_channel.dart | 1 + .../webview_flutter/lib/webview_flutter.dart | 27 ++++++++++++ packages/webview_flutter/pubspec.yaml | 2 +- .../test/webview_flutter_test.dart | 27 ++++++++++++ 11 files changed, 143 insertions(+), 3 deletions(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 6d2b4bb26815..574e1b247faf 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.3 + +* Add `mixedContentMode` option for Android WebView. + ## 2.0.2 * Fixes bug where text fields are hidden behind the keyboard diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index 94000772ca71..d1f355e40757 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -71,3 +71,7 @@ android { To use Material Components when the user interacts with input elements in the WebView, follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). + +### Android `mixedContentMode` + +1. Android `mixedContentMode` requires `usesCleartextTraffic` in `AndroidManifest.xml`; diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index 022f1c3597e7..fb2943c70048 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -13,6 +13,7 @@ import android.view.View; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; +import android.webkit.WebSettings; import android.webkit.WebStorage; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -371,6 +372,14 @@ private void applySettings(Map settings) { Integer mode = (Integer) settings.get(key); if (mode != null) updateJsMode(mode); break; + case "mixedContentMode": + { + Object value = settings.get(key); + if (value != null) { + updateMixedContentMode((Integer) value); + } + break; + } case "hasNavigationDelegate": final boolean hasNavigationDelegate = (boolean) settings.get(key); @@ -416,6 +425,24 @@ private void updateJsMode(int mode) { } } + private void updateMixedContentMode(int mode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + switch (mode) { + case 0: + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + break; + case 1: + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); + break; + case 2: + webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); + break; + default: + throw new IllegalArgumentException("Trying to set unknown mixed content mode: " + mode); + } + } + } + private void updateAutoMediaPlaybackPolicy(int mode) { // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all // other values we require a user gesture. diff --git a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml index f895f92bd7a4..9c79d0f4fe1d 100644 --- a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ + android:name="io.flutter.app.FlutterApplication" + android:usesCleartextTraffic="true"> diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 20391f70d2ff..3023c2065083 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -542,6 +542,48 @@ void main() { }, skip: true /* https://github.com/flutter/flutter/issues/72572 */); }); + // NOTE: Requires `usesCleartextTraffic` in `AndroidManifest.xml` + testWidgets('set mixedContentMode', (WidgetTester tester) async { + // Only for Android + if (!Platform.isAndroid) return; + + void test(bool allow) async { + final Completer controllerCompleter = + Completer(); + final Completer loadCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'https://www.mixedcontentexamples.com/Test/NonSecureImage', + javascriptMode: JavascriptMode.unrestricted, + mixedContentMode: allow + ? MixedContentMode.compatibilityMode + : MixedContentMode.neverAllow, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (_) => loadCompleter.complete(null), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await loadCompleter.future; + final height = int.tryParse(await controller.evaluateJavascript( + 'document.getElementsByTagName("img")[0].height')); + if (allow) { + expect(height, 398); + } else { + expect(height != 398, true); + } + } + + await test(false); + await test(true); + }); + group('Audio playback policy', () { String audioTestBase64; setUpAll(() async { diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index e7e7981150ca..45248503666e 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -59,6 +59,7 @@ class _WebViewExampleState extends State { return WebView( initialUrl: 'https://flutter.dev', javascriptMode: JavascriptMode.unrestricted, + mixedContentMode: MixedContentMode.compatibilityMode, onWebViewCreated: (WebViewController webViewController) { _controller.complete(webViewController); }, diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index 16b529d7090e..283fdbbf22f1 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -394,6 +394,7 @@ class WebSettings { this.hasNavigationDelegate, this.hasProgressTracking, this.debuggingEnabled, + this.mixedContentMode, this.gestureNavigationEnabled, this.allowsInlineMediaPlayback, required this.userAgent, @@ -429,6 +430,11 @@ class WebSettings { /// See also [WebView.userAgent]. final WebSetting userAgent; + /// The mixed content mode to be used by the webview. + /// + /// See also: [WebView.mixedContentMode] + final MixedContentMode? mixedContentMode; + /// Whether to allow swipe based navigation in iOS. /// /// See also: [WebView.gestureNavigationEnabled] @@ -436,7 +442,7 @@ class WebSettings { @override String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; + return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback, mixedContentMode: $mixedContentMode)'; } } diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index e26604f74628..ef1999a72aca 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -188,6 +188,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); _addIfNonNull('hasProgressTracking', settings.hasProgressTracking); _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); + _addIfNonNull('mixedContentMode', settings.mixedContentMode?.index); _addIfNonNull( 'gestureNavigationEnabled', settings.gestureNavigationEnabled); _addIfNonNull( diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 56315b6692a5..0c496403eecb 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -130,6 +130,23 @@ class SurfaceAndroidWebView extends AndroidWebView { } } +/// Describes the state of mixed content(e.g. http content in https pages) support in a given web view. +/// +/// Only works in Android. +/// +/// See also https://developer.android.com/reference/android/webkit/WebSettings.html#MIXED_CONTENT_ALWAYS_ALLOW +/// for these 3 options. +enum MixedContentMode { + /// See also Android `MIXED_CONTENT_ALWAYS_ALLOW` option + alwaysAllow, + + /// See also Android `MIXED_CONTENT_NEVER_ALLOW` option + neverAllow, + + /// See also Android `MIXED_CONTENT_COMPATIBILITY_MODE` option + compatibilityMode, +} + /// Decides how to handle a specific navigation request. /// /// The returned [NavigationDecision] determines how the navigation described by @@ -228,6 +245,7 @@ class WebView extends StatefulWidget { this.debuggingEnabled = false, this.gestureNavigationEnabled = false, this.userAgent, + this.mixedContentMode, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, this.allowsInlineMediaPlayback = false, @@ -407,6 +425,14 @@ class WebView extends StatefulWidget { /// By default `userAgent` is null. final String? userAgent; + /// Controls whether mixed content is allowed to load. + /// + /// This only works on Android. + /// For iOS, you can change the settings in info.plist. + /// + /// By default `mixedContentMode` is null, and Android would use its own default. + final MixedContentMode? mixedContentMode; + /// Which restrictions apply on automatic media playback. /// /// This initial value is applied to the platform's webview upon creation. Any following @@ -488,6 +514,7 @@ WebSettings _webSettingsFromWidget(WebView widget) { hasNavigationDelegate: widget.navigationDelegate != null, hasProgressTracking: widget.onProgress != null, debuggingEnabled: widget.debuggingEnabled, + mixedContentMode: widget.mixedContentMode, gestureNavigationEnabled: widget.gestureNavigationEnabled, allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, userAgent: WebSetting.of(widget.userAgent), diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 6ee9e119bd3a..a89ded4e9014 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0-259.9.beta <3.0.0" diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 8ae6e625431d..2ba62246b2c2 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -844,6 +844,31 @@ void main() { }); }); + group('mixedContentMode', () { + testWidgets('set mixedContentMode', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + mixedContentMode: MixedContentMode.alwaysAllow, + )); + + final FakePlatformWebView? platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView?.mixedContentMode, + MixedContentMode.alwaysAllow.index); + }); + + testWidgets('defaults to null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final FakePlatformWebView? platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView?.mixedContentMode, null); + }); + + // no need to change + }); + group('Custom platform implementation', () { setUpAll(() { WebView.platform = MyWebViewPlatform(); @@ -939,6 +964,7 @@ class FakePlatformWebView { hasNavigationDelegate = params['settings']['hasNavigationDelegate'] ?? false; debuggingEnabled = params['settings']['debuggingEnabled']; + mixedContentMode = params['settings']['mixedContentMode']; userAgent = params['settings']['userAgent']; channel = MethodChannel( 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); @@ -958,6 +984,7 @@ class FakePlatformWebView { bool? hasNavigationDelegate; bool? debuggingEnabled; + int? mixedContentMode; String? userAgent; Future onMethodCall(MethodCall call) {