From 729bcbd21750fa2f96ab72f55e69d388ae309b4f Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 18 Feb 2019 15:41:53 -0800 Subject: [PATCH 01/11] [WebView] Allow specifying a navigation delegate. This allows the app to prevent specific navigations(e.g prevent navigating to specific URLs). https://github.com/flutter/flutter/issues/25329 --- .../webviewflutter/FlutterWebView.java | 7 ++ .../webviewflutter/FlutterWebViewClient.java | 59 ++++++++++ .../webview_flutter/example/lib/main.dart | 50 +++++++-- .../webview_flutter/lib/webview_flutter.dart | 104 ++++++++++++++++-- .../test/webview_flutter_test.dart | 95 ++++++++++++++-- 5 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java 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 44fe31fcf758..87960cb5975d 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 @@ -21,6 +21,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; private final WebView webView; private final MethodChannel methodChannel; + private final FlutterWebViewClient flutterWebViewClient; @SuppressWarnings("unchecked") FlutterWebView(Context context, BinaryMessenger messenger, int id, Map params) { @@ -31,12 +32,15 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); methodChannel.setMethodCallHandler(this); + flutterWebViewClient = new FlutterWebViewClient(methodChannel); applySettings((Map) params.get("settings")); if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { registerJavaScriptChannelNames((List) params.get(JS_CHANNEL_NAMES_FIELD)); } + webView.setWebViewClient(flutterWebViewClient); + if (params.containsKey("initialUrl")) { String url = (String) params.get("initialUrl"); webView.loadUrl(url); @@ -178,6 +182,9 @@ private void applySettings(Map settings) { case "jsMode": updateJsMode((Integer) settings.get(key)); break; + case "hasNavigationDelegate": + flutterWebViewClient.setHasNavigationDelegate((boolean) settings.get(key)); + break; default: throw new IllegalArgumentException("Unknown WebView setting: " + key); } 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 new file mode 100644 index 000000000000..d2d41ab9743b --- /dev/null +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -0,0 +1,59 @@ +package io.flutter.plugins.webviewflutter; + +import android.annotation.TargetApi; +import android.os.Build; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; + +class FlutterWebViewClient extends WebViewClient { + private final MethodChannel methodChannel; + private boolean hasNavigationDelegate; + + FlutterWebViewClient(MethodChannel methodChannel) { + this.methodChannel = methodChannel; + } + + void setHasNavigationDelegate(boolean hasNavigationDelegate) { + this.hasNavigationDelegate = hasNavigationDelegate; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (!hasNavigationDelegate) { + return super.shouldOverrideUrlLoading(view, request); + } + notifyOnNavigationRequest(request.getUrl().toString(), request.isForMainFrame()); + // We must make a synchronous decision here whether to allow the navigation or not, + // if there Dart code has set a navigation delegate we want that delegate to decide whether + // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we + // return true here to block the navigation, if the Dart delegate decides to allow the + // navigation the plugin will later make an addition loadUrl call for this url. + // + // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop + // navigations that originated in the main frame, if the request is not for the main frame + // we just return false to allow the navigation. + // + // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 + return request.isForMainFrame(); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!hasNavigationDelegate) { + return super.shouldOverrideUrlLoading(view, url); + } + notifyOnNavigationRequest(url, true); + return true; + } + + private void notifyOnNavigationRequest(String url, boolean isMainFrame) { + HashMap args = new HashMap<>(); + args.put("url", url); + args.put("isMainFrame", isMainFrame); + methodChannel.invokeMethod("navigationRequest", args); + } +} diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index f688b602a359..0d6774e73d5c 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -3,11 +3,26 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() => runApp(MaterialApp(home: WebViewExample())); +const String kNavigationExamplePage = ''' + + +

+The navigation delegate is set to block navigation to the youtube website. +

