Added Android pull-to-refresh setSize method and size option, Updated Android androidx.webkit:webkit to 1.4.0, androidx.browser:browser to 1.3.0, androidx.appcompat:appcompat to 1.2.0, updated docs

This commit is contained in:
Lorenzo Pichilli 2021-03-19 17:34:32 +01:00
parent 6f356be623
commit 97edbe158f
19 changed files with 495 additions and 32 deletions

View File

@ -2,8 +2,10 @@
- Added `WebMessageChannel` and `WebMessageListener` features
- Added `canScrollVertically` and `canScrollHorizontally` webview methods
- Added Android pull-to-refresh `setSize` method and `size` option
- `AndroidInAppWebViewController.getCurrentWebViewPackage` is available now starting from Android API 21+
- Updated Android Gradle distributionUrl version to `5.6.4`
- Updated Android `androidx.webkit:webkit` to `1.4.0`, `androidx.browser:browser` to `1.3.0`, `androidx.appcompat:appcompat` to `1.2.0`
- Attempt to fix "InAppBrowserActivity.onCreate NullPointerException - Attempt to invoke virtual method 'java.lang.String android.os.Bundle.getString(java.lang.String)' on a null object reference" [#665](https://github.com/pichillilorenzo/flutter_inappwebview/issues/665)
- Fixed "[iOS] Application crashes when processing onCreateWindow" [#579](https://github.com/pichillilorenzo/flutter_inappwebview/issues/579)
- Fixed wrong mapping of `NavigationAction` class on Android for `androidHasGesture` and `androidIsRedirect` properties

View File

@ -45,9 +45,9 @@ android {
}
}
dependencies {
implementation 'androidx.webkit:webkit:1.3.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.appcompat:appcompat:1.2.0-rc02'
implementation 'androidx.webkit:webkit:1.4.0'
implementation 'androidx.browser:browser:1.3.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.squareup.okhttp3:mockwebserver:3.14.7'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}

View File

@ -229,8 +229,7 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle
result.success((webView != null) ? webView.getCopyBackForwardList() : null);
break;
case "startSafeBrowsing":
if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 &&
WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
WebViewCompat.startSafeBrowsing(webView.getContext(), new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean success) {
@ -514,16 +513,14 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle
}
break;
case "createWebMessageChannel":
if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) {
if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) {
result.success(webView.createCompatWebMessageChannel().toMap());
} else {
result.success(null);
}
break;
case "postWebMessage":
if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) {
if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) {
Map<String, Object> message = (Map<String, Object>) call.argument("message");
String targetOrigin = (String) call.argument("targetOrigin");
List<WebMessagePortCompat> ports = new ArrayList<>();

View File

@ -12,8 +12,10 @@ import androidx.webkit.WebViewFeature;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
@ -48,13 +50,22 @@ public class InAppWebViewStatic implements MethodChannel.MethodCallHandler {
}
break;
case "getSafeBrowsingPrivacyPolicyUrl":
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL)) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL)) {
result.success(WebViewCompat.getSafeBrowsingPrivacyPolicyUrl().toString());
} else
result.success(null);
break;
case "setSafeBrowsingWhitelist":
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST)) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ALLOWLIST)) {
Set<String> hosts = new HashSet<>((List<String>) call.argument("hosts"));
WebViewCompat.setSafeBrowsingAllowlist(hosts, new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean value) {
result.success(value);
}
});
}
else if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST)) {
List<String> hosts = (List<String>) call.argument("hosts");
WebViewCompat.setSafeBrowsingWhitelist(hosts, new ValueCallback<Boolean>() {
@Override
@ -62,8 +73,7 @@ public class InAppWebViewStatic implements MethodChannel.MethodCallHandler {
result.success(value);
}
});
}
else
} else
result.success(false);
break;
case "getCurrentWebViewPackage":

View File

