blob: 2e32705a83ea6b7751c89be237c0d7ef6b110f1c [file] [log] [blame]
// 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.
// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231)
// ignore: unnecessary_import
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'android_navigation_delegate.dart';
import 'android_proxy.dart';
import 'android_webview.dart' as android_webview;
import 'android_webview.dart';
import 'instance_manager.dart';
import 'platform_views_service_proxy.dart';
import 'weak_reference_utils.dart';
/// Object specifying creation parameters for creating a [AndroidWebViewController].
///
/// When adding additional fields make sure they can be null or have a default
/// value to avoid breaking changes. See [PlatformWebViewControllerCreationParams] for
/// more information.
@immutable
class AndroidWebViewControllerCreationParams
extends PlatformWebViewControllerCreationParams {
/// Creates a new [AndroidWebViewControllerCreationParams] instance.
AndroidWebViewControllerCreationParams({
@visibleForTesting this.androidWebViewProxy = const AndroidWebViewProxy(),
@visibleForTesting android_webview.WebStorage? androidWebStorage,
}) : androidWebStorage =
androidWebStorage ?? android_webview.WebStorage.instance,
super();
/// Creates a [AndroidWebViewControllerCreationParams] instance based on [PlatformWebViewControllerCreationParams].
factory AndroidWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams(
// Recommended placeholder to prevent being broken by platform interface.
// ignore: avoid_unused_constructor_parameters
PlatformWebViewControllerCreationParams params, {
@visibleForTesting
AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(),
@visibleForTesting android_webview.WebStorage? androidWebStorage,
}) {
return AndroidWebViewControllerCreationParams(
androidWebViewProxy: androidWebViewProxy,
androidWebStorage:
androidWebStorage ?? android_webview.WebStorage.instance,
);
}
/// Handles constructing objects and calling static methods for the Android WebView
/// native library.
@visibleForTesting
final AndroidWebViewProxy androidWebViewProxy;
/// Manages the JavaScript storage APIs provided by the [android_webview.WebView].
@visibleForTesting
final android_webview.WebStorage androidWebStorage;
}
/// Implementation of the [PlatformWebViewController] with the Android WebView API.
class AndroidWebViewController extends PlatformWebViewController {
/// Creates a new [AndroidWebViewCookieManager].
AndroidWebViewController(PlatformWebViewControllerCreationParams params)
: super.implementation(params is AndroidWebViewControllerCreationParams
? params
: AndroidWebViewControllerCreationParams
.fromPlatformWebViewControllerCreationParams(params)) {
_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);
}
AndroidWebViewControllerCreationParams get _androidWebViewParams =>
params as AndroidWebViewControllerCreationParams;
/// The native [android_webview.WebView] being controlled.
late final android_webview.WebView _webView =
_androidWebViewParams.androidWebViewProxy.createAndroidWebView(
// Due to changes in Flutter 3.0 the `useHybridComposition` doesn't have
// any effect and is purposefully not exposed publicly by the
// [AndroidWebViewController]. More info here:
// https://github.com/flutter/flutter/issues/108106
useHybridComposition: true,
);
/// The native [android_webview.FlutterAssetManager] allows managing assets.
late final android_webview.FlutterAssetManager _flutterAssetManager =
_androidWebViewParams.androidWebViewProxy.createFlutterAssetManager();
final Map<String, AndroidJavaScriptChannelParams> _javaScriptChannelParams =
<String, AndroidJavaScriptChannelParams>{};
// The keeps a reference to the current NavigationDelegate so that the
// callback methods remain reachable.
// ignore: unused_field
late AndroidNavigationDelegate _currentNavigationDelegate;
/// Whether to enable the platform's webview content debugging tools.
///
/// Defaults to false.
static Future<void> enableDebugging(
bool enabled, {
@visibleForTesting
AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(),
}) {
return webViewProxy.setWebContentsDebuggingEnabled(enabled);
}
@override
Future<void> loadFile(
String absoluteFilePath,
) {
final String url = absoluteFilePath.startsWith('file://')
? absoluteFilePath
: Uri.file(absoluteFilePath).toString();
_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(
Uri.file('/android_asset/$assetFilePath').toString(),
<String, String>{},
);
}
@override
Future<void> loadHtmlString(
String html, {
String? baseUrl,
}) {
return _webView.loadDataWithBaseUrl(
baseUrl: baseUrl,
data: html,
mimeType: 'text/html',
);
}
@override
Future<void> loadRequest(
LoadRequestParams params,
) {
if (!params.uri.hasScheme) {
throw ArgumentError('WebViewRequest#uri is required to have a scheme.');
}
switch (params.method) {
case LoadRequestMethod.get:
return _webView.loadUrl(params.uri.toString(), params.headers);
case LoadRequestMethod.post:
return _webView.postUrl(
params.uri.toString(), params.body ?? Uint8List(0));
default:
throw UnimplementedError(
'This version of `AndroidWebViewController` currently has no implementation for HTTP method ${params.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);
@override
Future<void> clearLocalStorage() =>
_androidWebViewParams.androidWebStorage.deleteAllData();
@override
Future<void> setPlatformNavigationDelegate(
covariant AndroidNavigationDelegate handler) async {
_currentNavigationDelegate = handler;
handler.setOnLoadRequest(loadRequest);
_webView.setWebViewClient(handler.androidWebViewClient);
_webView.setWebChromeClient(handler.androidWebChromeClient);
_webView.setDownloadListener(handler.androidDownloadListener);
}
@override
Future<void> runJavaScript(String javaScript) {
return _webView.evaluateJavascript(javaScript);
}
@override
Future<Object> runJavaScriptReturningResult(String javaScript) async {
final String? result = await _webView.evaluateJavascript(javaScript);
if (result == null) {
return '';
} else if (result == 'true') {
return true;
} else if (result == 'false') {
return false;
}
return num.tryParse(result) ?? result;
}
@override
Future<void> addJavaScriptChannel(
JavaScriptChannelParams javaScriptChannelParams,
) {
final AndroidJavaScriptChannelParams androidJavaScriptParams =
javaScriptChannelParams is AndroidJavaScriptChannelParams
? javaScriptChannelParams
: AndroidJavaScriptChannelParams.fromJavaScriptChannelParams(
javaScriptChannelParams);
// When JavaScript channel with the same name exists make sure to remove it
// before registering the new channel.
if (_javaScriptChannelParams.containsKey(androidJavaScriptParams.name)) {
_webView
.removeJavaScriptChannel(androidJavaScriptParams._javaScriptChannel);
}
_javaScriptChannelParams[androidJavaScriptParams.name] =
androidJavaScriptParams;
return _webView
.addJavaScriptChannel(androidJavaScriptParams._javaScriptChannel);
}
@override
Future<void> removeJavaScriptChannel(String javaScriptChannelName) async {
final AndroidJavaScriptChannelParams? javaScriptChannelParams =
_javaScriptChannelParams[javaScriptChannelName];
if (javaScriptChannelParams == null) {
return;
}
_javaScriptChannelParams.remove(javaScriptChannelName);
return _webView
.removeJavaScriptChannel(javaScriptChannelParams._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<Offset> getScrollPosition() {
return _webView.getScrollPosition();
}
@override
Future<void> enableZoom(bool enabled) =>
_webView.settings.setSupportZoom(enabled);
@override
Future<void> setBackgroundColor(Color color) =>
_webView.setBackgroundColor(color);
@override
Future<void> setJavaScriptMode(JavaScriptMode javaScriptMode) =>
_webView.settings
.setJavaScriptEnabled(javaScriptMode == JavaScriptMode.unrestricted);
@override
Future<void> setUserAgent(String? userAgent) =>
_webView.settings.setUserAgentString(userAgent);
/// Sets the restrictions that apply on automatic media playback.
Future<void> setMediaPlaybackRequiresUserGesture(bool require) {
return _webView.settings.setMediaPlaybackRequiresUserGesture(require);
}
}
/// An implementation of [JavaScriptChannelParams] with the Android WebView API.
///
/// See [AndroidWebViewController.addJavaScriptChannel].
@immutable
class AndroidJavaScriptChannelParams extends JavaScriptChannelParams {
/// Constructs a [AndroidJavaScriptChannelParams].
AndroidJavaScriptChannelParams({
required super.name,
required super.onMessageReceived,
@visibleForTesting
AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(),
}) : assert(name.isNotEmpty),
_javaScriptChannel = webViewProxy.createJavaScriptChannel(
name,
postMessage: withWeakRefenceTo(
onMessageReceived,
(WeakReference<void Function(JavaScriptMessage)> weakReference) {
return (
String message,
) {
if (weakReference.target != null) {
weakReference.target!(
JavaScriptMessage(message: message),
);
}
};
},
),
);
/// Constructs a [AndroidJavaScriptChannelParams] using a
/// [JavaScriptChannelParams].
AndroidJavaScriptChannelParams.fromJavaScriptChannelParams(
JavaScriptChannelParams params, {
@visibleForTesting
AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(),
}) : this(
name: params.name,
onMessageReceived: params.onMessageReceived,
webViewProxy: webViewProxy,
);
final android_webview.JavaScriptChannel _javaScriptChannel;
}
/// Object specifying creation parameters for creating a [AndroidWebViewWidget].
///
/// When adding additional fields make sure they can be null or have a default
/// value to avoid breaking changes. See [PlatformWebViewWidgetCreationParams] for
/// more information.
@immutable
class AndroidWebViewWidgetCreationParams
extends PlatformWebViewWidgetCreationParams {
/// Creates [AndroidWebWidgetCreationParams].
AndroidWebViewWidgetCreationParams({
super.key,
required super.controller,
super.layoutDirection,
super.gestureRecognizers,
this.displayWithHybridComposition = false,
@visibleForTesting InstanceManager? instanceManager,
@visibleForTesting
this.platformViewsServiceProxy = const PlatformViewsServiceProxy(),
}) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
/// Constructs a [WebKitWebViewWidgetCreationParams] using a
/// [PlatformWebViewWidgetCreationParams].
AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams(
PlatformWebViewWidgetCreationParams params, {
bool displayWithHybridComposition = false,
@visibleForTesting InstanceManager? instanceManager,
@visibleForTesting PlatformViewsServiceProxy platformViewsServiceProxy =
const PlatformViewsServiceProxy(),
}) : this(
key: params.key,
controller: params.controller,
layoutDirection: params.layoutDirection,
gestureRecognizers: params.gestureRecognizers,
displayWithHybridComposition: displayWithHybridComposition,
instanceManager: instanceManager,
platformViewsServiceProxy: platformViewsServiceProxy,
);
/// Maintains instances used to communicate with the native objects they
/// represent.
///
/// This field is exposed for testing purposes only and should not be used
/// outside of tests.
@visibleForTesting
final InstanceManager instanceManager;
/// Proxy that provides access to the platform views service.
///
/// This service allows creating and controlling platform-specific views.
@visibleForTesting
final PlatformViewsServiceProxy platformViewsServiceProxy;
/// Whether the [WebView] will be displayed using the Hybrid Composition
/// PlatformView implementation.
///
/// For most use cases, this flag should be set to false. Hybrid Composition
/// can have performance costs but doesn't have the limitation of rendering to
/// an Android SurfaceTexture. See
/// * https://flutter.dev/docs/development/platform-integration/platform-views#performance
/// * https://github.com/flutter/flutter/issues/104889
/// * https://github.com/flutter/flutter/issues/116954
///
/// Defaults to false.
final bool displayWithHybridComposition;
}
/// An implementation of [PlatformWebViewWidget] with the Android WebView API.
class AndroidWebViewWidget extends PlatformWebViewWidget {
/// Constructs a [WebKitWebViewWidget].
AndroidWebViewWidget(PlatformWebViewWidgetCreationParams params)
: super.implementation(
params is AndroidWebViewWidgetCreationParams
? params
: AndroidWebViewWidgetCreationParams
.fromPlatformWebViewWidgetCreationParams(params),
);
AndroidWebViewWidgetCreationParams get _androidParams =>
params as AndroidWebViewWidgetCreationParams;
@override
Widget build(BuildContext context) {
return PlatformViewLink(
key: _androidParams.key,
viewType: 'plugins.flutter.io/webview',
surfaceFactory: (
BuildContext context,
PlatformViewController controller,
) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: _androidParams.gestureRecognizers,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return _initAndroidView(
params,
displayWithHybridComposition:
_androidParams.displayWithHybridComposition,
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
}
AndroidViewController _initAndroidView(
PlatformViewCreationParams params, {
required bool displayWithHybridComposition,
}) {
if (displayWithHybridComposition) {
return _androidParams.platformViewsServiceProxy.initExpensiveAndroidView(
id: params.id,
viewType: 'plugins.flutter.io/webview',
layoutDirection: _androidParams.layoutDirection,
creationParams: _androidParams.instanceManager.getIdentifier(
(_androidParams.controller as AndroidWebViewController)._webView),
creationParamsCodec: const StandardMessageCodec(),
);
} else {
return _androidParams.platformViewsServiceProxy.initSurfaceAndroidView(
id: params.id,
viewType: 'plugins.flutter.io/webview',
layoutDirection: _androidParams.layoutDirection,
creationParams: _androidParams.instanceManager.getIdentifier(
(_androidParams.controller as AndroidWebViewController)._webView),
creationParamsCodec: const StandardMessageCodec(),
);
}
}
}