+ + + +'''; + class WebViewExample extends StatelessWidget { final Completer _controller = Completer(); @@ -37,6 +52,14 @@ class WebViewExample extends StatelessWidget { javascriptChannels: [ _toasterJavascriptChannel(context), ].toSet(), + navigationDelegate: (NavigationRequest request) { + if (request.url == 'https://www.youtube.com/') { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, ); }), floatingActionButton: favoriteButton(), @@ -76,12 +99,12 @@ class WebViewExample extends StatelessWidget { enum MenuOptions { showUserAgent, - toast, listCookies, clearCookies, addToCache, listCache, clearCache, + navigationDelegate, } class SampleMenu extends StatelessWidget { @@ -102,13 +125,6 @@ class SampleMenu extends StatelessWidget { case MenuOptions.showUserAgent: _onShowUserAgent(controller.data, context); break; - case MenuOptions.toast: - Scaffold.of(context).showSnackBar( - SnackBar( - content: Text('You selected: $value'), - ), - ); - break; case MenuOptions.listCookies: _onListCookies(controller.data, context); break; @@ -124,6 +140,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.clearCache: _onClearCache(controller.data, context); break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -132,10 +151,6 @@ class SampleMenu extends StatelessWidget { child: const Text('Show user agent'), enabled: controller.hasData, ), - const PopupMenuItem( - value: MenuOptions.toast, - child: Text('Make a toast'), - ), const PopupMenuItem( value: MenuOptions.listCookies, child: Text('List cookies'), @@ -156,6 +171,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.clearCache, child: Text('Clear cache'), ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), ], ); }, @@ -218,6 +237,13 @@ class SampleMenu extends StatelessWidget { )); } + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + controller.loadUrl('data:text/html;base64,$contentBase64'); + } + Widget _getCookieList(String cookies) { if (cookies == null || cookies == '""') { return Container(); diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 648f75a33f84..009069f4ac1f 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -33,6 +33,39 @@ class JavascriptMessage { /// Callback type for handling messages sent from Javascript running in a web view. typedef void JavascriptMessageHandler(JavascriptMessage message); +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest({this.url, this.isMainFrame}); + + /// The URL a navigation is requested to. + final String url; + + /// Whether the navigation request originated from the main HTML frame. + final bool isMainFrame; + + @override + String toString() { + return 'NavigationRequest(url: $url, isMainFrame: $isMainFrame)'; + } +} + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef NavigationDecision NavigationDelegate(NavigationRequest navigation); + final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$'); /// A named channel for receiving messaged from JavaScript code running inside a web view. @@ -78,6 +111,7 @@ class WebView extends StatefulWidget { this.initialUrl, this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, + this.navigationDelegate, this.gestureRecognizers, }) : assert(javascriptMode != null), super(key: key); @@ -131,6 +165,20 @@ class WebView extends StatefulWidget { /// A null value is equivalent to an empty set. final Set javascriptChannels; + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// On Android only navigation actions originated by the main frame can be intercepted, + /// navigation actions originating from subframes are allowed regardless of the value + /// returned by this delegate. + final NavigationDelegate navigationDelegate; + @override State createState() => _WebViewState(); } @@ -196,6 +244,7 @@ class _WebViewState extends State { final WebViewController controller = await _controller.future; controller._updateSettings(settings); controller._updateJavascriptChannels(widget.javascriptChannels); + controller._navigationDelegate = widget.navigationDelegate; } void _onPlatformViewCreated(int id) { @@ -203,6 +252,7 @@ class _WebViewState extends State { id, _WebSettings.fromWidget(widget), widget.javascriptChannels, + widget.navigationDelegate, ); _controller.complete(controller); if (widget.onWebViewCreated != null) { @@ -260,27 +310,35 @@ class _CreationParams { class _WebSettings { _WebSettings({ this.javascriptMode, + this.hasNavigationDelegate, }); static _WebSettings fromWidget(WebView widget) { - return _WebSettings(javascriptMode: widget.javascriptMode); + return _WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + ); } final JavascriptMode javascriptMode; + final bool hasNavigationDelegate; Map toMap() { return { 'jsMode': javascriptMode.index, + 'hasNavigationDelegate': hasNavigationDelegate, }; } Map updatesMap(_WebSettings newSettings) { - if (javascriptMode == newSettings.javascriptMode) { - return null; + final Map updates = {}; + if (javascriptMode != newSettings.javascriptMode) { + updates['jsMode'] = newSettings.javascriptMode.index; } - return { - 'jsMode': newSettings.javascriptMode.index, - }; + if (hasNavigationDelegate != newSettings.hasNavigationDelegate) { + updates['hasNavigationDelegate'] = newSettings.hasNavigationDelegate; + } + return updates; } } @@ -290,14 +348,19 @@ class _WebSettings { /// callback for a [WebView] widget. class WebViewController { WebViewController._( - int id, this._settings, Set javascriptChannels) - : _channel = MethodChannel('plugins.flutter.io/webview_$id') { + int id, + this._settings, + Set javascriptChannels, + this._navigationDelegate, + ) : _channel = MethodChannel('plugins.flutter.io/webview_$id') { _updateJavascriptChannelsFromSet(javascriptChannels); _channel.setMethodCallHandler(_onMethodCall); } final MethodChannel _channel; + NavigationDelegate _navigationDelegate; + _WebSettings _settings; // Maps a channel name to a channel. @@ -312,6 +375,29 @@ class WebViewController { _javascriptChannels[channel] .onMessageReceived(JavascriptMessage(message)); break; + case 'navigationRequest': + final NavigationRequest request = NavigationRequest( + url: call.arguments['url'], + isMainFrame: call.arguments['isMainFrame'], + ); + + // _navigationDelegate can be null if the widget was rebuilt with no + // navigation // delegate after a navigation happened and just before we + // got the navigationRequest message. + if (_navigationDelegate == null || + _navigationDelegate(request) == NavigationDecision.navigate) { + _completeNavigation(request, call.arguments['requestId']); + } + break; + } + } + + void _completeNavigation(NavigationRequest request, int requestId) { + if (defaultTargetPlatform == TargetPlatform.android) { + if (request.isMainFrame) { + loadUrl(request.url); + return; + } } } @@ -416,7 +502,7 @@ class WebViewController { Future _updateSettings(_WebSettings setting) async { final Map updateMap = _settings.updatesMap(setting); - if (updateMap == null) { + if (updateMap == null || updateMap.isEmpty) { return null; } _settings = setting; diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 4fcdc7f4e015..7e8244eaf6e0 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -575,6 +575,56 @@ void main() { expect(ttsMessagesReceived, ['Hello', 'World']); }); + + group('navigationDelegate', () { + testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.hasNavigationDelegate, false); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest r) => null, + )); + + expect(platformWebView.hasNavigationDelegate, true); + }); + + testWidgets('Block navigation', (WidgetTester tester) async { + final List navigationRequests = []; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest request) { + navigationRequests.add(request); + // Only allow navigating to https://flutter.dev + return request.url == 'https://flutter.dev' + ? NavigationDecision.navigate + : NavigationDecision.prevent; + })); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.hasNavigationDelegate, true); + + platformWebView.fakeNavigate('https://www.google.com'); + // The navigation delegate only allows navigation to https://flutter.dev + // so we should still be in https://youtube.com. + expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(navigationRequests.length, 1); + expect(navigationRequests[0].url, 'https://www.google.com'); + expect(navigationRequests[0].isMainFrame, true); + + platformWebView.fakeNavigate('https://flutter.dev'); + expect(platformWebView.currentUrl, 'https://flutter.dev'); + }); + }); } class FakePlatformWebView { @@ -585,12 +635,14 @@ class FakePlatformWebView { history.add(initialUrl); currentPosition++; } - javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; } if (params.containsKey('javascriptChannelNames')) { javascriptChannelNames = List.from(params['javascriptChannelNames']); } + javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; + hasNavigationDelegate = + params['settings']['hasNavigationDelegate'] ?? false; channel = MethodChannel( 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); channel.setMockMethodCallHandler(onMethodCall); @@ -607,20 +659,21 @@ class FakePlatformWebView { JavascriptMode javascriptMode; List javascriptChannelNames; + bool hasNavigationDelegate; + Future onMethodCall(MethodCall call) { switch (call.method) { case 'loadUrl': final String url = call.arguments; - history = history.sublist(0, currentPosition + 1); - history.add(url); - currentPosition++; - amountOfReloadsOnCurrentUrl = 0; + _loadUrl(url); return Future.sync(() {}); case 'updateSettings': - if (call.arguments['jsMode'] == null) { - break; + if (call.arguments['jsMode'] != null) { + javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; + } + if (call.arguments['hasNavigationDelegate'] != null) { + hasNavigationDelegate = call.arguments['hasNavigationDelegate']; } - javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; break; case 'canGoBack': return Future.sync(() => currentPosition > 0); @@ -672,6 +725,32 @@ class FakePlatformWebView { BinaryMessages.handlePlatformMessage( channel.name, data, (ByteData data) {}); } + + // Fakes a main frame navigation that was initiated by the webview, e.g when + // the user clicks a link in the currently loaded page. + void fakeNavigate(String url) { + if (!hasNavigationDelegate) { + print('no navigation delegate'); + _loadUrl(url); + return; + } + final StandardMethodCodec codec = const StandardMethodCodec(); + final Map arguments = { + 'url': url, + 'isMainFrame': true + }; + final ByteData data = + codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); + BinaryMessages.handlePlatformMessage( + channel.name, data, (ByteData data) {}); + } + + void _loadUrl(String url) { + history = history.sublist(0, currentPosition + 1); + history.add(url); + currentPosition++; + amountOfReloadsOnCurrentUrl = 0; + } } class _FakePlatformViewsController { From 8ac15be612a09c774c60efc2ca1130e67007c5a6 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 27 Feb 2019 11:44:13 -0800 Subject: [PATCH 02/11] review comments follow-up --- .../plugins/webviewflutter/FlutterWebViewClient.java | 4 ++++ packages/webview_flutter/example/lib/main.dart | 3 ++- packages/webview_flutter/lib/webview_flutter.dart | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) 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 d2d41ab9743b..e7d748ef20e3 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 @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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.annotation.TargetApi; diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 0d6774e73d5c..9aa95bba9c21 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -11,6 +11,7 @@ void main() => runApp(MaterialApp(home: WebViewExample())); const String kNavigationExamplePage = ''' +Navigation Delegate Examplethanks