@ -1619,7 +1619,6 @@ final public class InAppWebView extends InputAwareWebView {
return computeHorizontalScrollRange() > computeHorizontalScrollExtent();
}
@TargetApi(Build.VERSION_CODES.M)
public WebMessageChannel createCompatWebMessageChannel() {
String id = UUID.randomUUID().toString();
WebMessageChannel webMessageChannel = new WebMessageChannel(id, this);

View File

@ -71,6 +71,8 @@ public class PullToRefreshLayout extends SwipeRefreshLayout implements MethodCha
setDistanceToTriggerSync(options.distanceToTriggerSync);
if (options.slingshotDistance != null)
setSlingshotDistance(options.slingshotDistance);
if (options.size != null)
setSize(options.size);
}
@Override
@ -124,6 +126,13 @@ public class PullToRefreshLayout extends SwipeRefreshLayout implements MethodCha
case "getDefaultSlingshotDistance":
result.success(SwipeRefreshLayout.DEFAULT_SLINGSHOT_DISTANCE);
break;
case "setSize":
{
Integer size = (Integer) call.argument("size");
setSize(size);
}
result.success(true);
break;
default:
result.notImplemented();
}

View File

@ -19,6 +19,8 @@ public class PullToRefreshOptions implements Options<PullToRefreshLayout> {
public Integer distanceToTriggerSync;
@Nullable
public Integer slingshotDistance;
@Nullable
public Integer size;
public PullToRefreshOptions parse(Map<String, Object> options) {
for (Map.Entry<String, Object> pair : options.entrySet()) {
@ -44,6 +46,9 @@ public class PullToRefreshOptions implements Options<PullToRefreshLayout> {
case "slingshotDistance":
slingshotDistance = (Integer) value;
break;
case "size":
size = (Integer) value;
break;
}
}
@ -57,6 +62,7 @@ public class PullToRefreshOptions implements Options<PullToRefreshLayout> {
options.put("backgroundColor", backgroundColor);
options.put("distanceToTriggerSync", distanceToTriggerSync);
options.put("slingshotDistance", slingshotDistance);
options.put("size", size);
return options;
}

View File

@ -1 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"android":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.5-nullsafety/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.2.0-nullsafety/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.1.0-nullsafety.3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-03-13 14:56:36.936663","version":"2.1.0-10.0.pre"}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"android":[{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-2.0.0-nullsafety/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.1.0+2/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.5-nullsafety/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.2.0-nullsafety/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.1.0-nullsafety.3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-03-19 15:15:51.579916","version":"2.1.0-10.0.pre"}

View File

@ -4908,6 +4908,12 @@ setTimeout(function() {
final pullToRefreshController = PullToRefreshController(
options: PullToRefreshOptions(
color: Colors.blue,
size: AndroidPullToRefreshSize.DEFAULT,
backgroundColor: Colors.grey,
enabled: true,
slingshotDistance: 150,
distanceToTriggerSync: 150,
attributedTitle: IOSNSAttributedString(string: "test")
),
onRefresh: () {
@ -4938,6 +4944,119 @@ setTimeout(function() {
expect(currentUrl, 'https://github.com/flutter');
});
group('WebMessage', () {
testWidgets('WebMessageChannel', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer webMessageCompleter = Completer<String>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
initialData: InAppWebViewInitialData(
data: """
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebMessageChannel Test</title>
</head>
<body>
<button id="button" onclick="port.postMessage(input.value);" />Send</button>
<br />
<input id="input" type="text" value="JavaScript To Native" />
<script>
var port;
window.addEventListener('message', function(event) {
if (event.data == 'capturePort') {
if (event.ports[0] != null) {
port = event.ports[0];
port.onmessage = function (event) {
console.log(event.data);
};
}
}
}, false);
</script>
</body>
</html>
"""),
onWebViewCreated: (controller) {
controllerCompleter.complete(controller);
},
onConsoleMessage: (controller, consoleMessage) {
webMessageCompleter.complete(consoleMessage.message);
},
onLoadStop: (controller, url) async {
var webMessageChannel = await controller.createWebMessageChannel();
var port1 = webMessageChannel!.port1;
var port2 = webMessageChannel.port2;
await port1.setWebMessageCallback((message) async {
await port1.postMessage(WebMessage(data: message! + " and back"));
});
await controller.postWebMessage(message: WebMessage(data: "capturePort", ports: [port2]), targetOrigin: Uri.parse("*"));
await controller.evaluateJavascript(source: "document.getElementById('button').click();");
},
),
),
);
await controllerCompleter.future;
final String message = await webMessageCompleter.future;
expect(message, 'JavaScript To Native and back');
});
testWidgets('WebMessageListener', (WidgetTester tester) async {
final Completer controllerCompleter = Completer<InAppWebViewController>();
final Completer<void> pageLoaded = Completer<void>();
final Completer webMessageCompleter = Completer<String>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: InAppWebView(
key: GlobalKey(),
onWebViewCreated: (controller) async {
await controller.addWebMessageListener(WebMessageListener(
jsObjectName: "myTestObj",
allowedOriginRules: Set.from(["https://*.example.com"]),
onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) {
expect(sourceOrigin.toString(), "https://www.example.com");
expect(isMainFrame, true);
replyProxy.postMessage(message! + " and back");
},
));
controllerCompleter.complete(controller);
},
onConsoleMessage: (controller, consoleMessage) {
webMessageCompleter.complete(consoleMessage.message);
},
onLoadStop: (controller, url) async {
if (url.toString() == "https://www.example.com/") {
pageLoaded.complete();
}
},
),
),
);
final controller = await controllerCompleter.future;
await controller.loadUrl(urlRequest: URLRequest(url: Uri.parse("https://www.example.com/")));
await pageLoaded.future;
await controller.evaluateJavascript(source: """
myTestObj.addEventListener('message', function(event) {
console.log(event.data);
});
myTestObj.postMessage('JavaScript To Native');
""");
final String message = await webMessageCompleter.future;
expect(message, 'JavaScript To Native and back');
});
});
group('android methods', () {
testWidgets('clearSslPreferences', (WidgetTester tester) async {
final Completer controllerCompleter =

View File

@ -152,7 +152,8 @@ class AndroidWebViewFeature {
const AndroidWebViewFeature._internal(
"SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL");
///
///Use [SAFE_BROWSING_ALLOWLIST] instead.
@Deprecated('Use `SAFE_BROWSING_ALLOWLIST` instead')
static const SAFE_BROWSING_WHITELIST =
const AndroidWebViewFeature._internal("SAFE_BROWSING_WHITELIST");

View File

@ -37,7 +37,9 @@ class ChromeSafariBrowserNotOpenedException implements Exception {
///`android.support.customtabs.action.CustomTabsService` in your `AndroidManifest.xml`
///(you can read more about it here: https://developers.google.com/web/android/custom-tabs/best-practices#applications_targeting_android_11_api_level_30_or_above).
class ChromeSafariBrowser {
late String id;
///View ID used internally.
late final String id;
Map<int, ChromeSafariBrowserMenuItem> _menuItems = new HashMap();
bool _isOpened = false;
late MethodChannel _channel;

View File

@ -41,7 +41,7 @@ class InAppBrowserNotOpenedException implements Exception {
///This class uses the native WebView of the platform.
///The [webViewController] field can be used to access the [InAppWebViewController] API.
class InAppBrowser {
///View ID.
///View ID used internally.
late final String id;
///Context menu used by the browser. It should be set before opening the browser.

View File

@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import '../_static_channel.dart';
import '../../types.dart';
import '../../android/webview_feature.dart';
///Class represents the Android controller that contains only android-specific methods for the WebView.
class AndroidInAppWebViewController {
@ -21,10 +22,10 @@ class AndroidInAppWebViewController {
///URL loads are not guaranteed to be protected by Safe Browsing until after the this method returns true.
///Safe Browsing is not fully supported on all devices. For those devices this method will returns false.
///
///This should not be called if Safe Browsing has been disabled by manifest tag
///or [AndroidInAppWebViewOptions.safeBrowsingEnabled]. This prepares resources used for Safe Browsing.
///This should not be called if Safe Browsing has been disabled by manifest tag or [AndroidInAppWebViewOptions.safeBrowsingEnabled].
///This prepares resources used for Safe Browsing.
///
///**NOTE**: available only on Android 27+.
///This method should only be called if [AndroidWebViewFeature.isFeatureSupported] returns `true` for [AndroidWebViewFeature.START_SAFE_BROWSING].
///
///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#startSafeBrowsing(android.content.Context,%20android.webkit.ValueCallback%3Cjava.lang.Boolean%3E)
Future<bool> startSafeBrowsing() async {
@ -134,7 +135,7 @@ class AndroidInAppWebViewController {
///Returns a URL pointing to the privacy policy for Safe Browsing reporting.
///
///**NOTE**: available only on Android 27+.
///This method should only be called if [AndroidWebViewFeature.isFeatureSupported] returns `true` for [AndroidWebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL].
///
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getSafeBrowsingPrivacyPolicyUrl()
static Future<Uri?> getSafeBrowsingPrivacyPolicyUrl() async {
@ -156,11 +157,11 @@ class AndroidInAppWebViewController {
///
///All other rules, including wildcards, are invalid. The correct syntax for hosts is defined by [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.2.2).
///
///This method should only be called if [AndroidWebViewFeature.isFeatureSupported] returns `true` for [AndroidWebViewFeature.SAFE_BROWSING_ALLOWLIST].
///
///[hosts] represents the list of hosts. This value must never be `null`.
///
///**NOTE**: available only on Android 27+.
///
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#getSafeBrowsingPrivacyPolicyUrl()
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#setSafeBrowsingAllowlist(java.util.Set%3Cjava.lang.String%3E,%20android.webkit.ValueCallback%3Cjava.lang.Boolean%3E)
static Future<bool> setSafeBrowsingWhitelist(
{required List<String> hosts}) async {
Map<String, dynamic> args = <String, dynamic>{};

View File

@ -20,6 +20,7 @@ import '../web_storage/web_storage.dart';
import '../util.dart';
import '../web_message/web_message_channel.dart';
import '../web_message/web_message_listener.dart';
import '../android/webview_feature.dart';
import 'headless_in_app_webview.dart';
import 'in_app_webview.dart';
@ -2090,6 +2091,18 @@ class InAppWebViewController {
return await _channel.invokeMethod('isSecureContext', args);
}
///Creates a message channel to communicate with JavaScript and returns the message channel with ports that represent the endpoints of this message channel.
///The HTML5 message channel functionality is described [here](https://html.spec.whatwg.org/multipage/comms.html#messagechannel).
///
///The returned message channels are entangled and already in started state.
///
///This method should be called when the page is loaded, for example, when the [WebView.onLoadStop] is fired, otherwise the [WebMessageChannel] won't work.
///
///**NOTE for Android**: This method should only be called if [AndroidWebViewFeature.isFeatureSupported] returns `true` for [AndroidWebViewFeature.CREATE_WEB_MESSAGE_CHANNEL].
///
///**NOTE for iOS**: This is implemented using Javascript.
///
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#createWebMessageChannel(android.webkit.WebView)
Future<WebMessageChannel?> createWebMessageChannel() async {
Map<String, dynamic> args = <String, dynamic>{};
Map<String, dynamic>? result = (await _channel.invokeMethod('createWebMessageChannel', args))
@ -2097,6 +2110,16 @@ class InAppWebViewController {
return WebMessageChannel.fromMap(result);
}
///Post a message to main frame. The embedded application can restrict the messages to a certain target origin.
///See [HTML5 spec](https://html.spec.whatwg.org/multipage/comms.html#posting-messages) for how target origin can be used.
///
///A target origin can be set as a wildcard ("*"). However this is not recommended.
///
///**NOTE for Android**: This method should only be called if [AndroidWebViewFeature.isFeatureSupported] returns `true` for [AndroidWebViewFeature.POST_WEB_MESSAGE].
///
///**NOTE for iOS**: This is implemented using Javascript.
///
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#postWebMessage(android.webkit.WebView,%20androidx.webkit.WebMessageCompat,%20android.net.Uri)
Future<void> postWebMessage({required WebMessage message, Uri? targetOrigin}) async {
if (targetOrigin == null) {
targetOrigin = Uri.parse("");
@ -2107,6 +2130,165 @@ class InAppWebViewController {
await _channel.invokeMethod('postWebMessage', args);
}
///Adds a [WebMessageListener] to the WebView and injects a JavaScript object into each frame that the [WebMessageListener] will listen on.
///
///The injected JavaScript object will be named [WebMessageListener.jsObjectName] in the global scope.
///This will inject the JavaScript object in any frame whose origin matches [WebMessageListener.allowedOriginRules]
///for every navigation after this call, and the JavaScript object will be available immediately when the page begins to load.
///
///Each [WebMessageListener.allowedOriginRules] entry must follow the format `SCHEME "://" [ HOSTNAME_PATTERN [ ":" PORT ] ]`, each part is explained in the below table:
///
///<table>
/// <colgroup>
/// <col width="25%">
/// </colgroup>
/// <tbody>
/// <tr>
/// <th>Rule</th>
/// <th>Description</th>
/// <th>Example</th>
/// </tr>
/// <tr>
/// <td>http/https with hostname</td>
/// <td><code translate="no" dir="ltr">SCHEME</code> is http or https; <code translate="no" dir="ltr">HOSTNAME_<wbr>PATTERN</code> is a regular hostname; <code translate="no" dir="ltr">PORT</code> is optional, when not present, the rule will match port <code translate="no" dir="ltr">80</code> for http and port
/// <code translate="no" dir="ltr">443</code> for https.
/// </td>
/// <td>
/// <ul>
/// <li><code translate="no" dir="ltr">https://foobar.com:8080</code> - Matches https:// URL on port 8080, whose normalized
/// host is foobar.com.
/// </li>
/// <li><code translate="no" dir="ltr">https://www.example.com</code> - Matches https:// URL on port 443, whose normalized host
/// is www.example.com.
/// </li>
/// </ul>
/// </td>
/// </tr>
/// <tr>
/// <td>http/https with pattern matching</td>
/// <td><code translate="no" dir="ltr">SCHEME</code> is http or https; <code translate="no" dir="ltr">HOSTNAME_<wbr>PATTERN</code> is a sub-domain matching
/// pattern with a leading <code translate="no" dir="ltr">*.<wbr></code>; <code translate="no" dir="ltr">PORT</code> is optional, when not present, the rule will
/// match port <code translate="no" dir="ltr">80</code> for http and port <code translate="no" dir="ltr">443</code> for https.
/// </td>
/// <td>
/// <ul>
/// <li><code translate="no" dir="ltr">https://*.example.com</code> - Matches https://calendar.example.com and
/// https://foo.bar.example.com but not https://example.com.
/// </li>
/// <li><code translate="no" dir="ltr">https://*.example.com:8080</code> - Matches https://calendar.example.com:8080</li>
/// </ul>
/// </td>
/// </tr>
/// <tr>
/// <td>http/https with IP literal</td>
/// <td><code translate="no" dir="ltr">SCHEME</code> is https or https; <code translate="no" dir="ltr">HOSTNAME_<wbr>PATTERN</code> is IP literal; <code translate="no" dir="ltr">PORT</code> is
/// optional, when not present, the rule will match port <code translate="no" dir="ltr">80</code> for http and port <code translate="no" dir="ltr">443</code>
/// for https.
/// </td>
/// <td>
/// <ul>
/// <li><code translate="no" dir="ltr">https://127.0.0.1</code> - Matches https:// URL on port 443, whose IPv4 address is
/// 127.0.0.1
/// </li>
/// <li><code translate="no" dir="ltr">https://[::1]</code> or <code translate="no" dir="ltr">https://[0:0::1]</code>- Matches any URL to the IPv6 loopback
/// address with port 443.
/// </li>
/// <li><code translate="no" dir="ltr">https://[::1]:99</code> - Matches any https:// URL to the IPv6 loopback on port 99.</li>
/// </ul>
/// </td>
/// </tr>
/// <tr>
/// <td>Custom scheme</td>
/// <td><code translate="no" dir="ltr">SCHEME</code> is a custom scheme; <code translate="no" dir="ltr">HOSTNAME_<wbr>PATTERN</code> and <code translate="no" dir="ltr">PORT</code> must not be
/// present.
/// </td>
/// <td>
/// <ul>
/// <li><code translate="no" dir="ltr">my-app-scheme://</code> - Matches any my-app-scheme:// URL.</li>
/// </ul>
/// </td>
/// </tr>
/// <tr>
/// <td><code translate="no" dir="ltr">*</code></td>
/// <td>Wildcard rule, matches any origin.</td>
/// <td>
/// <ul>
/// <li><code translate="no" dir="ltr">*</code></li>
/// </ul>
/// </td>
/// </tr>
/// </tbody>
///</table>
///
///Note that this is a powerful API, as the JavaScript object will be injected when the frame's origin matches any one of the allowed origins.
///The HTTPS scheme is strongly recommended for security; allowing HTTP origins exposes the injected object to any potential network-based attackers.
///If a wildcard "*" is provided, it will inject the JavaScript object to all frames.
///A wildcard should only be used if the app wants **any** third party web page to be able to use the injected object.
///When using a wildcard, the app must treat received messages as untrustworthy and validate any data carefully.
///
///This method can be called multiple times to inject multiple JavaScript objects.
///
///Let's say the injected JavaScript object is named `myObject`. We will have following methods on that object once it is available to use:
///
///```javascript
/// // Web page (in JavaScript)
/// // message needs to be a JavaScript String, MessagePorts is an optional parameter.
/// myObject.postMessage(message[, MessagePorts]) // on Android
/// myObject.postMessage(message) // on iOS
///
/// // To receive messages posted from the app side, assign a function to the "onmessage"
/// // property. This function should accept a single "event" argument. "event" has a "data"
/// // property, which is the message string from the app side.
/// myObject.onmessage = function(event) { ... }
///
/// // To be compatible with DOM EventTarget's addEventListener, it accepts type and listener
/// // parameters, where type can be only "message" type and listener can only be a JavaScript
/// // function for myObject. An event object will be passed to listener with a "data" property,
/// // which is the message string from the app side.
/// myObject.addEventListener(type, listener)
///
/// // To be compatible with DOM EventTarget's removeEventListener, it accepts type and listener
/// // parameters, where type can be only "message" type and listener can only be a JavaScript
/// // function for myObject.
/// myObject.removeEventListener(type, listener)
///```
///
///We start the communication between JavaScript and the app from the JavaScript side.
///In order to send message from the app to JavaScript, it needs to post a message from JavaScript first,
///so the app will have a [JavaScriptReplyProxy] object to respond. Example:
///
///```javascript
/// // Web page (in JavaScript)
/// myObject.onmessage = function(event) {
/// // prints "Got it!" when we receive the app's response.
/// console.log(event.data);
/// }
/// myObject.postMessage("I'm ready!");
///```
///
///```dart
/// // Flutter App
/// child: InAppWebView(
/// onWebViewCreated: (controller) async {
/// if (!Platform.isAndroid || await AndroidWebViewFeature.isFeatureSupported(AndroidWebViewFeature.WEB_MESSAGE_LISTENER)) {
/// await controller.addWebMessageListener(WebMessageListener(
/// jsObjectName: "myObject",
/// onPostMessage: (message, sourceOrigin, isMainFrame, replyProxy) {
/// // do something about message, sourceOrigin and isMainFrame.
/// replyProxy.postMessage("Got it!");
/// },
/// ));
/// }
/// await controller.loadUrl(urlRequest: URLRequest(url: Uri.parse("https://www.example.com")));
/// },
/// ),
///```
///
///**NOTE for Android**: This method should only be called if [AndroidWebViewFeature.isFeatureSupported] returns `true` for [AndroidWebViewFeature.WEB_MESSAGE_LISTENER].
///
///**NOTE for iOS**: This is implemented using Javascript.
///
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat#addWebMessageListener(android.webkit.WebView,%20java.lang.String,%20java.util.Set%3Cjava.lang.String%3E,%20androidx.webkit.WebViewCompat.WebMessageListener)
Future<void> addWebMessageListener(WebMessageListener webMessageListener) async {
assert(!_webMessageListenerObjNames.contains(webMessageListener.jsObjectName), "jsObjectName ${webMessageListener.jsObjectName} was already added.");
_webMessageListenerObjNames.add(webMessageListener.jsObjectName);
@ -2116,11 +2298,13 @@ class InAppWebViewController {
await _channel.invokeMethod('addWebMessageListener', args);
}
///Returns `true` if the webpage can scroll vertically, otherwise `false`.
Future<bool> canScrollVertically() async {
Map<String, dynamic> args = <String, dynamic>{};
return await _channel.invokeMethod('canScrollVertically', args);
}
///Returns `true` if the webpage can scroll horizontally, otherwise `false`.
Future<bool> canScrollHorizontally() async {
Map<String, dynamic> args = <String, dynamic>{};
return await _channel.invokeMethod('canScrollHorizontally', args);

View File

@ -115,6 +115,15 @@ class PullToRefreshController {
return await _channel?.invokeMethod('getDefaultSlingshotDistance', args);
}
///Sets the size of the refresh indicator. One of [AndroidPullToRefreshSize.DEFAULT], or [AndroidPullToRefreshSize.LARGE].
///
///**NOTE**: Available only on Android.
Future<void> setSize(AndroidPullToRefreshSize size) async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('size', () => size.toValue());
await _channel?.invokeMethod('setSize', args);
}
///Sets the styled title text to display in the refresh control.
///
///**NOTE**: Available only on iOS.

View File

@ -22,6 +22,11 @@ class PullToRefreshOptions {
///**NOTE**: Available only on Android.
int? slingshotDistance;
///The size of the refresh indicator.
///
///**NOTE**: Available only on Android.
AndroidPullToRefreshSize? size;
///The title text to display in the refresh control.
///
///**NOTE**: Available only on iOS.
@ -33,6 +38,7 @@ class PullToRefreshOptions {
this.backgroundColor,
this.distanceToTriggerSync,
this.slingshotDistance,
this.size,
this.attributedTitle});
Map<String, dynamic> toMap() {
@ -42,6 +48,7 @@ class PullToRefreshOptions {
"backgroundColor": backgroundColor?.toHex(),
"distanceToTriggerSync": distanceToTriggerSync,
"slingshotDistance": slingshotDistance,
"size": size?.toValue(),
"attributedTitle": attributedTitle?.toMap() ?? {}
};
}

View File

@ -17,6 +17,8 @@ import 'web_storage/web_storage.dart';
import 'pull_to_refresh/pull_to_refresh_controller.dart';
import 'pull_to_refresh/pull_to_refresh_options.dart';
import 'util.dart';
import 'web_message/web_message_listener.dart';
import 'web_message/web_message_channel.dart';
///This type represents a callback, added with [InAppWebViewController.addJavaScriptHandler], that listens to post messages sent from JavaScript.
///
@ -30,6 +32,13 @@ import 'util.dart';
///In this case, simply return data that you want to send and it will be automatically json encoded using [jsonEncode] from the `dart:convert` library.
typedef dynamic JavaScriptHandlerCallback(List<dynamic> arguments);
///The listener for handling [WebMessageListener] events sent by a `postMessage()` on the injected JavaScript object.
typedef void OnPostMessageCallback(String? message, Uri? sourceOrigin, bool isMainFrame, JavaScriptReplyProxy replyProxy);
///The listener for handling [WebMessagePort] events.
///The message callback methods are called on the main thread.
typedef void WebMessageCallback(String? message);
///Class representing the level of a console message.
class ConsoleMessageLevel {
final int _value;
@ -6775,3 +6784,51 @@ class IOSNSAttributedStringTextEffectStyle {
@override
int get hashCode => _value.hashCode;
}
///Android-specific class representing the size of the refresh indicator.
class AndroidPullToRefreshSize {
final int _value;
const AndroidPullToRefreshSize._internal(this._value);
static final Set<AndroidPullToRefreshSize> values = [
AndroidPullToRefreshSize.DEFAULT,
AndroidPullToRefreshSize.LARGE,
].toSet();
static AndroidPullToRefreshSize? fromValue(int? value) {
if (value != null) {
try {
return AndroidPullToRefreshSize.values
.firstWhere((element) => element.toValue() == value);
} catch (e) {
return null;
}
}
return null;
}
int toValue() => _value;
@override
String toString() {
switch (_value) {
case 0:
return "LARGE";
case 1:
default:
return "DEFAULT";
}
}
///Default size.
static const DEFAULT = const AndroidPullToRefreshSize._internal(1);
///Large size.
static const LARGE = const AndroidPullToRefreshSize._internal(0);
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
}

View File

@ -1,9 +1,17 @@
import 'package:flutter/services.dart';
import '../types.dart';
import '../in_app_webview/in_app_webview_controller.dart';
///The representation of the [HTML5 message channels](https://html.spec.whatwg.org/multipage/web-messaging.html#message-channels).
class WebMessageChannel {
String id;
WebMessagePort port1;
WebMessagePort port2;
///Message Channel ID used internally.
final String id;
///The first [WebMessagePort] object of the channel.
final WebMessagePort port1;
///The second [WebMessagePort] object of the channel.
final WebMessagePort port2;
late MethodChannel _channel;
@ -44,23 +52,43 @@ class WebMessageChannel {
}
}
///The representation of the [HTML5 message ports](https://html.spec.whatwg.org/multipage/comms.html#messageport).
///
///A Message port represents one endpoint of a Message Channel. In Android webview, there is no separate Message Channel object.
///When a message channel is created, both ports are tangled to each other and started.
///See [InAppWebViewController.createWebMessageChannel] for creating a message channel.
///
///When a message port is first created or received via transfer, it does not have a [WebMessageCallback] to receive web messages.
///On Android, the messages are queued until a [WebMessageCallback] is set.
///
///A message port should be closed when it is not used by the embedder application anymore.
///A closed port cannot be transferred or cannot be reopened to send messages.
///Close can be called multiple times.
///
///When a port is transferred to JavaScript, it cannot be used to send or receive messages at the Dart side anymore.
///Different from HTML5 Spec, a port cannot be transferred if one of these has ever happened: i. a message callback was set, ii. a message was posted on it.
///A transferred port cannot be closed by the application, since the ownership is also transferred.
///
///It is possible to transfer both ports of a channel to JavaScript, for example for communication between subframes.
class WebMessagePort {
late final int _index;
Function(String? message)? _onMessage;
WebMessageCallback? _onMessage;
late WebMessageChannel _webMessageChannel;
WebMessagePort({required int index}) {
this._index = index;
}
Future<void> setWebMessageCallback(Function(String? message)? onMessage) async {
///Sets a callback to receive message events on the main thread.
Future<void> setWebMessageCallback(WebMessageCallback? onMessage) async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('index', () => this._index);
await _webMessageChannel._channel.invokeMethod('setWebMessageCallback', args);
this._onMessage = onMessage;
}
///Post a WebMessage to the entangled port.
Future<void> postMessage(WebMessage message) async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('index', () => this._index);
@ -68,6 +96,7 @@ class WebMessagePort {
await _webMessageChannel._channel.invokeMethod('postMessage', args);
}
///Close the message port and free any resources associated with it.
Future<void> close() async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('index', () => this._index);
@ -91,8 +120,13 @@ class WebMessagePort {
}
}
///The Dart representation of the HTML5 PostMessage event.
///See https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces for definition of a MessageEvent in HTML5.
class WebMessage {
///The data of the message.
String? data;
///The ports that are sent with the message.
List<WebMessagePort>? ports;
WebMessage({this.data, this.ports});

View File

@ -1,10 +1,28 @@
import 'package:flutter/services.dart';
import '../in_app_webview/in_app_webview_controller.dart';
import '../types.dart';
///This listener receives messages sent on the JavaScript object which was injected by [InAppWebViewController.addWebMessageListener].
class WebMessageListener {
String jsObjectName;
///The name for the injected JavaScript object.
final String jsObjectName;
///A set of matching rules for the allowed origins.
late Set<String> allowedOriginRules;
JavaScriptReplyProxy? _replyProxy;
Function(String? message, Uri? sourceOrigin, bool isMainFrame, JavaScriptReplyProxy replyProxy)? onPostMessage;
///Event that receives a message sent by a `postMessage()` on the injected JavaScript object.
///
///Note that when the frame is `file:` or `content:` origin, the value of [sourceOrigin] is `null`.
///
///- [message] represents the message from JavaScript.
///- [sourceOrigin] represents the origin of the frame that the message is from.
///- [isMainFrame] is `true` if the message is from the main frame.
///- [replyProxy] is used to reply back to the JavaScript object.
///
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewCompat.WebMessageListener#onPostMessage(android.webkit.WebView,%20androidx.webkit.WebMessageCompat,%20android.net.Uri,%20boolean,%20androidx.webkit.JavaScriptReplyProxy)
OnPostMessageCallback? onPostMessage;
late MethodChannel _channel;
@ -52,6 +70,11 @@ class WebMessageListener {
}
}
///This class represents the JavaScript object injected by [InAppWebViewController.addWebMessageListener].
///An instance will be given by [WebMessageListener.onPostMessage].
///The app can use `postMessage(String)` to talk to the JavaScript context.
///
///There is a 1:1 relationship between this object and the JavaScript object in a frame.
class JavaScriptReplyProxy {
late WebMessageListener _webMessageListener;
@ -59,6 +82,9 @@ class JavaScriptReplyProxy {
this._webMessageListener = webMessageListener;
}
///Post a [message] to the injected JavaScript object which sent this [JavaScriptReplyProxy].
///
///**Official Android API**: https://developer.android.com/reference/androidx/webkit/JavaScriptReplyProxy#postMessage(java.lang.String)
Future<void> postMessage(String message) async {
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('message', () => message);