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 e7b10ce2257b..4402284d29aa 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 @@ -5,12 +5,16 @@ package io.flutter.plugins.webviewflutter; import android.annotation.TargetApi; +import android.graphics.Bitmap; import android.os.Build; import android.util.Log; +import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; +import androidx.webkit.WebResourceErrorCompat; import androidx.webkit.WebViewClientCompat; import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; @@ -85,6 +89,20 @@ private void notifyOnNavigationRequest( } } + private void onReceiveError(WebView view, int code, String description, String url) { + Map args = new HashMap<>(); + args.put("url", url); + args.put("code", code); + args.put("description", description); + methodChannel.invokeMethod("onPageReceiveError", args); + } + + private void onPageStarted(WebView view, String url) { + Map args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onPageStarted", args); + } + // This method attempts to avoid using WebViewClientCompat due to bug // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see // https://github.com/flutter/flutter/issues/29446. @@ -110,6 +128,37 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request 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.onReceiveError( + view, + error.getErrorCode(), + error.getDescription().toString(), + request.getUrl().toString()); + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public void onReceivedHttpError( + WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + FlutterWebViewClient.this.onReceiveError( + view, errorResponse.getStatusCode(), null, request.getUrl().toString()); + } + + @SuppressWarnings("deprecation") + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + FlutterWebViewClient.this.onReceiveError(view, errorCode, description, failingUrl); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } }; } @@ -130,6 +179,42 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { public void onPageFinished(WebView view, String url) { FlutterWebViewClient.this.onPageFinished(view, url); } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void onReceivedHttpError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceResponse errorResponse) { + FlutterWebViewClient.this.onReceiveError( + view, errorResponse.getStatusCode(), null, request.getUrl().toString()); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void onReceivedError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + //TODO: is really need to check WebViewFeature.isFeatureSupported() and api version. + FlutterWebViewClient.this.onReceiveError( + view, + error.getErrorCode(), + error.getDescription().toString(), + request.getUrl().toString()); + } + + @SuppressWarnings("deprecation") + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + FlutterWebViewClient.this.onReceiveError(view, errorCode, description, failingUrl); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } }; } diff --git a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m index abcca0a5e8a9..5f7a313424ed 100644 --- a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m +++ b/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m @@ -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; @@ -16,14 +16,14 @@ - (instancetype)initWithChannel:(FlutterMethodChannel*)channel { return self; } -- (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) }; @@ -50,13 +50,64 @@ - (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}]; } + +- (void)webView:(WKWebView *)webView + didFailNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + NSDictionary *arguments = @{ + @"url" : webView.URL.absoluteString ?: [NSNull null], + @"code" : [NSNumber numberWithLong:error.code], + @"description" : [error localizedDescription], + }; + + [_methodChannel invokeMethod:@"onPageReceiveError" arguments:arguments]; +} + +- (void)webView:(WKWebView *)webView + didFailProvisionalNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + NSDictionary *arguments = @{ + @"url" : error.userInfo[NSURLErrorFailingURLStringErrorKey], + @"code" : [NSNumber numberWithLong:error.code], + @"description" : [error localizedDescription], + }; + + [_methodChannel invokeMethod:@"onPageReceiveError" arguments:arguments]; +} + +- (void)webView:(WKWebView *)webView + decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse + decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response; + if (response.statusCode >= 400 && response.statusCode < 600) { + NSDictionary *arguments = @{ + @"url" : response.URL.absoluteString ?: [NSNull null], + @"code" : [NSNumber numberWithLong:response.statusCode], + @"description" : [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], + }; + + [_methodChannel invokeMethod:@"onPageReceiveError" arguments:arguments]; + } + } + + decisionHandler(WKNavigationResponsePolicyAllow); +} + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { + [_methodChannel invokeMethod:@"onPageStarted" + arguments:@{ + @"url" : webView.URL.absoluteString ?: [NSNull null], + }]; +} + @end diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index bfb5b6255421..9bc58de404b9 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -25,6 +25,10 @@ abstract class WebViewPlatformCallbacksHandler { /// Invoked by [WebViewPlatformController] when a page has finished loading. void onPageFinished(String url); + + void onPageReceiveError({String url, int code, String description}); + + void onPageStarted(String url); } /// Interface for talking to the webview's platform implementation. diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index ce9988743853..0e471b28f7ef 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -38,6 +38,15 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { case 'onPageFinished': _platformCallbacksHandler.onPageFinished(call.arguments['url']); return null; + case 'onPageReceiveError': + _platformCallbacksHandler.onPageReceiveError( + url: call.arguments['url'], + code: call.arguments['code'], + description: call.arguments['description']); + return null; + case 'onPageStarted': + _platformCallbacksHandler.onPageStarted(call.arguments['url']); + return null; } throw MissingPluginException( '${call.method} was invoked but has no handler'); diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 3c0175e2f22b..073ecdc413d4 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -72,6 +72,14 @@ typedef NavigationDecision NavigationDelegate(NavigationRequest navigation); /// Signature for when a [WebView] has finished loading a page. typedef void PageFinishedCallback(String url); +/// Signature for when a [WebView] receive a error. +/// Code may be NSURLErrorDomain code or const from Android WebViewClient or http status code. +/// Description is optional +typedef void PageReceiveErrorCallback(String url, int code, String description); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + 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. @@ -121,6 +129,8 @@ class WebView extends StatefulWidget { this.gestureRecognizers, this.onPageFinished, this.debuggingEnabled = false, + this.onPageReceiveError, + this.onPageStarted, }) : assert(javascriptMode != null), super(key: key); @@ -255,6 +265,10 @@ class WebView extends StatefulWidget { /// By default `debuggingEnabled` is false. final bool debuggingEnabled; + final PageReceiveErrorCallback onPageReceiveError; + + final PageStartedCallback onPageStarted; + @override State createState() => _WebViewState(); } @@ -397,6 +411,20 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } } + @override + void onPageReceiveError({String url, int code, String description}) { + if (_widget.onPageReceiveError != null) { + _widget.onPageReceiveError(url, code, description); + } + } + + @override + void onPageStarted(String url) { + if (_widget.onPageStarted != null) { + _widget.onPageStarted(url); + } + } + void _updateJavascriptChannelsFromSet(Set channels) { _javascriptChannels.clear(); if (channels == null) { diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index d451a86b19c8..fd5e61e4e1ac 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -807,6 +807,128 @@ void main() { expect(platform.lastRequestHeaders, headers); }); }); + + group('$PageReceiveErrorCallback', () { + testWidgets('onPageReceiveError is not null', (WidgetTester tester) async { + String returnedUrl; + int returnedCode; + String returnedDescription; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageReceiveError: (String url, int code, String description) { + returnedUrl = url; + returnedCode = code; + returnedDescription = description; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageReceiveErrorCallback(404, "description"); + + expect(platformWebView.currentUrl, returnedUrl); + expect(404, returnedCode); + expect("description", returnedDescription); + }); + + testWidgets('onPageReceiveError is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onPageReceiveError: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageReceiveErrorCallback(404, "description"); + }); + + testWidgets('onPageReceiveError changed', (WidgetTester tester) async { + String returnedUrl; + int returnedCode; + String returnedDescription; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageReceiveError: (String url, int code, String description) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageReceiveError: (String url, int code, String description) { + returnedUrl = url; + returnedCode = code; + returnedDescription = description; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageReceiveErrorCallback(404, "description"); + + expect(platformWebView.currentUrl, returnedUrl); + expect(404, returnedCode); + expect("description", returnedDescription); + }); + }); + + group('$PageStartedCallback', () { + testWidgets('onPageStarted is not null', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + + testWidgets('onPageStarted is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onPageStarted: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageStartedCallback(); + }); + + testWidgets('onPageStarted changed', (WidgetTester tester) async { + String returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + }); } class FakePlatformWebView { @@ -961,6 +1083,46 @@ class FakePlatformWebView { ); } + void fakeOnPageReceiveErrorCallback(int code, String description) { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onPageReceiveError', + { + 'url': currentUrl, + 'code': code, + 'description': description + }, + )); + + // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. + // https://github.com/flutter/flutter/issues/33446 + // ignore: deprecated_member_use + BinaryMessages.handlePlatformMessage( + channel.name, + data, + (ByteData data) {}, + ); + } + + void fakeOnPageStartedCallback() { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onPageStarted', + {'url': currentUrl}, + )); + + // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. + // https://github.com/flutter/flutter/issues/33446 + // ignore: deprecated_member_use + BinaryMessages.handlePlatformMessage( + channel.name, + data, + (ByteData data) {}, + ); + } + void _loadUrl(String url) { history = history.sublist(0, currentPosition + 1); history.add(url);