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

Commit 9017d6e

Browse files
authored
[webview_flutter]Allow specifying a navigation delegate(Android and Dart). (#1236)
This allows the app to prevent specific navigations(e.g prevent navigating to specific URLs). flutter/flutter#25329 iOS implementation in #1323
1 parent 45cc819 commit 9017d6e

File tree

9 files changed

+373
-32
lines changed

9 files changed

+373
-32
lines changed

packages/webview_flutter/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.3.4
2+
3+
* Support specifying navigation delegates that can prevent navigations from being executed.
4+
15
## 0.3.3+2
26

37
* Exclude LongPress handler from semantics tree since it does nothing.

packages/webview_flutter/android/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,8 @@ android {
4444
lintOptions {
4545
disable 'InvalidPackage'
4646
}
47+
48+
dependencies {
49+
implementation 'androidx.webkit:webkit:1.0.0'
50+
}
4751
}

packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
package io.flutter.plugins.webviewflutter;
66

7+
import android.annotation.TargetApi;
78
import android.content.Context;
9+
import android.os.Build;
810
import android.view.View;
911
import android.webkit.WebStorage;
1012
import android.webkit.WebView;
@@ -21,6 +23,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler {
2123
private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames";
2224
private final WebView webView;
2325
private final MethodChannel methodChannel;
26+
private final FlutterWebViewClient flutterWebViewClient;
2427

2528
@SuppressWarnings("unchecked")
2629
FlutterWebView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
@@ -31,12 +34,15 @@ public class FlutterWebView implements PlatformView, MethodCallHandler {
3134
methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
3235
methodChannel.setMethodCallHandler(this);
3336

37+
flutterWebViewClient = new FlutterWebViewClient(methodChannel);
3438
applySettings((Map<String, Object>) params.get("settings"));
3539

3640
if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) {
3741
registerJavaScriptChannelNames((List<String>) params.get(JS_CHANNEL_NAMES_FIELD));
3842
}
3943

44+
webView.setWebViewClient(flutterWebViewClient);
45+
4046
if (params.containsKey("initialUrl")) {
4147
String url = (String) params.get("initialUrl");
4248
webView.loadUrl(url);
@@ -135,6 +141,7 @@ private void updateSettings(MethodCall methodCall, Result result) {
135141
result.success(null);
136142
}
137143

144+
@TargetApi(Build.VERSION_CODES.KITKAT)
138145
private void evaluateJavaScript(MethodCall methodCall, final Result result) {
139146
String jsString = (String) methodCall.arguments;
140147
if (jsString == null) {
@@ -178,6 +185,9 @@ private void applySettings(Map<String, Object> settings) {
178185
case "jsMode":
179186
updateJsMode((Integer) settings.get(key));
180187
break;
188+
case "hasNavigationDelegate":
189+
flutterWebViewClient.setHasNavigationDelegate((boolean) settings.get(key));
190+
break;
181191
default:
182192
throw new IllegalArgumentException("Unknown WebView setting: " + key);
183193
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.webviewflutter;
6+
7+
import android.annotation.TargetApi;
8+
import android.os.Build;
9+
import android.util.Log;
10+
import android.webkit.WebResourceRequest;
11+
import android.webkit.WebView;
12+
import androidx.webkit.WebViewClientCompat;
13+
import io.flutter.plugin.common.MethodChannel;
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
17+
// We need to use WebViewClientCompat to get
18+
// shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
19+
// invoked by the webview on older Android devices, without it pages that use iframes will
20+
// be broken when a navigationDelegate is set on Android version earlier than N.
21+
class FlutterWebViewClient extends WebViewClientCompat {
22+
private static final String TAG = "FlutterWebViewClient";
23+
private final MethodChannel methodChannel;
24+
private boolean hasNavigationDelegate;
25+
26+
FlutterWebViewClient(MethodChannel methodChannel) {
27+
this.methodChannel = methodChannel;
28+
}
29+
30+
void setHasNavigationDelegate(boolean hasNavigationDelegate) {
31+
this.hasNavigationDelegate = hasNavigationDelegate;
32+
}
33+
34+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
35+
@Override
36+
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
37+
if (!hasNavigationDelegate) {
38+
return false;
39+
}
40+
notifyOnNavigationRequest(
41+
request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame());
42+
// We must make a synchronous decision here whether to allow the navigation or not,
43+
// if the Dart code has set a navigation delegate we want that delegate to decide whether
44+
// to navigate or not, and as we cannot get a response from the Dart delegate synchronously we
45+
// return true here to block the navigation, if the Dart delegate decides to allow the
46+
// navigation the plugin will later make an addition loadUrl call for this url.
47+
//
48+
// Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop
49+
// navigations that target the main frame, if the request is not for the main frame
50+
// we just return false to allow the navigation.
51+
//
52+
// For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209
53+
return request.isForMainFrame();
54+
}
55+
56+
@Override
57+
public boolean shouldOverrideUrlLoading(WebView view, String url) {
58+
if (!hasNavigationDelegate) {
59+
return false;
60+
}
61+
// This version of shouldOverrideUrlLoading is only invoked by the webview on devices with
62+
// webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false).
63+
// On these devices we cannot tell whether the navigation is targeted to the main frame or not.
64+
// We proceed assuming that the navigation is targeted to the main frame. If the page had any
65+
// frames they will be loaded in the main frame instead.
66+
Log.w(
67+
TAG,
68+
"Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work");
69+
notifyOnNavigationRequest(url, null, view, true);
70+
return true;
71+
}
72+
73+
private void notifyOnNavigationRequest(
74+
String url, Map<String, String> headers, WebView webview, boolean isMainFrame) {
75+
HashMap<String, Object> args = new HashMap<>();
76+
args.put("url", url);
77+
args.put("isForMainFrame", isMainFrame);
78+
if (isMainFrame) {
79+
methodChannel.invokeMethod(
80+
"navigationRequest", args, new OnNavigationRequestResult(url, headers, webview));
81+
} else {
82+
methodChannel.invokeMethod("navigationRequest", args);
83+
}
84+
}
85+
86+
private static class OnNavigationRequestResult implements MethodChannel.Result {
87+
private final String url;
88+
private final Map<String, String> headers;
89+
private final WebView webView;
90+
91+
private OnNavigationRequestResult(String url, Map<String, String> headers, WebView webView) {
92+
this.url = url;
93+
this.headers = headers;
94+
this.webView = webView;
95+
}
96+
97+
@Override
98+
public void success(Object shouldLoad) {
99+
Boolean typedShouldLoad = (Boolean) shouldLoad;
100+
if (typedShouldLoad) {
101+
loadUrl();
102+
}
103+
}
104+
105+
@Override
106+
public void error(String errorCode, String s1, Object o) {
107+
throw new IllegalStateException("navigationRequest calls must succeed");
108+
}
109+
110+
@Override
111+
public void notImplemented() {
112+
throw new IllegalStateException(
113+
"navigationRequest must be implemented by the webview method channel");
114+
}
115+
116+
private void loadUrl() {
117+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
118+
webView.loadUrl(url, headers);
119+
} else {
120+
webView.loadUrl(url);
121+
}
122+
}
123+
}
124+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
org.gradle.jvmargs=-Xmx1536M
2+
android.useAndroidX=true

packages/webview_flutter/example/lib/main.dart

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,27 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67
import 'package:flutter/material.dart';
78
import 'package:webview_flutter/webview_flutter.dart';
89

910
void main() => runApp(MaterialApp(home: WebViewExample()));
1011

12+
const String kNavigationExamplePage = '''
13+
<!DOCTYPE html><html>
14+
<head><title>Navigation Delegate Example</title></head>
15+
<body>
16+
<p>
17+
The navigation delegate is set to block navigation to the youtube website.
18+
</p>
19+
<ul>
20+
<ul><a href="https://www.youtube.com/">https://www.youtube.com/</a></ul>
21+
<ul><a href="https://www.google.com/">https://www.google.com/</a></ul>
22+
</ul>
23+
</body>
24+
</html>
25+
''';
26+
1127
class WebViewExample extends StatelessWidget {
1228
final Completer<WebViewController> _controller =
1329
Completer<WebViewController>();
@@ -37,6 +53,14 @@ class WebViewExample extends StatelessWidget {
3753
javascriptChannels: <JavascriptChannel>[
3854
_toasterJavascriptChannel(context),
3955
].toSet(),
56+
navigationDelegate: (NavigationRequest request) {
57+
if (request.url.startsWith('https://www.youtube.com/')) {
58+
print('blocking navigation to $request}');
59+
return NavigationDecision.prevent;
60+
}
61+
print('allowing navigation to $request');
62+
return NavigationDecision.navigate;
63+
},
4064
);
4165
}),
4266
floatingActionButton: favoriteButton(),
@@ -76,12 +100,12 @@ class WebViewExample extends StatelessWidget {
76100

77101
enum MenuOptions {
78102
showUserAgent,
79-
toast,
80103
listCookies,
81104
clearCookies,
82105
addToCache,
83106
listCache,
84107
clearCache,
108+
navigationDelegate,
85109
}
86110

87111
class SampleMenu extends StatelessWidget {
@@ -102,13 +126,6 @@ class SampleMenu extends StatelessWidget {
102126
case MenuOptions.showUserAgent:
103127
_onShowUserAgent(controller.data, context);
104128
break;
105-
case MenuOptions.toast:
106-
Scaffold.of(context).showSnackBar(
107-
SnackBar(
108-
content: Text('You selected: $value'),
109-
),
110-
);
111-
break;
112129
case MenuOptions.listCookies:
113130
_onListCookies(controller.data, context);
114131
break;
@@ -124,6 +141,9 @@ class SampleMenu extends StatelessWidget {
124141
case MenuOptions.clearCache:
125142
_onClearCache(controller.data, context);
126143
break;
144+
case MenuOptions.navigationDelegate:
145+
_onNavigationDelegateExample(controller.data, context);
146+
break;
127147
}
128148
},
129149
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
@@ -132,10 +152,6 @@ class SampleMenu extends StatelessWidget {
132152
child: const Text('Show user agent'),
133153
enabled: controller.hasData,
134154
),
135-
const PopupMenuItem<MenuOptions>(
136-
value: MenuOptions.toast,
137-
child: Text('Make a toast'),
138-
),
139155
const PopupMenuItem<MenuOptions>(
140156
value: MenuOptions.listCookies,
141157
child: Text('List cookies'),
@@ -156,6 +172,10 @@ class SampleMenu extends StatelessWidget {
156172
value: MenuOptions.clearCache,
157173
child: Text('Clear cache'),
158174
),
175+
const PopupMenuItem<MenuOptions>(
176+
value: MenuOptions.navigationDelegate,
177+
child: Text('Navigation Delegate example'),
178+
),
159179
],
160180
);
161181
},
@@ -218,6 +238,13 @@ class SampleMenu extends StatelessWidget {
218238
));
219239
}
220240

241+
void _onNavigationDelegateExample(
242+
WebViewController controller, BuildContext context) async {
243+
final String contentBase64 =
244+
base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
245+
controller.loadUrl('data:text/html;base64,$contentBase64');
246+
}
247+
221248
Widget _getCookieList(String cookies) {
222249
if (cookies == null || cookies == '""') {
223250
return Container();

0 commit comments

Comments
 (0)