| // 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 'dart:typed_data'; |
| |
| import 'package:flutter/widgets.dart'; |
| // ignore: implementation_imports |
| import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; |
| |
| import '../android_webview.dart' as android_webview; |
| import '../weak_reference_utils.dart'; |
| import 'webview_android_cookie_manager.dart'; |
| |
| /// Creates a [Widget] with a [android_webview.WebView]. |
| class WebViewAndroidWidget extends StatefulWidget { |
| /// Constructs a [WebViewAndroidWidget]. |
| const WebViewAndroidWidget({ |
| super.key, |
| required this.creationParams, |
| required this.useHybridComposition, |
| required this.callbacksHandler, |
| required this.javascriptChannelRegistry, |
| required this.onBuildWidget, |
| @visibleForTesting this.webViewProxy = const WebViewProxy(), |
| @visibleForTesting |
| this.flutterAssetManager = const android_webview.FlutterAssetManager(), |
| @visibleForTesting this.webStorage, |
| }); |
| |
| /// Initial parameters used to setup the WebView. |
| final CreationParams creationParams; |
| |
| /// Whether the [android_webview.WebView] will be rendered with an [AndroidViewSurface]. |
| /// |
| /// This implementation uses hybrid composition to render the |
| /// [WebViewAndroidWidget]. This comes at the cost of some performance on |
| /// Android versions below 10. See |
| /// https://flutter.dev/docs/development/platform-integration/platform-views#performance |
| /// for more information. |
| /// |
| /// Defaults to false. |
| final bool useHybridComposition; |
| |
| /// Handles callbacks that are made by [android_webview.WebViewClient], [android_webview.DownloadListener], and [android_webview.WebChromeClient]. |
| final WebViewPlatformCallbacksHandler callbacksHandler; |
| |
| /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. |
| final JavascriptChannelRegistry javascriptChannelRegistry; |
| |
| /// Handles constructing [android_webview.WebView]s and calling static methods. |
| /// |
| /// This should only be changed for testing purposes. |
| final WebViewProxy webViewProxy; |
| |
| /// Manages access to Flutter assets that are part of the Android App bundle. |
| /// |
| /// This should only be changed for testing purposes. |
| final android_webview.FlutterAssetManager flutterAssetManager; |
| |
| /// Callback to build a widget once [android_webview.WebView] has been initialized. |
| final Widget Function(WebViewAndroidPlatformController controller) |
| onBuildWidget; |
| |
| /// Manages the JavaScript storage APIs. |
| final android_webview.WebStorage? webStorage; |
| |
| @override |
| State<StatefulWidget> createState() => _WebViewAndroidWidgetState(); |
| } |
| |
| class _WebViewAndroidWidgetState extends State<WebViewAndroidWidget> { |
| late final WebViewAndroidPlatformController controller; |
| |
| @override |
| void initState() { |
| super.initState(); |
| controller = WebViewAndroidPlatformController( |
| useHybridComposition: widget.useHybridComposition, |
| creationParams: widget.creationParams, |
| callbacksHandler: widget.callbacksHandler, |
| javascriptChannelRegistry: widget.javascriptChannelRegistry, |
| webViewProxy: widget.webViewProxy, |
| flutterAssetManager: widget.flutterAssetManager, |
| webStorage: widget.webStorage, |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return widget.onBuildWidget(controller); |
| } |
| } |
| |
| /// Implementation of [WebViewPlatformController] with the Android WebView api. |
| class WebViewAndroidPlatformController extends WebViewPlatformController { |
| /// Construct a [WebViewAndroidPlatformController]. |
| WebViewAndroidPlatformController({ |
| required bool useHybridComposition, |
| required CreationParams creationParams, |
| required this.callbacksHandler, |
| required this.javascriptChannelRegistry, |
| @visibleForTesting this.webViewProxy = const WebViewProxy(), |
| @visibleForTesting |
| this.flutterAssetManager = const android_webview.FlutterAssetManager(), |
| @visibleForTesting android_webview.WebStorage? webStorage, |
| }) : webStorage = webStorage ?? android_webview.WebStorage.instance, |
| assert(creationParams.webSettings?.hasNavigationDelegate != null), |
| super(callbacksHandler) { |
| webView = webViewProxy.createWebView( |
| useHybridComposition: useHybridComposition, |
| ); |
| |
| webView.settings.setDomStorageEnabled(true); |
| webView.settings.setJavaScriptCanOpenWindowsAutomatically(true); |
| webView.settings.setSupportMultipleWindows(true); |
| webView.settings.setLoadWithOverviewMode(true); |
| webView.settings.setUseWideViewPort(true); |
| webView.settings.setDisplayZoomControls(false); |
| webView.settings.setBuiltInZoomControls(true); |
| |
| _setCreationParams(creationParams); |
| webView.setDownloadListener(downloadListener); |
| webView.setWebChromeClient(webChromeClient); |
| webView.setWebViewClient(webViewClient); |
| |
| final String? initialUrl = creationParams.initialUrl; |
| if (initialUrl != null) { |
| loadUrl(initialUrl, <String, String>{}); |
| } |
| } |
| |
| final Map<String, WebViewAndroidJavaScriptChannel> _javaScriptChannels = |
| <String, WebViewAndroidJavaScriptChannel>{}; |
| |
| late final android_webview.WebViewClient _webViewClient = withWeakReferenceTo( |
| this, (WeakReference<WebViewAndroidPlatformController> weakReference) { |
| return webViewProxy.createWebViewClient( |
| onPageStarted: (_, String url) { |
| weakReference.target?.callbacksHandler.onPageStarted(url); |
| }, |
| onPageFinished: (_, String url) { |
| weakReference.target?.callbacksHandler.onPageFinished(url); |
| }, |
| onReceivedError: ( |
| _, |
| int errorCode, |
| String description, |
| String failingUrl, |
| ) { |
| weakReference.target?.callbacksHandler |
| .onWebResourceError(WebResourceError( |
| errorCode: errorCode, |
| description: description, |
| failingUrl: failingUrl, |
| errorType: _errorCodeToErrorType(errorCode), |
| )); |
| }, |
| onReceivedRequestError: ( |
| _, |
| android_webview.WebResourceRequest request, |
| android_webview.WebResourceError error, |
| ) { |
| if (request.isForMainFrame) { |
| weakReference.target?.callbacksHandler |
| .onWebResourceError(WebResourceError( |
| errorCode: error.errorCode, |
| description: error.description, |
| failingUrl: request.url, |
| errorType: _errorCodeToErrorType(error.errorCode), |
| )); |
| } |
| }, |
| urlLoading: (_, String url) { |
| weakReference.target?._handleNavigationRequest( |
| url: url, |
| isForMainFrame: true, |
| ); |
| }, |
| requestLoading: (_, android_webview.WebResourceRequest request) { |
| weakReference.target?._handleNavigationRequest( |
| url: request.url, |
| isForMainFrame: request.isForMainFrame, |
| ); |
| }, |
| ); |
| }); |
| |
| bool _hasNavigationDelegate = false; |
| bool _hasProgressTracking = false; |
| |
| /// Represents the WebView maintained by platform code. |
| late final android_webview.WebView webView; |
| |
| /// Handles callbacks that are made by [android_webview.WebViewClient], [android_webview.DownloadListener], and [android_webview.WebChromeClient]. |
| final WebViewPlatformCallbacksHandler callbacksHandler; |
| |
| /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. |
| final JavascriptChannelRegistry javascriptChannelRegistry; |
| |
| /// Handles constructing [android_webview.WebView]s and calling static methods. |
| /// |
| /// This should only be changed for testing purposes. |
| final WebViewProxy webViewProxy; |
| |
| /// Manages access to Flutter assets that are part of the Android App bundle. |
| /// |
| /// This should only be changed for testing purposes. |
| final android_webview.FlutterAssetManager flutterAssetManager; |
| |
| /// Receives callbacks when content should be downloaded instead. |
| @visibleForTesting |
| late final android_webview.DownloadListener downloadListener = |
| android_webview.DownloadListener( |
| onDownloadStart: withWeakReferenceTo( |
| this, |
| (WeakReference<WebViewAndroidPlatformController> weakReference) { |
| return ( |
| String url, |
| String userAgent, |
| String contentDisposition, |
| String mimetype, |
| int contentLength, |
| ) { |
| weakReference.target?._handleNavigationRequest( |
| url: url, |
| isForMainFrame: true, |
| ); |
| }; |
| }, |
| ), |
| ); |
| |
| /// Handles JavaScript dialogs, favicons, titles, new windows, and the progress for [android_webview.WebView]. |
| @visibleForTesting |
| late final android_webview.WebChromeClient webChromeClient = |
| android_webview.WebChromeClient( |
| onProgressChanged: withWeakReferenceTo( |
| this, |
| (WeakReference<WebViewAndroidPlatformController> weakReference) { |
| return (_, int progress) { |
| final WebViewAndroidPlatformController? controller = |
| weakReference.target; |
| if (controller != null && controller._hasProgressTracking) { |
| controller.callbacksHandler.onProgress(progress); |
| } |
| }; |
| }, |
| )); |
| |
| /// Manages the JavaScript storage APIs. |
| final android_webview.WebStorage webStorage; |
| |
| /// Receive various notifications and requests for [android_webview.WebView]. |
| @visibleForTesting |
| android_webview.WebViewClient get webViewClient => _webViewClient; |
| |
| @override |
| Future<void> loadHtmlString(String html, {String? baseUrl}) { |
| return webView.loadDataWithBaseUrl( |
| baseUrl: baseUrl, |
| data: html, |
| mimeType: 'text/html', |
| ); |
| } |
| |
| @override |
| Future<void> loadFile(String absoluteFilePath) { |
| final String url = absoluteFilePath.startsWith('file://') |
| ? absoluteFilePath |
| : 'file://$absoluteFilePath'; |
| |
| webView.settings.setAllowFileAccess(true); |
| return webView.loadUrl(url, <String, String>{}); |
| } |
| |
| @override |
| Future<void> loadFlutterAsset(String key) async { |
| final String assetFilePath = |
| await flutterAssetManager.getAssetFilePathByName(key); |
| final List<String> pathElements = assetFilePath.split('/'); |
| final String fileName = pathElements.removeLast(); |
| final List<String?> paths = |
| await flutterAssetManager.list(pathElements.join('/')); |
| |
| if (!paths.contains(fileName)) { |
| throw ArgumentError( |
| 'Asset for key "$key" not found.', |
| 'key', |
| ); |
| } |
| |
| return webView.loadUrl( |
| 'file:///android_asset/$assetFilePath', |
| <String, String>{}, |
| ); |
| } |
| |
| @override |
| Future<void> loadUrl( |
| String url, |
| Map<String, String>? headers, |
| ) { |
| return webView.loadUrl(url, headers ?? <String, String>{}); |
| } |
| |
| /// When making a POST request, headers are ignored. As a workaround, make |
| /// the request manually and load the response data using [loadHTMLString]. |
| @override |
| Future<void> loadRequest( |
| WebViewRequest request, |
| ) async { |
| if (!request.uri.hasScheme) { |
| throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); |
| } |
| switch (request.method) { |
| case WebViewRequestMethod.get: |
| return webView.loadUrl(request.uri.toString(), request.headers); |
| case WebViewRequestMethod.post: |
| return webView.postUrl( |
| request.uri.toString(), request.body ?? Uint8List(0)); |
| } |
| // The enum comes from a different package, which could get a new value at |
| // any time, so a fallback case is necessary. Since there is no reasonable |
| // default behavior, throw to alert the client that they need an updated |
| // version. This is deliberately outside the switch rather than a `default` |
| // so that the linter will flag the switch as needing an update. |
| // ignore: dead_code |
| throw UnimplementedError( |
| 'This version of webview_android_widget currently has no ' |
| 'implementation for HTTP method ${request.method.serialize()} in ' |
| 'loadRequest.'); |
| } |
| |
| @override |
| Future<String?> currentUrl() => webView.getUrl(); |
| |
| @override |
| Future<bool> canGoBack() => webView.canGoBack(); |
| |
| @override |
| Future<bool> canGoForward() => webView.canGoForward(); |
| |
| @override |
| Future<void> goBack() => webView.goBack(); |
| |
| @override |
| Future<void> goForward() => webView.goForward(); |
| |
| @override |
| Future<void> reload() => webView.reload(); |
| |
| @override |
| Future<void> clearCache() { |
| webView.clearCache(true); |
| return webStorage.deleteAllData(); |
| } |
| |
| @override |
| Future<void> updateSettings(WebSettings setting) async { |
| _hasProgressTracking = setting.hasProgressTracking ?? _hasProgressTracking; |
| await Future.wait(<Future<void>>[ |
| _setUserAgent(setting.userAgent), |
| if (setting.hasNavigationDelegate != null) |
| _setHasNavigationDelegate(setting.hasNavigationDelegate!), |
| if (setting.javascriptMode != null) |
| _setJavaScriptMode(setting.javascriptMode!), |
| if (setting.debuggingEnabled != null) |
| _setDebuggingEnabled(setting.debuggingEnabled!), |
| if (setting.zoomEnabled != null) _setZoomEnabled(setting.zoomEnabled!), |
| ]); |
| } |
| |
| @override |
| Future<String> evaluateJavascript(String javascript) async { |
| return runJavascriptReturningResult(javascript); |
| } |
| |
| @override |
| Future<void> runJavascript(String javascript) async { |
| await webView.evaluateJavascript(javascript); |
| } |
| |
| @override |
| Future<String> runJavascriptReturningResult(String javascript) async { |
| return await webView.evaluateJavascript(javascript) ?? ''; |
| } |
| |
| @override |
| Future<void> addJavascriptChannels(Set<String> javascriptChannelNames) { |
| return Future.wait( |
| javascriptChannelNames.where( |
| (String channelName) { |
| return !_javaScriptChannels.containsKey(channelName); |
| }, |
| ).map<Future<void>>( |
| (String channelName) { |
| final WebViewAndroidJavaScriptChannel javaScriptChannel = |
| WebViewAndroidJavaScriptChannel( |
| channelName, javascriptChannelRegistry); |
| _javaScriptChannels[channelName] = javaScriptChannel; |
| return webView.addJavaScriptChannel(javaScriptChannel); |
| }, |
| ), |
| ); |
| } |
| |
| @override |
| Future<void> removeJavascriptChannels( |
| Set<String> javascriptChannelNames, |
| ) { |
| return Future.wait( |
| javascriptChannelNames.where( |
| (String channelName) { |
| return _javaScriptChannels.containsKey(channelName); |
| }, |
| ).map<Future<void>>( |
| (String channelName) { |
| final WebViewAndroidJavaScriptChannel javaScriptChannel = |
| _javaScriptChannels[channelName]!; |
| _javaScriptChannels.remove(channelName); |
| return webView.removeJavaScriptChannel(javaScriptChannel); |
| }, |
| ), |
| ); |
| } |
| |
| @override |
| Future<String?> getTitle() => webView.getTitle(); |
| |
| @override |
| Future<void> scrollTo(int x, int y) => webView.scrollTo(x, y); |
| |
| @override |
| Future<void> scrollBy(int x, int y) => webView.scrollBy(x, y); |
| |
| @override |
| Future<int> getScrollX() => webView.getScrollX(); |
| |
| @override |
| Future<int> getScrollY() => webView.getScrollY(); |
| |
| void _setCreationParams(CreationParams creationParams) { |
| final WebSettings? webSettings = creationParams.webSettings; |
| if (webSettings != null) { |
| updateSettings(webSettings); |
| } |
| |
| final String? userAgent = creationParams.userAgent; |
| if (userAgent != null) { |
| webView.settings.setUserAgentString(userAgent); |
| } |
| |
| webView.settings.setMediaPlaybackRequiresUserGesture( |
| creationParams.autoMediaPlaybackPolicy != |
| AutoMediaPlaybackPolicy.always_allow, |
| ); |
| |
| final Color? backgroundColor = creationParams.backgroundColor; |
| if (backgroundColor != null) { |
| webView.setBackgroundColor(backgroundColor); |
| } |
| |
| addJavascriptChannels(creationParams.javascriptChannelNames); |
| |
| // TODO(BeMacized): Remove once platform implementations |
| // are able to register themselves (Flutter >=2.8), |
| // https://github.com/flutter/flutter/issues/94224 |
| WebViewCookieManagerPlatform.instance ??= WebViewAndroidCookieManager(); |
| |
| creationParams.cookies |
| .forEach(WebViewCookieManagerPlatform.instance!.setCookie); |
| } |
| |
| Future<void> _setHasNavigationDelegate(bool hasNavigationDelegate) { |
| _hasNavigationDelegate = hasNavigationDelegate; |
| return _webViewClient.setSynchronousReturnValueForShouldOverrideUrlLoading( |
| hasNavigationDelegate, |
| ); |
| } |
| |
| Future<void> _setJavaScriptMode(JavascriptMode mode) { |
| switch (mode) { |
| case JavascriptMode.disabled: |
| return webView.settings.setJavaScriptEnabled(false); |
| case JavascriptMode.unrestricted: |
| return webView.settings.setJavaScriptEnabled(true); |
| } |
| } |
| |
| Future<void> _setDebuggingEnabled(bool debuggingEnabled) { |
| return webViewProxy.setWebContentsDebuggingEnabled(debuggingEnabled); |
| } |
| |
| Future<void> _setUserAgent(WebSetting<String?> userAgent) { |
| if (userAgent.isPresent) { |
| // If the string is empty, the system default value will be used. |
| return webView.settings.setUserAgentString(userAgent.value ?? ''); |
| } |
| |
| return Future<void>.value(); |
| } |
| |
| Future<void> _setZoomEnabled(bool zoomEnabled) { |
| return webView.settings.setSupportZoom(zoomEnabled); |
| } |
| |
| 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', |
| ); |
| } |
| |
| void _handleNavigationRequest({ |
| required String url, |
| required bool isForMainFrame, |
| }) { |
| if (!_hasNavigationDelegate) { |
| return; |
| } |
| |
| final FutureOr<bool> returnValue = callbacksHandler.onNavigationRequest( |
| url: url, |
| isForMainFrame: isForMainFrame, |
| ); |
| |
| if (returnValue is bool && returnValue) { |
| loadUrl(url, <String, String>{}); |
| } else if (returnValue is Future<bool>) { |
| returnValue.then((bool shouldLoadUrl) { |
| if (shouldLoadUrl) { |
| loadUrl(url, <String, String>{}); |
| } |
| }); |
| } |
| } |
| } |
| |
| /// Exposes a channel to receive calls from javaScript. |
| class WebViewAndroidJavaScriptChannel |
| extends android_webview.JavaScriptChannel { |
| /// Creates a [WebViewAndroidJavaScriptChannel]. |
| WebViewAndroidJavaScriptChannel( |
| super.channelName, |
| this.javascriptChannelRegistry, |
| ) : super( |
| postMessage: withWeakReferenceTo( |
| javascriptChannelRegistry, |
| (WeakReference<JavascriptChannelRegistry> weakReference) { |
| return (String message) { |
| weakReference.target?.onJavascriptChannelMessage( |
| channelName, |
| message, |
| ); |
| }; |
| }, |
| ), |
| ); |
| |
| /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. |
| final JavascriptChannelRegistry javascriptChannelRegistry; |
| } |
| |
| /// Handles constructing [android_webview.WebView]s and calling static methods. |
| /// |
| /// This should only be used for testing purposes. |
| @visibleForTesting |
| class WebViewProxy { |
| /// Creates a [WebViewProxy]. |
| const WebViewProxy(); |
| |
| /// Constructs a [android_webview.WebView]. |
| android_webview.WebView createWebView({required bool useHybridComposition}) { |
| return android_webview.WebView(useHybridComposition: useHybridComposition); |
| } |
| |
| /// Constructs a [android_webview.WebViewClient]. |
| android_webview.WebViewClient createWebViewClient({ |
| 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, |
| 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, |
| }) { |
| return android_webview.WebViewClient( |
| onPageStarted: onPageStarted, |
| onPageFinished: onPageFinished, |
| onReceivedRequestError: onReceivedRequestError, |
| onReceivedError: onReceivedError, |
| requestLoading: requestLoading, |
| urlLoading: urlLoading, |
| ); |
| } |
| |
| /// Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. |
| /// |
| /// This flag can be enabled in order to facilitate debugging of web layouts |
| /// and JavaScript code running inside WebViews. Please refer to |
| /// [android_webview.WebView] documentation for the debugging guide. The |
| /// default is false. |
| /// |
| /// See [android_webview.WebView].setWebContentsDebuggingEnabled. |
| Future<void> setWebContentsDebuggingEnabled(bool enabled) { |
| return android_webview.WebView.setWebContentsDebuggingEnabled(enabled); |
| } |
| } |