The navigation delegate is set to block navigation to the youtube website. @@ -53,7 +54,7 @@ class WebViewExample extends StatelessWidget { _toasterJavascriptChannel(context), ].toSet(), navigationDelegate: (NavigationRequest request) { - if (request.url == 'https://www.youtube.com/') { + if (request.url.startsWith('https://www.youtube.com/')) { print('blocking navigation to $request}'); return NavigationDecision.prevent; } diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 009069f4ac1f..b87bf49e153f 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -35,9 +35,9 @@ typedef void JavascriptMessageHandler(JavascriptMessage message); /// Information about a navigation action that is about to be executed. class NavigationRequest { - NavigationRequest({this.url, this.isMainFrame}); + NavigationRequest._({this.url, this.isMainFrame}); - /// The URL a navigation is requested to. + /// The URL that will be loaded if the navigation is executed. final String url; /// Whether the navigation request originated from the main HTML frame. @@ -45,7 +45,7 @@ class NavigationRequest { @override String toString() { - return 'NavigationRequest(url: $url, isMainFrame: $isMainFrame)'; + return '$runtimeType(url: $url, isMainFrame: $isMainFrame)'; } } @@ -376,7 +376,7 @@ class WebViewController { .onMessageReceived(JavascriptMessage(message)); break; case 'navigationRequest': - final NavigationRequest request = NavigationRequest( + final NavigationRequest request = NavigationRequest._( url: call.arguments['url'], isMainFrame: call.arguments['isMainFrame'], ); From 8e2a9e946ce3d69e333a6a94efb6561879d79e1a Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 6 Mar 2019 12:48:09 -0800 Subject: [PATCH 03/11] remove stray thanks --- packages/webview_flutter/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 9aa95bba9c21..7d6ce10a56ad 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -11,7 +11,7 @@ void main() => runApp(MaterialApp(home: WebViewExample())); const String kNavigationExamplePage = ''' -Navigation Delegate Examplethanks +Navigation Delegate Example

The navigation delegate is set to block navigation to the youtube website. From 46d8bf6439d01b17788c4df6748b750743e1f39a Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 6 Mar 2019 13:35:55 -0800 Subject: [PATCH 04/11] retain HTTP headers when possible when using a navigation delegate --- .../webviewflutter/FlutterWebViewClient.java | 62 ++++++++++++++-- .../webview_flutter/lib/webview_flutter.dart | 70 +++++++------------ 2 files changed, 82 insertions(+), 50 deletions(-) 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 e7d748ef20e3..cee6be013bb5 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 @@ -11,6 +11,7 @@ import android.webkit.WebViewClient; import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; +import java.util.Map; class FlutterWebViewClient extends WebViewClient { private final MethodChannel methodChannel; @@ -30,9 +31,14 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request if (!hasNavigationDelegate) { return super.shouldOverrideUrlLoading(view, request); } - notifyOnNavigationRequest(request.getUrl().toString(), request.isForMainFrame()); + notifyOnNavigationRequest( + request.getUrl().toString(), + request.getRequestHeaders(), + view, + request.isForMainFrame() + ); // We must make a synchronous decision here whether to allow the navigation or not, - // if there Dart code has set a navigation delegate we want that delegate to decide whether + // if the Dart code has set a navigation delegate we want that delegate to decide whether // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we // return true here to block the navigation, if the Dart delegate decides to allow the // navigation the plugin will later make an addition loadUrl call for this url. @@ -50,14 +56,60 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!hasNavigationDelegate) { return super.shouldOverrideUrlLoading(view, url); } - notifyOnNavigationRequest(url, true); + notifyOnNavigationRequest(url, null, view, true); return true; } - private void notifyOnNavigationRequest(String url, boolean isMainFrame) { + private void notifyOnNavigationRequest(String url, Map headers, WebView webview, boolean isMainFrame) { HashMap args = new HashMap<>(); args.put("url", url); args.put("isMainFrame", isMainFrame); - methodChannel.invokeMethod("navigationRequest", args); + if (isMainFrame) { + methodChannel.invokeMethod( + "navigationRequest", + args, + new OnNavigationRequestResult(url, headers, webview) + ); + } else { + methodChannel.invokeMethod( "navigationRequest", args); + } + } + + private static class OnNavigationRequestResult implements MethodChannel.Result{ + private final String url; + private final Map headers; + private final WebView webView; + + private OnNavigationRequestResult(String url, Map headers, WebView webView) { + this.url = url; + this.headers = headers; + this.webView = webView; + } + + @Override + public void success(Object shouldLoad) { + Boolean typedShouldLoad = (Boolean) shouldLoad; + if (typedShouldLoad) { + loadUrl(); + } + } + + @Override + public void error(String errorCode, String s1, Object o) { + throw new IllegalStateException("navigation "); + } + + @Override + public void notImplemented() { + throw new IllegalStateException("navigationRequest must be implemented by the webview method channel"); + } + + private void loadUrl() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webView.loadUrl(url, headers); + } else { + webView.loadUrl(url); + } + } } } diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index b87bf49e153f..26db2c25ffec 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -177,6 +177,9 @@ class WebView extends StatefulWidget { /// On Android only navigation actions originated by the main frame can be intercepted, /// navigation actions originating from subframes are allowed regardless of the value /// returned by this delegate. + /// + /// On Android API levels smaller than 21, when a navigationDelegate is set, HTTP requests do not + /// include the HTTP referer header. final NavigationDelegate navigationDelegate; @override @@ -184,8 +187,7 @@ class WebView extends StatefulWidget { } class _WebViewState extends State { - final Completer _controller = - Completer(); + final Completer _controller = Completer(); _WebSettings _settings; @@ -222,8 +224,7 @@ class _WebViewState extends State { creationParamsCodec: const StandardMessageCodec(), ); } - return Text( - '$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); + return Text('$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); } @override @@ -261,12 +262,11 @@ class _WebViewState extends State { } void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || - widget.javascriptChannels.isEmpty) { + if (widget.javascriptChannels == null || widget.javascriptChannels.isEmpty) { return; } - assert(_extractChannelNames(widget.javascriptChannels).length == - widget.javascriptChannels.length); + assert( + _extractChannelNames(widget.javascriptChannels).length == widget.javascriptChannels.length); } } @@ -280,15 +280,13 @@ Set _extractChannelNames(Set channels) { } class _CreationParams { - _CreationParams( - {this.initialUrl, this.settings, this.javascriptChannelNames}); + _CreationParams({this.initialUrl, this.settings, this.javascriptChannelNames}); static _CreationParams fromWidget(WebView widget) { return _CreationParams( initialUrl: widget.initialUrl, settings: _WebSettings.fromWidget(widget), - javascriptChannelNames: - _extractChannelNames(widget.javascriptChannels).toList(), + javascriptChannelNames: _extractChannelNames(widget.javascriptChannels).toList(), ); } @@ -364,17 +362,15 @@ class WebViewController { _WebSettings _settings; // Maps a channel name to a channel. - Map _javascriptChannels = - {}; + Map _javascriptChannels = {}; - Future _onMethodCall(MethodCall call) async { + Future _onMethodCall(MethodCall call) async { switch (call.method) { case 'javascriptChannelMessage': final String channel = call.arguments['channel']; final String message = call.arguments['message']; - _javascriptChannels[channel] - .onMessageReceived(JavascriptMessage(message)); - break; + _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); + return true; case 'navigationRequest': final NavigationRequest request = NavigationRequest._( url: call.arguments['url'], @@ -382,23 +378,13 @@ class WebViewController { ); // _navigationDelegate can be null if the widget was rebuilt with no - // navigation // delegate after a navigation happened and just before we + // navigation delegate after a navigation happened and just before we // got the navigationRequest message. - if (_navigationDelegate == null || - _navigationDelegate(request) == NavigationDecision.navigate) { - _completeNavigation(request, call.arguments['requestId']); - } - break; - } - } - - void _completeNavigation(NavigationRequest request, int requestId) { - if (defaultTargetPlatform == TargetPlatform.android) { - if (request.isMainFrame) { - loadUrl(request.url); - return; - } + final bool allowNavigation = _navigationDelegate == null || + _navigationDelegate(request) == NavigationDecision.navigate; + return allowNavigation; } + throw MissingPluginException('${call.method} was invoked but has no handler'); } /// Loads the specified URL. @@ -512,20 +498,16 @@ class WebViewController { return _channel.invokeMethod('updateSettings', updateMap); } - Future _updateJavascriptChannels( - Set newChannels) async { + Future _updateJavascriptChannels(Set newChannels) async { final Set currentChannels = _javascriptChannels.keys.toSet(); final Set newChannelNames = _extractChannelNames(newChannels); - final Set channelsToAdd = - newChannelNames.difference(currentChannels); - final Set channelsToRemove = - currentChannels.difference(newChannelNames); + final Set channelsToAdd = newChannelNames.difference(currentChannels); + final Set channelsToRemove = currentChannels.difference(newChannelNames); if (channelsToRemove.isNotEmpty) { // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method - _channel.invokeMethod( - 'removeJavascriptChannels', channelsToRemove.toList()); + _channel.invokeMethod('removeJavascriptChannels', channelsToRemove.toList()); } if (channelsToAdd.isNotEmpty) { // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. @@ -569,8 +551,7 @@ class WebViewController { // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method - final String result = - await _channel.invokeMethod('evaluateJavascript', javascriptString); + final String result = await _channel.invokeMethod('evaluateJavascript', javascriptString); return result; } } @@ -584,8 +565,7 @@ class CookieManager { CookieManager._(); - static const MethodChannel _channel = - MethodChannel('plugins.flutter.io/cookie_manager'); + static const MethodChannel _channel = MethodChannel('plugins.flutter.io/cookie_manager'); static CookieManager _instance; /// Clears all cookies. From 8d9101cb0f01fd5f41f031ef47d91548920952d8 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 6 Mar 2019 13:44:11 -0800 Subject: [PATCH 05/11] add more documentation for caveats on Android --- .../webview_flutter/lib/webview_flutter.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 26db2c25ffec..e2643418d2e7 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -174,12 +174,16 @@ class WebView extends StatefulWidget { /// /// When null all navigation actions are allowed. /// - /// On Android only navigation actions originated by the main frame can be intercepted, - /// navigation actions originating from subframes are allowed regardless of the value - /// returned by this delegate. - /// - /// On Android API levels smaller than 21, when a navigationDelegate is set, HTTP requests do not - /// include the HTTP referer header. + /// Caveats on Android: + /// + /// * Navigation actions originated by the main frame can be intercepted, + /// navigation actions originating from subframes are allowed regardless of the value + /// returned by this delegate. + /// * On API levels smaller than 21, when a navigationDelegate is set, HTTP requests do not + /// include the HTTP referer header. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. final NavigationDelegate navigationDelegate; @override From a743f8db09823b7b96939e4a12750fc46b777e27 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 7 Mar 2019 11:53:49 -0800 Subject: [PATCH 06/11] format --- .../webviewflutter/FlutterWebViewClient.java | 23 ++++------ .../webview_flutter/lib/webview_flutter.dart | 46 ++++++++++++------- 2 files changed, 39 insertions(+), 30 deletions(-) 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 cee6be013bb5..7c1923869aa5 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 @@ -32,11 +32,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request return super.shouldOverrideUrlLoading(view, request); } notifyOnNavigationRequest( - request.getUrl().toString(), - request.getRequestHeaders(), - view, - request.isForMainFrame() - ); + request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); // We must make a synchronous decision here whether to allow the navigation or not, // if the Dart code has set a navigation delegate we want that delegate to decide whether // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we @@ -60,22 +56,20 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } - private void notifyOnNavigationRequest(String url, Map headers, WebView webview, boolean isMainFrame) { + private void notifyOnNavigationRequest( + String url, Map headers, WebView webview, boolean isMainFrame) { HashMap args = new HashMap<>(); args.put("url", url); args.put("isMainFrame", isMainFrame); if (isMainFrame) { methodChannel.invokeMethod( - "navigationRequest", - args, - new OnNavigationRequestResult(url, headers, webview) - ); + "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); } else { - methodChannel.invokeMethod( "navigationRequest", args); + methodChannel.invokeMethod("navigationRequest", args); } } - private static class OnNavigationRequestResult implements MethodChannel.Result{ + private static class OnNavigationRequestResult implements MethodChannel.Result { private final String url; private final Map headers; private final WebView webView; @@ -96,12 +90,13 @@ public void success(Object shouldLoad) { @Override public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigation "); + throw new IllegalStateException("navigation "); } @Override public void notImplemented() { - throw new IllegalStateException("navigationRequest must be implemented by the webview method channel"); + throw new IllegalStateException( + "navigationRequest must be implemented by the webview method channel"); } private void loadUrl() { diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index e2643418d2e7..141915e42db8 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -191,7 +191,8 @@ class WebView extends StatefulWidget { } class _WebViewState extends State { - final Completer _controller = Completer(); + final Completer _controller = + Completer(); _WebSettings _settings; @@ -228,7 +229,8 @@ class _WebViewState extends State { creationParamsCodec: const StandardMessageCodec(), ); } - return Text('$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); + return Text( + '$defaultTargetPlatform is not yet supported by the webview_flutter plugin'); } @override @@ -266,11 +268,12 @@ class _WebViewState extends State { } void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || widget.javascriptChannels.isEmpty) { + if (widget.javascriptChannels == null || + widget.javascriptChannels.isEmpty) { return; } - assert( - _extractChannelNames(widget.javascriptChannels).length == widget.javascriptChannels.length); + assert(_extractChannelNames(widget.javascriptChannels).length == + widget.javascriptChannels.length); } } @@ -284,13 +287,15 @@ Set _extractChannelNames(Set channels) { } class _CreationParams { - _CreationParams({this.initialUrl, this.settings, this.javascriptChannelNames}); + _CreationParams( + {this.initialUrl, this.settings, this.javascriptChannelNames}); static _CreationParams fromWidget(WebView widget) { return _CreationParams( initialUrl: widget.initialUrl, settings: _WebSettings.fromWidget(widget), - javascriptChannelNames: _extractChannelNames(widget.javascriptChannels).toList(), + javascriptChannelNames: + _extractChannelNames(widget.javascriptChannels).toList(), ); } @@ -366,14 +371,16 @@ class WebViewController { _WebSettings _settings; // Maps a channel name to a channel. - Map _javascriptChannels = {}; + Map _javascriptChannels = + {}; Future _onMethodCall(MethodCall call) async { switch (call.method) { case 'javascriptChannelMessage': final String channel = call.arguments['channel']; final String message = call.arguments['message']; - _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); + _javascriptChannels[channel] + .onMessageReceived(JavascriptMessage(message)); return true; case 'navigationRequest': final NavigationRequest request = NavigationRequest._( @@ -388,7 +395,8 @@ class WebViewController { _navigationDelegate(request) == NavigationDecision.navigate; return allowNavigation; } - throw MissingPluginException('${call.method} was invoked but has no handler'); + throw MissingPluginException( + '${call.method} was invoked but has no handler'); } /// Loads the specified URL. @@ -502,16 +510,20 @@ class WebViewController { return _channel.invokeMethod('updateSettings', updateMap); } - Future _updateJavascriptChannels(Set newChannels) async { + Future _updateJavascriptChannels( + Set newChannels) async { final Set currentChannels = _javascriptChannels.keys.toSet(); final Set newChannelNames = _extractChannelNames(newChannels); - final Set channelsToAdd = newChannelNames.difference(currentChannels); - final Set channelsToRemove = currentChannels.difference(newChannelNames); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); if (channelsToRemove.isNotEmpty) { // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method - _channel.invokeMethod('removeJavascriptChannels', channelsToRemove.toList()); + _channel.invokeMethod( + 'removeJavascriptChannels', channelsToRemove.toList()); } if (channelsToAdd.isNotEmpty) { // TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter. @@ -555,7 +567,8 @@ class WebViewController { // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method - final String result = await _channel.invokeMethod('evaluateJavascript', javascriptString); + final String result = + await _channel.invokeMethod('evaluateJavascript', javascriptString); return result; } } @@ -569,7 +582,8 @@ class CookieManager { CookieManager._(); - static const MethodChannel _channel = MethodChannel('plugins.flutter.io/cookie_manager'); + static const MethodChannel _channel = + MethodChannel('plugins.flutter.io/cookie_manager'); static CookieManager _instance; /// Clears all cookies. From 6df13d9b3fee402fa74cd738dc53a60e8480eee8 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 8 Mar 2019 15:17:31 -0800 Subject: [PATCH 07/11] use WebViewClientCompat, rename isMainFrame to isForMainFrame --- packages/webview_flutter/android/build.gradle | 4 ++++ .../webviewflutter/FlutterWebViewClient.java | 22 +++++++++++++++---- .../example/android/gradle.properties | 1 + .../webview_flutter/lib/webview_flutter.dart | 10 ++++----- .../test/webview_flutter_test.dart | 13 +++++++---- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/webview_flutter/android/build.gradle b/packages/webview_flutter/android/build.gradle index 098c2f4bf388..45ab74d37938 100644 --- a/packages/webview_flutter/android/build.gradle +++ b/packages/webview_flutter/android/build.gradle @@ -44,4 +44,8 @@ android { lintOptions { disable 'InvalidPackage' } + + dependencies { + implementation 'androidx.webkit:webkit:1.0.0' + } } 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 7c1923869aa5..7634511fe1ab 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 @@ -6,14 +6,20 @@ import android.annotation.TargetApi; import android.os.Build; +import android.util.Log; import android.webkit.WebResourceRequest; import android.webkit.WebView; -import android.webkit.WebViewClient; +import androidx.webkit.WebViewClientCompat; import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; import java.util.Map; -class FlutterWebViewClient extends WebViewClient { +// We need to use WebViewClientCompat to get +// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) +// invoked by the webview on older Android devices, without it pages that use iframes will +// be broken when a navigationDelegate is set on Android version earlier than N. +class FlutterWebViewClient extends WebViewClientCompat { + private static final String TAG = "FlutterWebViewClient"; private final MethodChannel methodChannel; private boolean hasNavigationDelegate; @@ -40,7 +46,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request // navigation the plugin will later make an addition loadUrl call for this url. // // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that originated in the main frame, if the request is not for the main frame + // navigations that target the main frame, if the request is not for the main frame // we just return false to allow the navigation. // // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 @@ -52,6 +58,14 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!hasNavigationDelegate) { return super.shouldOverrideUrlLoading(view, url); } + // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with + // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). + // On these devices we cannot tell whether the navigation is targeted to the main frame or not. + // We proceed assuming that the navigation is targeted to the main frame. If the page had any + // frames they will be loaded in the main frame instead. + Log.w( + TAG, + "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); notifyOnNavigationRequest(url, null, view, true); return true; } @@ -60,7 +74,7 @@ private void notifyOnNavigationRequest( String url, Map headers, WebView webview, boolean isMainFrame) { HashMap args = new HashMap<>(); args.put("url", url); - args.put("isMainFrame", isMainFrame); + args.put("isForMainFrame", isMainFrame); if (isMainFrame) { methodChannel.invokeMethod( "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); diff --git a/packages/webview_flutter/example/android/gradle.properties b/packages/webview_flutter/example/android/gradle.properties index 8bd86f680510..d2993e56b839 100644 --- a/packages/webview_flutter/example/android/gradle.properties +++ b/packages/webview_flutter/example/android/gradle.properties @@ -1 +1,2 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroixX=true \ No newline at end of file diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 141915e42db8..a2ae4a2912c5 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -35,17 +35,17 @@ typedef void JavascriptMessageHandler(JavascriptMessage message); /// Information about a navigation action that is about to be executed. class NavigationRequest { - NavigationRequest._({this.url, this.isMainFrame}); + NavigationRequest._({this.url, this.isForMainFrame}); /// The URL that will be loaded if the navigation is executed. final String url; - /// Whether the navigation request originated from the main HTML frame. - final bool isMainFrame; + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; @override String toString() { - return '$runtimeType(url: $url, isMainFrame: $isMainFrame)'; + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; } } @@ -385,7 +385,7 @@ class WebViewController { case 'navigationRequest': final NavigationRequest request = NavigationRequest._( url: call.arguments['url'], - isMainFrame: call.arguments['isMainFrame'], + isForMainFrame: call.arguments['isForMainFrame'], ); // _navigationDelegate can be null if the widget was rebuilt with no diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 7e8244eaf6e0..8635875ab7a6 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -619,9 +619,10 @@ void main() { expect(platformWebView.currentUrl, 'https://youtube.com'); expect(navigationRequests.length, 1); expect(navigationRequests[0].url, 'https://www.google.com'); - expect(navigationRequests[0].isMainFrame, true); + expect(navigationRequests[0].isForMainFrame, true); platformWebView.fakeNavigate('https://flutter.dev'); + await tester.pump(); expect(platformWebView.currentUrl, 'https://flutter.dev'); }); }); @@ -737,12 +738,16 @@ class FakePlatformWebView { final StandardMethodCodec codec = const StandardMethodCodec(); final Map arguments = { 'url': url, - 'isMainFrame': true + 'isForMainFrame': true }; final ByteData data = codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); - BinaryMessages.handlePlatformMessage( - channel.name, data, (ByteData data) {}); + BinaryMessages.handlePlatformMessage(channel.name, data, (ByteData data) { + final bool allow = codec.decodeEnvelope(data); + if (allow) { + _loadUrl(url); + } + }); } void _loadUrl(String url) { From d9d671ee263bf1e92d2fcbf0ca15bad1ecdbd702 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 8 Mar 2019 16:06:44 -0800 Subject: [PATCH 08/11] typo fix, update documentation --- .../webview_flutter/example/android/gradle.properties | 2 +- packages/webview_flutter/lib/webview_flutter.dart | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/webview_flutter/example/android/gradle.properties b/packages/webview_flutter/example/android/gradle.properties index d2993e56b839..ad8917e962e5 100644 --- a/packages/webview_flutter/example/android/gradle.properties +++ b/packages/webview_flutter/example/android/gradle.properties @@ -1,2 +1,2 @@ org.gradle.jvmargs=-Xmx1536M -android.useAndroixX=true \ No newline at end of file +android.useAndroidX=true \ No newline at end of file diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index a2ae4a2912c5..a1d8eb70363e 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -176,14 +176,17 @@ class WebView extends StatefulWidget { /// /// Caveats on Android: /// - /// * Navigation actions originated by the main frame can be intercepted, - /// navigation actions originating from subframes are allowed regardless of the value + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value /// returned by this delegate. - /// * On API levels smaller than 21, when a navigationDelegate is set, HTTP requests do not - /// include the HTTP referer header. /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were /// triggered by a user gesture, this disables some of Chromium's security mechanisms. /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. final NavigationDelegate navigationDelegate; @override From e3eec87d9b0738944c6886a30f31763c8afb39de Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 8 Mar 2019 16:07:11 -0800 Subject: [PATCH 09/11] bump version, update changelog --- packages/webview_flutter/CHANGELOG.md | 4 ++++ packages/webview_flutter/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 74c8f4d340f4..faac49dfebf5 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.4 + +* Support specifying navigation delegates that can prevent navigations from being executed. + ## 0.3.3+1 * Fixed a memory leak on Android - the WebView was not properly disposed. diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index de8600f8419a..84484a2bcbec 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.3+1 +version: 0.3.4 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter From 530d024db48d04b2f09927b946036a1e9f9c0997 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 8 Mar 2019 16:33:47 -0800 Subject: [PATCH 10/11] don't call super.shouldOverrideUrlLoading --- .../io/flutter/plugins/webviewflutter/FlutterWebView.java | 3 +++ .../flutter/plugins/webviewflutter/FlutterWebViewClient.java | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 87960cb5975d..070ba74a9669 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 @@ -4,7 +4,9 @@ package io.flutter.plugins.webviewflutter; +import android.annotation.TargetApi; import android.content.Context; +import android.os.Build; import android.view.View; import android.webkit.WebStorage; import android.webkit.WebView; @@ -139,6 +141,7 @@ private void updateSettings(MethodCall methodCall, Result result) { result.success(null); } + @TargetApi(Build.VERSION_CODES.KITKAT) private void evaluateJavaScript(MethodCall methodCall, final Result result) { String jsString = (String) methodCall.arguments; if (jsString == null) { 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 7634511fe1ab..6a67413c7b5c 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 @@ -35,7 +35,7 @@ void setHasNavigationDelegate(boolean hasNavigationDelegate) { @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { if (!hasNavigationDelegate) { - return super.shouldOverrideUrlLoading(view, request); + return false; } notifyOnNavigationRequest( request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); @@ -56,7 +56,7 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!hasNavigationDelegate) { - return super.shouldOverrideUrlLoading(view, url); + return false; } // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). From ad309267f50f924f9e4620f2126e72a2686c88a0 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 8 Mar 2019 19:48:58 -0800 Subject: [PATCH 11/11] fix nit --- .../io/flutter/plugins/webviewflutter/FlutterWebViewClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6a67413c7b5c..fe4482c154b0 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 @@ -104,7 +104,7 @@ public void success(Object shouldLoad) { @Override public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigation "); + throw new IllegalStateException("navigationRequest calls must succeed"); } @Override