[webview_flutter] [url_launcher] Handle Multiwindows in WebViews (#2991)
diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 063aa99..d803ddf 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md
@@ -1,3 +1,7 @@ +## 5.7.0 + +* Handle WebView multi-window support. + ## 5.6.0 * Support Windows by default.
diff --git a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java index 5624d75..98c5613 100644 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java +++ b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java
@@ -1,5 +1,6 @@ package io.flutter.plugins.urllauncher; +import android.annotation.TargetApi; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; @@ -7,11 +8,14 @@ import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; +import android.os.Message; import android.provider.Browser; import android.view.KeyEvent; +import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import java.util.HashMap; import java.util.Map; @@ -67,6 +71,39 @@ private IntentFilter closeIntentFilter = new IntentFilter(ACTION_CLOSE); + // 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) { + final WebViewClient webViewClient = + new WebViewClient() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + webview.loadUrl(request.getUrl().toString()); + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + webview.loadUrl(url); + return true; + } + }; + + final WebView newWebView = new WebView(webview.getContext()); + newWebView.setWebViewClient(webViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -88,6 +125,10 @@ // Open new urls inside the webview itself. webview.setWebViewClient(webViewClient); + // Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679. + webview.getSettings().setSupportMultipleWindows(true); + webview.setWebChromeClient(new FlutterWebChromeClient()); + // Register receiver that may finish this Activity. registerReceiver(broadcastReceiver, closeIntentFilter); }
diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 965a431..a4061af 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml
@@ -2,7 +2,7 @@ description: Flutter plugin for launching a URL on Android and iOS. Supports web, phone, SMS, and email schemes. homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher -version: 5.6.0 +version: 5.7.0 flutter: plugin:
diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index baa485b..e1f4c3c 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@ +## 0.3.23 + +* Handle WebView multi-window support. + ## 0.3.22+2 * Update package:e2e reference to use the local version in the flutter/plugins
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 f9659d9..9bec8fa 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
@@ -9,9 +9,14 @@ import android.hardware.display.DisplayManager; import android.os.Build; import android.os.Handler; +import android.os.Message; import android.view.View; +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 io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -29,6 +34,46 @@ private final FlutterWebViewClient flutterWebViewClient; private final Handler platformThreadHandler; + // 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) { + final WebViewClient webViewClient = + new WebViewClient() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + final String url = request.getUrl().toString(); + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, request)) { + webView.loadUrl(url); + } + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, url)) { + webView.loadUrl(url); + } + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(webViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @SuppressWarnings("unchecked") FlutterWebView( @@ -50,6 +95,10 @@ 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);
diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 474435c..24926bf 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java
@@ -77,7 +77,7 @@ } @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { if (!hasNavigationDelegate) { return false; } @@ -97,7 +97,7 @@ return request.isForMainFrame(); } - private boolean shouldOverrideUrlLoading(WebView view, String url) { + boolean shouldOverrideUrlLoading(WebView view, String url) { if (!hasNavigationDelegate) { return false; }
diff --git a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart index 162ca2c..53fc991 100644 --- a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart +++ b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart
@@ -828,6 +828,108 @@ final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'about:blank'); }); + + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer<WebViewController> controllerCompleter = + Completer<WebViewController>(); + final Completer<void> pageLoaded = Completer<void>(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: 'https://flutter.dev', + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('window.open("https://www.google.com")'); + await pageLoaded.future; + expect(controller.currentUrl(), completion('https://www.google.com/')); + + await controller.goBack(); + expect(controller.currentUrl(), completion('https://www.flutter.dev')); + }, + skip: !Platform.isAndroid, + ); + + testWidgets( + 'javascript does not run in parent window', + (WidgetTester tester) async { + final String iframe = ''' + <!DOCTYPE html> + <script> + window.onload = () => { + window.open(`javascript: + var elem = document.createElement("p"); + elem.innerHTML = "<b>Executed JS in parent origin: " + window.location.origin + "</b>"; + document.body.append(elem); + `); + }; + </script> + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframe)); + + final String openWindowTest = ''' + <!DOCTYPE html> + <html> + <head> + <title>XSS test</title> + </head> + <body> + <iframe + onload="window.iframeLoaded = true;" + src="data:text/html;charset=utf-8;base64,$iframeTestBase64"></iframe> + </body> + </html> + '''; + final String openWindowTestBase64 = + base64Encode(const Utf8Encoder().convert(openWindowTest)); + final Completer<WebViewController> controllerCompleter = + Completer<WebViewController>(); + final Completer<void> pageLoadCompleter = Completer<void>(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + initialUrl: + 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', + onPageFinished: (String url) { + pageLoadCompleter.complete(); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoadCompleter.future; + + expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect( + controller.evaluateJavascript( + 'document.querySelector("p") && document.querySelector("p").textContent'), + completion('null'), + ); + }, + skip: !Platform.isAndroid, + ); } // JavaScript booleans evaluate to different string values on Android and iOS.
diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index ec81183..8a4a62e 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml
@@ -1,6 +1,6 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 0.3.22+2 +version: 0.3.23 homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter environment: