Merge branch 'v4_webview' of github.com:flutter/plugins into v4_webview_android_navigation_delegate
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index a9d8d12..8aa5f7d 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart
@@ -128,10 +128,9 @@ class WebViewHostApiImpl extends WebViewHostApi { /// Constructs a [WebViewHostApiImpl]. WebViewHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -342,10 +341,9 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { /// Constructs a [WebSettingsHostApiImpl]. WebSettingsHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -484,10 +482,9 @@ class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { /// Constructs a [JavaScriptChannelHostApiImpl]. JavaScriptChannelHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -529,10 +526,9 @@ class WebViewClientHostApiImpl extends WebViewClientHostApi { /// Constructs a [WebViewClientHostApiImpl]. WebViewClientHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -717,10 +713,9 @@ class DownloadListenerHostApiImpl extends DownloadListenerHostApi { /// Constructs a [DownloadListenerHostApiImpl]. DownloadListenerHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -772,10 +767,9 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { /// Constructs a [WebChromeClientHostApiImpl]. WebChromeClientHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -822,10 +816,9 @@ class WebStorageHostApiImpl extends WebStorageHostApi { /// Constructs a [WebStorageHostApiImpl]. WebStorageHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager;
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_navigation_delegate.dart b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_navigation_delegate.dart new file mode 100644 index 0000000..0d2630f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_navigation_delegate.dart
@@ -0,0 +1,279 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +import '../../android_webview.dart' as android_webview; +import 'android_proxy.dart'; + +/// Signature for the `loadUrl` callback responsible for loading the [url] +/// after a navigation request has been approved. +typedef LoadUrlCallback = Future<void> Function( + String url, Map<String, String>? headers); + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +class AndroidWebResourceError extends WebResourceError { + /// Creates a new [AndroidWebResourceError]. + AndroidWebResourceError._({ + required super.errorCode, + required super.description, + this.failingUrl, + }) : super( + errorType: _errorCodeToErrorType(errorCode), + ); + + /// Gets the URL for which the failing resource request was made. + final String? failingUrl; + + static WebResourceErrorType? _errorCodeToErrorType(int errorCode) { + switch (errorCode) { + case android_webview.WebViewClient.errorAuthentication: + return WebResourceErrorType.authentication; + case android_webview.WebViewClient.errorBadUrl: + return WebResourceErrorType.badUrl; + case android_webview.WebViewClient.errorConnect: + return WebResourceErrorType.connect; + case android_webview.WebViewClient.errorFailedSslHandshake: + return WebResourceErrorType.failedSslHandshake; + case android_webview.WebViewClient.errorFile: + return WebResourceErrorType.file; + case android_webview.WebViewClient.errorFileNotFound: + return WebResourceErrorType.fileNotFound; + case android_webview.WebViewClient.errorHostLookup: + return WebResourceErrorType.hostLookup; + case android_webview.WebViewClient.errorIO: + return WebResourceErrorType.io; + case android_webview.WebViewClient.errorProxyAuthentication: + return WebResourceErrorType.proxyAuthentication; + case android_webview.WebViewClient.errorRedirectLoop: + return WebResourceErrorType.redirectLoop; + case android_webview.WebViewClient.errorTimeout: + return WebResourceErrorType.timeout; + case android_webview.WebViewClient.errorTooManyRequests: + return WebResourceErrorType.tooManyRequests; + case android_webview.WebViewClient.errorUnknown: + return WebResourceErrorType.unknown; + case android_webview.WebViewClient.errorUnsafeResource: + return WebResourceErrorType.unsafeResource; + case android_webview.WebViewClient.errorUnsupportedAuthScheme: + return WebResourceErrorType.unsupportedAuthScheme; + case android_webview.WebViewClient.errorUnsupportedScheme: + return WebResourceErrorType.unsupportedScheme; + } + + throw ArgumentError( + 'Could not find a WebResourceErrorType for errorCode: $errorCode', + ); + } +} + +/// Object specifying creation parameters for creating a [AndroidNavigationDelegate]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformNavigationDelegateCreationParams] for +/// more information. +@immutable +class AndroidNavigationDelegateCreationParams + extends PlatformNavigationDelegateCreationParams { + /// Creates a new [AndroidNavigationDelegateCreationParams] instance. + const AndroidNavigationDelegateCreationParams._({ + this.loadUrl, + @visibleForTesting this.androidWebViewProxy = const AndroidWebViewProxy(), + }) : super(); + + /// Creates a [AndroidNavigationDelegateCreationParams] instance based on [PlatformNavigationDelegateCreationParams]. + factory AndroidNavigationDelegateCreationParams.fromPlatformNavigationDelegateCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformNavigationDelegateCreationParams params, { + LoadUrlCallback? loadUrl, + @visibleForTesting + AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), + }) { + return AndroidNavigationDelegateCreationParams._( + loadUrl: loadUrl, + androidWebViewProxy: androidWebViewProxy, + ); + } + + /// Callback responsible for loading the [url] after a navigation request is approved. + final LoadUrlCallback? loadUrl; + + /// Handles constructing objects and calling static methods for the Android WebView + /// native library. + @visibleForTesting + final AndroidWebViewProxy androidWebViewProxy; +} + +/// A place to register callback methods responsible to handle navigation events +/// triggered by the [android_webview.WebView]. +class AndroidNavigationDelegate extends PlatformNavigationDelegate { + /// Creates a new [AndroidNavigationkDelegate]. + AndroidNavigationDelegate(PlatformNavigationDelegateCreationParams params) + : super.implementation(params is AndroidNavigationDelegateCreationParams + ? params + : AndroidNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams(params)) { + final WeakReference<AndroidNavigationDelegate> weakThis = + WeakReference<AndroidNavigationDelegate>(this); + + _webChromeClient = (params as AndroidNavigationDelegateCreationParams) + .androidWebViewProxy + .createAndroidWebChromeClient( + onProgressChanged: (android_webview.WebView webView, int progress) { + if (weakThis.target?._onProgress != null) { + weakThis.target!._onProgress!(progress); + } + }); + + _webViewClient = params.androidWebViewProxy.createAndroidWebViewClient( + onPageFinished: (android_webview.WebView webView, String url) { + if (weakThis.target?._onPageFinished != null) { + weakThis.target!._onPageFinished!(url); + } + }, + onPageStarted: (android_webview.WebView webView, String url) { + if (weakThis.target?._onPageStarted != null) { + weakThis.target!._onPageStarted!(url); + } + }, + onReceivedRequestError: ( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + ) { + if (weakThis.target?._onWebResourceError != null && + request.isForMainFrame) { + weakThis.target!._onWebResourceError!(AndroidWebResourceError._( + errorCode: error.errorCode, + description: error.description, + failingUrl: request.url, + )); + } + }, + onReceivedError: ( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + ) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!(AndroidWebResourceError._( + errorCode: errorCode, + description: description, + failingUrl: failingUrl, + )); + } + }, + requestLoading: ( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + ) { + if (weakThis.target != null) { + weakThis.target!._handleNavigation( + request.url, + request.requestHeaders, + ); + } + }, + urlLoading: ( + android_webview.WebView webView, + String url, + ) { + if (weakThis.target != null) { + weakThis.target!._handleNavigation(url, <String, String>{}); + } + }, + ); + } + + late final android_webview.WebChromeClient _webChromeClient; + + /// Gets the native [android_webview.WebChromeClient] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebChromeClient`. + android_webview.WebChromeClient get androidWebChromeClient => + _webChromeClient; + + late final android_webview.WebViewClient _webViewClient; + + /// Gets the native [android_webview.WebViewClient] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebViewClient`. + android_webview.WebViewClient get androidWebViewClient => _webViewClient; + + PageEventCallback? _onPageFinished; + PageEventCallback? _onPageStarted; + ProgressCallback? _onProgress; + WebResourceErrorCallback? _onWebResourceError; + NavigationRequestCallback? _onNavigationRequest; + + AndroidNavigationDelegateCreationParams get _androidParams => + params as AndroidNavigationDelegateCreationParams; + + bool get _handlesNavigation => + _onNavigationRequest != null && _androidParams.loadUrl != null; + + void _handleNavigation(String url, Map<String, String> headers) { + if (!_handlesNavigation) { + return; + } + + final FutureOr<NavigationDecision> returnValue = _onNavigationRequest!( + NavigationRequest( + isMainFrame: true, + url: url, + ), + ); + + if (returnValue is NavigationDecision && + returnValue == NavigationDecision.navigate) { + _androidParams.loadUrl!(url, headers); + } else if (returnValue is Future<NavigationDecision>) { + returnValue.then((NavigationDecision shouldLoadUrl) { + if (shouldLoadUrl == NavigationDecision.navigate) { + _androidParams.loadUrl!(url, headers); + } + }); + } + } + + @override + Future<void> setOnNavigationRequest( + NavigationRequestCallback onNavigationRequest, + ) async { + _onNavigationRequest = onNavigationRequest; + } + + @override + Future<void> setOnPageStarted( + PageEventCallback onPageStarted, + ) async { + _onPageStarted = onPageStarted; + } + + @override + Future<void> setOnPageFinished( + PageEventCallback onPageFinished, + ) async { + _onPageFinished = onPageFinished; + } + + @override + Future<void> setOnProgress( + ProgressCallback onProgress, + ) async { + _onProgress = onProgress; + } + + @override + Future<void> setOnWebResourceError( + WebResourceErrorCallback onWebResourceError, + ) async { + _onWebResourceError = onWebResourceError; + } +}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_proxy.dart new file mode 100644 index 0000000..757397a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_proxy.dart
@@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../android_webview.dart' as android_webview; + +/// Handles constructing objects and calling static methods for the Android +/// WebView native library. +/// +/// This class provides dependency injection for the implementations of the +/// platform interface classes. Improving the ease of unit testing and/or +/// overriding the underlying Android WebView classes. +/// +/// By default each function calls the default constructor of the WebView class +/// it intends to return. +class AndroidWebViewProxy { + /// Constructs a [AndroidWebViewProxy]. + const AndroidWebViewProxy({ + this.createAndroidWebChromeClient = android_webview.WebChromeClient.new, + this.createAndroidWebViewClient = android_webview.WebViewClient.new, + }); + + /// Constructs a [android_webview.WebChromeClient]. + final android_webview.WebChromeClient Function({ + void Function(android_webview.WebView webView, int progress)? + onProgressChanged, + }) createAndroidWebChromeClient; + + /// Constructs a [android_webview.WebViewClient]. + final android_webview.WebViewClient Function({ + bool shouldOverrideUrlLoading, + void Function(android_webview.WebView webView, String url)? onPageStarted, + void Function(android_webview.WebView webView, String url)? onPageFinished, + void Function( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + )? + onReceivedRequestError, + @Deprecated('Only called on Android version < 23.') + void Function( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + )? + onReceivedError, + void Function( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + )? + requestLoading, + void Function(android_webview.WebView webView, String url)? urlLoading, + }) createAndroidWebViewClient; +}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart index 9a64a62..127b7e4 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/v4/src/android_webview_cookie_manager.dart
@@ -44,10 +44,10 @@ /// with the default [AndroidWebViewCookieManager] constructor. @visibleForTesting AndroidWebViewCookieManager.fromNativeApi( - AndroidWebViewCookieManagerCreationParams params, { + AndroidWebViewCookieManagerCreationParams super.params, { required CookieManager cookieManager, }) : _cookieManager = cookieManager, - super.implementation(params); + super.implementation(); final CookieManager _cookieManager;
diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart index d1fa1b8..4cb99b5 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart
@@ -16,7 +16,7 @@ class WebViewAndroidWidget extends StatefulWidget { /// Constructs a [WebViewAndroidWidget]. const WebViewAndroidWidget({ - Key? key, + super.key, required this.creationParams, required this.useHybridComposition, required this.callbacksHandler, @@ -26,7 +26,7 @@ @visibleForTesting this.flutterAssetManager = const android_webview.FlutterAssetManager(), @visibleForTesting this.webStorage, - }) : super(key: key); + }); /// Initial parameters used to setup the WebView. final CreationParams creationParams; @@ -570,10 +570,9 @@ extends android_webview.JavaScriptChannel { /// Creates a [WebViewAndroidJavaScriptChannel]. WebViewAndroidJavaScriptChannel( - String channelName, + super.channelName, this.javascriptChannelRegistry, ) : super( - channelName, postMessage: withWeakRefenceTo( javascriptChannelRegistry, (WeakReference<JavascriptChannelRegistry> weakReference) {
diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 8f7cd9b..8384b7c 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
@@ -6,7 +6,7 @@ publish_to: none environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" flutter:
diff --git a/packages/webview_flutter/webview_flutter_android/test/v4/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/v4/android_navigation_delegate_test.dart new file mode 100644 index 0000000..dfde021 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/v4/android_navigation_delegate_test.dart
@@ -0,0 +1,396 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/src/v4/src/android_navigation_delegate.dart'; +import 'package:webview_flutter_android/src/v4/src/android_proxy.dart'; +import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; + +void main() { + group('AndroidNavigationDelegate', () { + test('onPageFinished', () { + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final String callbackUrl; + webKitDelegate.setOnPageFinished((String url) => callbackUrl = url); + + CapturingWebViewClient.lastCreatedDelegate.onPageFinished!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackUrl, 'https://www.google.com'); + }); + + test('onPageStarted', () { + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final String callbackUrl; + webKitDelegate.setOnPageStarted((String url) => callbackUrl = url); + + CapturingWebViewClient.lastCreatedDelegate.onPageStarted!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackUrl, 'https://www.google.com'); + }); + + test('onWebResourceError from onReceivedRequestError', () { + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final WebResourceError callbackError; + webKitDelegate.setOnWebResourceError( + (WebResourceError error) => callbackError = error); + + CapturingWebViewClient.lastCreatedDelegate.onReceivedRequestError!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: <String, String>{'X-Mock': 'mocking'}, + ), + android_webview.WebResourceError( + errorCode: android_webview.WebViewClient.errorFileNotFound, + description: 'Page not found.', + ), + ); + + expect(callbackError.errorCode, + android_webview.WebViewClient.errorFileNotFound); + expect(callbackError.description, 'Page not found.'); + expect(callbackError.errorType, WebResourceErrorType.fileNotFound); + }); + + test('onWebResourceError from onRequestError', () { + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final WebResourceError callbackError; + webKitDelegate.setOnWebResourceError( + (WebResourceError error) => callbackError = error); + + CapturingWebViewClient.lastCreatedDelegate.onReceivedError!( + android_webview.WebView.detached(), + android_webview.WebViewClient.errorFileNotFound, + 'Page not found.', + 'https://www.google.com', + ); + + expect(callbackError.errorCode, + android_webview.WebViewClient.errorFileNotFound); + expect(callbackError.description, 'Page not found.'); + expect(callbackError.errorType, WebResourceErrorType.fileNotFound); + }); + + test( + 'onNavigationRequest from requestLoading should not be called when loadUrlCallback is not specified', + () { + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + NavigationRequest? callbackNavigationRequest; + webKitDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: <String, String>{'X-Mock': 'mocking'}, + ), + ); + + expect(callbackNavigationRequest, isNull); + }); + + test( + 'onLoadUrl from requestLoading should not be called when navigationRequestCallback is not specified', + () { + final Completer<void> completer = Completer<void>(); + AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: ( + String url, + Map<String, String>? headers, + ) { + completer.complete(); + return completer.future; + })); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: <String, String>{'X-Mock': 'mocking'}, + ), + ); + + expect(completer.isCompleted, false); + }); + + test( + 'onLoadUrl from requestLoading should not be called when onNavigationRequestCallback returns NavigationDecision.prevent', + () { + final Completer<void> completer = Completer<void>(); + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: ( + String url, + Map<String, String>? headers, + ) { + completer.complete(); + return completer.future; + })); + + late final NavigationRequest callbackNavigationRequest; + webKitDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: <String, String>{'X-Mock': 'mocking'}, + ), + ); + + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, false); + }); + + test( + 'onLoadUrl from requestLoading should complete when onNavigationRequestCallback returns NavigationDecision.navigate', + () { + final Completer<void> completer = Completer<void>(); + late final String loadUrlCallbackUrl; + late final Map<String, String>? loadUrlCallbackHeaders; + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: ( + String url, + Map<String, String>? headers, + ) { + loadUrlCallbackUrl = url; + loadUrlCallbackHeaders = headers; + completer.complete(); + return completer.future; + })); + + late final NavigationRequest callbackNavigationRequest; + webKitDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.navigate; + }); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: <String, String>{'X-Mock': 'mocking'}, + ), + ); + + expect(loadUrlCallbackUrl, 'https://www.google.com'); + expect(loadUrlCallbackHeaders, <String, String>{'X-Mock': 'mocking'}); + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, true); + }); + + test( + 'onNavigationRequest from urlLoading should not be called when loadUrlCallback is not specified', + () { + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + NavigationRequest? callbackNavigationRequest; + webKitDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackNavigationRequest, isNull); + }); + + test( + 'onLoadUrl from urlLoading should not be called when navigationRequestCallback is not specified', + () { + final Completer<void> completer = Completer<void>(); + AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: ( + String url, + Map<String, String>? headers, + ) { + completer.complete(); + return completer.future; + })); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(completer.isCompleted, false); + }); + + test( + 'onLoadUrl from urlLoading should not be called when onNavigationRequestCallback returns NavigationDecision.prevent', + () { + final Completer<void> completer = Completer<void>(); + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: ( + String url, + Map<String, String>? headers, + ) { + completer.complete(); + return completer.future; + })); + + late final NavigationRequest callbackNavigationRequest; + webKitDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, false); + }); + + test( + 'onLoadUrl from urlLoading should complete when onNavigationRequestCallback returns NavigationDecision.navigate', + () { + final Completer<void> completer = Completer<void>(); + late final String loadUrlCallbackUrl; + late final Map<String, String>? loadUrlCallbackHeaders; + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams(loadUrlCallback: ( + String url, + Map<String, String>? headers, + ) { + loadUrlCallbackUrl = url; + loadUrlCallbackHeaders = headers; + completer.complete(); + return completer.future; + })); + + late final NavigationRequest callbackNavigationRequest; + webKitDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.navigate; + }); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(loadUrlCallbackUrl, 'https://www.google.com'); + expect(loadUrlCallbackHeaders, <String, String>{}); + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, true); + }); + + test('setOnProgress', () { + final AndroidNavigationDelegate webKitDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final int callbackProgress; + webKitDelegate + .setOnProgress((int progress) => callbackProgress = progress); + + CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!( + android_webview.WebView.detached(), + 42, + ); + + expect(callbackProgress, 42); + }); + }); +} + +AndroidNavigationDelegateCreationParams _buildCreationParams({ + LoadUrlCallback? loadUrlCallback, +}) { + return AndroidNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams( + const PlatformNavigationDelegateCreationParams(), + loadUrl: loadUrlCallback, + androidWebViewProxy: const AndroidWebViewProxy( + createAndroidWebChromeClient: CapturingWebChromeClient.new, + createAndroidWebViewClient: CapturingWebViewClient.new, + ), + ); +} + +// Records the last created instance of itself. +class CapturingWebViewClient extends android_webview.WebViewClient { + CapturingWebViewClient({ + super.onPageFinished, + super.onPageStarted, + super.onReceivedError, + super.onReceivedRequestError, + super.requestLoading, + super.shouldOverrideUrlLoading, + super.urlLoading, + }) : super.detached() { + lastCreatedDelegate = this; + } + static CapturingWebViewClient lastCreatedDelegate = CapturingWebViewClient(); +} + +// Records the last created instance of itself. +class CapturingWebChromeClient extends android_webview.WebChromeClient { + CapturingWebChromeClient({ + super.onProgressChanged, + }) : super.detached() { + lastCreatedDelegate = this; + } + static CapturingWebChromeClient lastCreatedDelegate = + CapturingWebChromeClient(); +}