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.20

* Added support for receiving web resource loading errors. See `WebView.onWebResourceError`.

## 0.3.19+10

* Replace deprecated `getFlutterEngine` call on Android.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@

package io.flutter.plugins.webviewflutter;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.os.Build;
import android.util.Log;
import android.view.KeyEvent;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.webkit.WebResourceErrorCompat;
import androidx.webkit.WebViewClientCompat;
import io.flutter.plugin.common.MethodChannel;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

// We need to use WebViewClientCompat to get
Expand All @@ -30,6 +34,47 @@ class FlutterWebViewClient {
this.methodChannel = methodChannel;
}

private static String errorCodeToString(int errorCode) {
switch (errorCode) {
case WebViewClient.ERROR_AUTHENTICATION:
return "authentication";
case WebViewClient.ERROR_BAD_URL:
return "badUrl";
case WebViewClient.ERROR_CONNECT:
return "connect";
case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE:
return "failedSslHandshake";
case WebViewClient.ERROR_FILE:
return "file";
case WebViewClient.ERROR_FILE_NOT_FOUND:
return "fileNotFound";
case WebViewClient.ERROR_HOST_LOOKUP:
return "hostLookup";
case WebViewClient.ERROR_IO:
return "io";
case WebViewClient.ERROR_PROXY_AUTHENTICATION:
return "proxyAuthentication";
case WebViewClient.ERROR_REDIRECT_LOOP:
return "redirectLoop";
case WebViewClient.ERROR_TIMEOUT:
return "timeout";
case WebViewClient.ERROR_TOO_MANY_REQUESTS:
return "tooManyRequests";
case WebViewClient.ERROR_UNKNOWN:
return "unknown";
case WebViewClient.ERROR_UNSAFE_RESOURCE:
return "unsafeResource";
case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME:
return "unsupportedAuthScheme";
case WebViewClient.ERROR_UNSUPPORTED_SCHEME:
return "unsupportedScheme";
}

final String message =
String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Is %d formatting affected by the locale?
Also seems like you can omit specifying Locale.getDefault() as this is the default for the String.format version that doesn't take a locale.

Copy link
Contributor Author

@bparrishMines bparrishMines Apr 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only added the Locale.getDefault() to get rid of the lint warning. It does change the digit to whatever locale the device is set to. I don't think it would change the interpretation of the digit though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, my read of https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax is that %d formatting is not affected by the locale.

throw new IllegalArgumentException(message);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
if (!hasNavigationDelegate) {
Expand Down Expand Up @@ -79,6 +124,14 @@ private void onPageFinished(WebView view, String url) {
methodChannel.invokeMethod("onPageFinished", args);
}

private void onWebResourceError(final int errorCode, final String description) {
final Map<String, Object> args = new HashMap<>();
args.put("errorCode", errorCode);
args.put("description", description);
args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode));
methodChannel.invokeMethod("onWebResourceError", args);
}

private void notifyOnNavigationRequest(
String url, Map<String, String> headers, WebView webview, boolean isMainFrame) {
HashMap<String, Object> args = new HashMap<>();
Expand Down Expand Up @@ -123,6 +176,20 @@ public void onPageFinished(WebView view, String url) {
FlutterWebViewClient.this.onPageFinished(view, url);
}

@TargetApi(Build.VERSION_CODES.M)
@Override
public void onReceivedError(
WebView view, WebResourceRequest request, WebResourceError error) {
FlutterWebViewClient.this.onWebResourceError(
error.getErrorCode(), error.getDescription().toString());
}

@Override
public void onReceivedError(
WebView view, int errorCode, String description, String failingUrl) {
FlutterWebViewClient.this.onWebResourceError(errorCode, description);
}

@Override
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
// Deliberately empty. Occasionally the webview will mark events as having failed to be
Expand Down Expand Up @@ -154,6 +221,22 @@ public void onPageFinished(WebView view, String url) {
FlutterWebViewClient.this.onPageFinished(view, url);
}

// This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is
// enabled. The deprecated method is called when a device doesn't support this.
@SuppressLint("RequiresFeature")
@Override
public void onReceivedError(
WebView view, WebResourceRequest request, WebResourceErrorCompat error) {
FlutterWebViewClient.this.onWebResourceError(
error.getErrorCode(), error.getDescription().toString());
}

@Override
public void onReceivedError(
WebView view, int errorCode, String description, String failingUrl) {
FlutterWebViewClient.this.onWebResourceError(errorCode, description);
}

@Override
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
// Deliberately empty. Occasionally the webview will mark events as having failed to be
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:webview_flutter/platform_interface.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:e2e/e2e.dart';

Expand Down Expand Up @@ -576,6 +578,52 @@ void main() {
expect(currentUrl, 'https://www.google.com/');
});

