Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/webview_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.4

* Support specifying navigation delegates that can prevent navigations from being executed.

## 0.3.3+2

* Exclude LongPress handler from semantics tree since it does nothing.
Expand Down
4 changes: 4 additions & 0 deletions packages/webview_flutter/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ android {
lintOptions {
disable 'InvalidPackage'
}

dependencies {
implementation 'androidx.webkit:webkit:1.0.0'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +23,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<String, Object> params) {
Expand All @@ -31,12 +34,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<String, Object>) params.get("settings"));

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

webView.setWebViewClient(flutterWebViewClient);

if (params.containsKey("initialUrl")) {
String url = (String) params.get("initialUrl");
webView.loadUrl(url);
Expand Down Expand Up @@ -135,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) {
Expand Down Expand Up @@ -178,6 +185,9 @@ private void applySettings(Map<String, Object> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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;
import android.os.Build;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import androidx.webkit.WebViewClientCompat;
import io.flutter.plugin.common.MethodChannel;
import java.util.HashMap;
import java.util.Map;

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

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 false;
}
notifyOnNavigationRequest(
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
// 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 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
return request.isForMainFrame();
}

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (!hasNavigationDelegate) {
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).
// 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;
}

private void notifyOnNavigationRequest(
String url, Map<String, String> headers, WebView webview, boolean isMainFrame) {
HashMap<String, Object> args = new HashMap<>();
args.put("url", url);
args.put("isForMainFrame", isMainFrame);
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<String, String> headers;
private final WebView webView;

private OnNavigationRequestResult(String url, Map<String, String> 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("navigationRequest calls must succeed");
}

@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);
}
}
}
}
1 change: 1 addition & 0 deletions packages/webview_flutter/example/android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
51 changes: 39 additions & 12 deletions packages/webview_flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,27 @@
// 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 = '''
<!DOCTYPE html><html>
<head><title>Navigation Delegate Example</title></head>
<body>
<p>
The navigation delegate is set to block navigation to the youtube website.
</p>
<ul>
<ul><a href="https://www.youtube.com/">https://www.youtube.com/</a></ul>
<ul><a href="https://www.google.com/">https://www.google.com/</a></ul>
</ul>
</body>
</html>
''';

class WebViewExample extends StatelessWidget {
final Completer<WebViewController> _controller =
Completer<WebViewController>();
Expand Down Expand Up @@ -37,6 +53,14 @@ class WebViewExample extends StatelessWidget {
javascriptChannels: <JavascriptChannel>[
_toasterJavascriptChannel(context),
].toSet(),
navigationDelegate: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
print('blocking navigation to $request}');
return NavigationDecision.prevent;
}
print('allowing navigation to $request');
return NavigationDecision.navigate;
},
);
}),
floatingActionButton: favoriteButton(),
Expand Down Expand Up @@ -76,12 +100,12 @@ class WebViewExample extends StatelessWidget {

enum MenuOptions {
showUserAgent,
toast,
listCookies,
clearCookies,
addToCache,
listCache,
clearCache,
navigationDelegate,
}

class SampleMenu extends StatelessWidget {
Expand All @@ -102,13 +126,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;
Expand All @@ -124,6 +141,9 @@ class SampleMenu extends StatelessWidget {
case MenuOptions.clearCache:
_onClearCache(controller.data, context);
break;
case MenuOptions.navigationDelegate:
_onNavigationDelegateExample(controller.data, context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
Expand All @@ -132,10 +152,6 @@ class SampleMenu extends StatelessWidget {
child: const Text('Show user agent'),
enabled: controller.hasData,
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.toast,
child: Text('Make a toast'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.listCookies,
child: Text('List cookies'),
Expand All @@ -156,6 +172,10 @@ class SampleMenu extends StatelessWidget {
value: MenuOptions.clearCache,
child: Text('Clear cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.navigationDelegate,
child: Text('Navigation Delegate example'),
),
],
);
},
Expand Down Expand Up @@ -218,6 +238,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();
Expand Down
Loading