testWidgets('onWebResourceError', (WidgetTester tester) async {
final Completer<WebResourceError> errorCompleter =
Completer<WebResourceError>();

await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WebView(
key: GlobalKey(),
initialUrl: 'https://www.notawebsite..com',
onWebResourceError: (WebResourceError error) {
errorCompleter.complete(error);
},
),
),
);

final WebResourceError error = await errorCompleter.future;
expect(error, isNotNull);

if (Platform.isIOS) expect(error.domain, isNotNull);
if (Platform.isAndroid) expect(error.errorType, isNotNull);
});

testWidgets('onWebResourceError is not called with valid url',
(WidgetTester tester) async {
final Completer<WebResourceError> errorCompleter =
Completer<WebResourceError>();

await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WebView(
key: GlobalKey(),
initialUrl:
'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
onWebResourceError: (WebResourceError error) {
errorCompleter.complete(error);
},
),
),
);

expect(errorCompleter.future, doesNotComplete);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a test with a valid url that verifies the error callback is not invoked on a valid url. I'd use a data url like:

'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


testWidgets('can block requests', (WidgetTester tester) async {
final Completer<WebViewController> controllerCompleter =
Completer<WebViewController>();
Expand Down
55 changes: 47 additions & 8 deletions packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
#import "FLTWKNavigationDelegate.h"

@implementation FLTWKNavigationDelegate {
FlutterMethodChannel* _methodChannel;
FlutterMethodChannel *_methodChannel;
}

- (instancetype)initWithChannel:(FlutterMethodChannel*)channel {
- (instancetype)initWithChannel:(FlutterMethodChannel *)channel {
self = [super init];
if (self) {
_methodChannel = channel;
Expand All @@ -18,18 +18,18 @@ - (instancetype)initWithChannel:(FlutterMethodChannel*)channel {

#pragma mark - WKNavigationDelegate conformance

- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation {
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
[_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}];
}

- (void)webView:(WKWebView*)webView
decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (!self.hasDartNavigationDelegate) {
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
NSDictionary* arguments = @{
NSDictionary *arguments = @{
@"url" : navigationAction.request.URL.absoluteString,
@"isForMainFrame" : @(navigationAction.targetFrame.isMainFrame)
};
Expand All @@ -56,13 +56,52 @@ - (void)webView:(WKWebView*)webView
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
NSNumber* typedResult = result;
NSNumber *typedResult = result;
decisionHandler([typedResult boolValue] ? WKNavigationActionPolicyAllow
: WKNavigationActionPolicyCancel);
}];
}

- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation {
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}];
}

+ (id)errorCodeToString:(NSUInteger)code {
switch (code) {
case WKErrorUnknown:
return @"unknown";
case WKErrorWebContentProcessTerminated:
return @"webContentProcessTerminated";
case WKErrorWebViewInvalidated:
return @"webViewInvalidated";
case WKErrorJavaScriptExceptionOccurred:
return @"javaScriptExceptionOccurred";
case WKErrorJavaScriptResultTypeIsUnsupported:
return @"javaScriptResultTypeIsUnsupported";
}

return [NSNull null];
}

- (void)onWebResourceError:(NSError *)error {
[_methodChannel invokeMethod:@"onWebResourceError"
arguments:@{
@"errorCode" : @(error.code),
@"domain" : error.domain,
@"description" : error.description,
@"errorType" : [FLTWKNavigationDelegate errorCodeToString:error.code],
}];
}

- (void)webView:(WKWebView *)webView
didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error {
[self onWebResourceError:error];
}

- (void)webView:(WKWebView *)webView
didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error {
[self onWebResourceError:error];
}
@end
Loading