diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e4de10..b8709b14 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/android/build.gradle b/android/build.gradle index bb894086..a5e831c4 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -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' } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java index 746ba692..5c4ef366 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java @@ -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() { @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 message = (Map) call.argument("message"); String targetOrigin = (String) call.argument("targetOrigin"); List ports = new ArrayList<>(); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java index 2ce006ab..36ea9369 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java @@ -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 hosts = new HashSet<>((List) call.argument("hosts")); + WebViewCompat.setSafeBrowsingAllowlist(hosts, new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } + else if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST)) { List hosts = (List) call.argument("hosts"); WebViewCompat.setSafeBrowsingWhitelist(hosts, new ValueCallback() { @Override @@ -62,8 +73,7 @@ public class InAppWebViewStatic implements MethodChannel.MethodCallHandler { result.success(value); } }); - } - else + } else result.success(false); break; case "getCurrentWebViewPackage": diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java index 9a34cb75..5a43b458 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_webview/InAppWebView.java @@ -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); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshLayout.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshLayout.java index c9b5c0df..ec40fd31 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshLayout.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshLayout.java @@ -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(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshOptions.java index ac213071..81d3af99 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/pull_to_refresh/PullToRefreshOptions.java @@ -19,6 +19,8 @@ public class PullToRefreshOptions implements Options { public Integer distanceToTriggerSync; @Nullable public Integer slingshotDistance; + @Nullable + public Integer size; public PullToRefreshOptions parse(Map options) { for (Map.Entry pair : options.entrySet()) { @@ -44,6 +46,9 @@ public class PullToRefreshOptions implements Options { case "slingshotDistance": slingshotDistance = (Integer) value; break; + case "size": + size = (Integer) value; + break; } } @@ -57,6 +62,7 @@ public class PullToRefreshOptions implements Options { options.put("backgroundColor", backgroundColor); options.put("distanceToTriggerSync", distanceToTriggerSync); options.put("slingshotDistance", slingshotDistance); + options.put("size", size); return options; } diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index f72a8334..abd8f007 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/example/integration_test/webview_flutter_test.dart b/example/integration_test/webview_flutter_test.dart index 592a9f1a..af8a9ccb 100644 --- a/example/integration_test/webview_flutter_test.dart +++ b/example/integration_test/webview_flutter_test.dart @@ -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(); + final Completer webMessageCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialData: InAppWebViewInitialData( + data: """ + + + + + WebMessageChannel Test + + + +
+ + + + + + """), + 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(); + final Completer pageLoaded = Completer(); + final Completer webMessageCompleter = Completer(); + 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 = diff --git a/lib/src/android/webview_feature.dart b/lib/src/android/webview_feature.dart index 69309fdf..6867c1b8 100644 --- a/lib/src/android/webview_feature.dart +++ b/lib/src/android/webview_feature.dart @@ -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"); diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index a95b18a2..a8c37a69 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -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 _menuItems = new HashMap(); bool _isOpened = false; late MethodChannel _channel; diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index f6a70682..451a0de5 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -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. diff --git a/lib/src/in_app_webview/android/in_app_webview_controller.dart b/lib/src/in_app_webview/android/in_app_webview_controller.dart index 4df59218..d4154f5e 100644 --- a/lib/src/in_app_webview/android/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/android/in_app_webview_controller.dart @@ -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 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 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 setSafeBrowsingWhitelist( {required List hosts}) async { Map args = {}; diff --git a/lib/src/in_app_webview/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart index a3c81e48..d126372d 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -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 createWebMessageChannel() async { Map args = {}; Map? 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 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: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + ///
RuleDescriptionExample
http/https with hostnameSCHEME is http or https; HOSTNAME_PATTERN is a regular hostname; PORT is optional, when not present, the rule will match port 80 for http and port + /// 443 for https. + /// + ///
    + ///
  • https://foobar.com:8080 - Matches https:// URL on port 8080, whose normalized + /// host is foobar.com. + ///
  • + ///
  • https://www.example.com - Matches https:// URL on port 443, whose normalized host + /// is www.example.com. + ///
  • + ///
+ ///
http/https with pattern matchingSCHEME is http or https; HOSTNAME_PATTERN is a sub-domain matching + /// pattern with a leading *.; PORT is optional, when not present, the rule will + /// match port 80 for http and port 443 for https. + /// + ///
    + ///
  • https://*.example.com - Matches https://calendar.example.com and + /// https://foo.bar.example.com but not https://example.com. + ///
  • + ///
  • https://*.example.com:8080 - Matches https://calendar.example.com:8080
  • + ///
+ ///
http/https with IP literalSCHEME is https or https; HOSTNAME_PATTERN is IP literal; PORT is + /// optional, when not present, the rule will match port 80 for http and port 443 + /// for https. + /// + ///
    + ///
  • https://127.0.0.1 - Matches https:// URL on port 443, whose IPv4 address is + /// 127.0.0.1 + ///
  • + ///
  • https://[::1] or https://[0:0::1]- Matches any URL to the IPv6 loopback + /// address with port 443. + ///
  • + ///
  • https://[::1]:99 - Matches any https:// URL to the IPv6 loopback on port 99.
  • + ///
+ ///
Custom schemeSCHEME is a custom scheme; HOSTNAME_PATTERN and PORT must not be + /// present. + /// + ///
    + ///
  • my-app-scheme:// - Matches any my-app-scheme:// URL.
  • + ///
+ ///
*Wildcard rule, matches any origin. + ///
    + ///
  • *
  • + ///
+ ///
+ /// + ///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 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 canScrollVertically() async { Map args = {}; return await _channel.invokeMethod('canScrollVertically', args); } + ///Returns `true` if the webpage can scroll horizontally, otherwise `false`. Future canScrollHorizontally() async { Map args = {}; return await _channel.invokeMethod('canScrollHorizontally', args); diff --git a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart index 5acb3b77..00ccde54 100644 --- a/lib/src/pull_to_refresh/pull_to_refresh_controller.dart +++ b/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -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 setSize(AndroidPullToRefreshSize size) async { + Map args = {}; + 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. diff --git a/lib/src/pull_to_refresh/pull_to_refresh_options.dart b/lib/src/pull_to_refresh/pull_to_refresh_options.dart index 7267c298..da86e682 100644 --- a/lib/src/pull_to_refresh/pull_to_refresh_options.dart +++ b/lib/src/pull_to_refresh/pull_to_refresh_options.dart @@ -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 toMap() { @@ -42,6 +48,7 @@ class PullToRefreshOptions { "backgroundColor": backgroundColor?.toHex(), "distanceToTriggerSync": distanceToTriggerSync, "slingshotDistance": slingshotDistance, + "size": size?.toValue(), "attributedTitle": attributedTitle?.toMap() ?? {} }; } diff --git a/lib/src/types.dart b/lib/src/types.dart index f35752ab..01c02b52 100755 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -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 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 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; +} \ No newline at end of file diff --git a/lib/src/web_message/web_message_channel.dart b/lib/src/web_message/web_message_channel.dart index 9938c617..d2dffaf8 100644 --- a/lib/src/web_message/web_message_channel.dart +++ b/lib/src/web_message/web_message_channel.dart @@ -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 setWebMessageCallback(Function(String? message)? onMessage) async { + ///Sets a callback to receive message events on the main thread. + Future setWebMessageCallback(WebMessageCallback? onMessage) async { Map args = {}; args.putIfAbsent('index', () => this._index); await _webMessageChannel._channel.invokeMethod('setWebMessageCallback', args); this._onMessage = onMessage; } + ///Post a WebMessage to the entangled port. Future postMessage(WebMessage message) async { Map args = {}; 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 close() async { Map args = {}; 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? ports; WebMessage({this.data, this.ports}); diff --git a/lib/src/web_message/web_message_listener.dart b/lib/src/web_message/web_message_listener.dart index 91b811c3..d8fe8b17 100644 --- a/lib/src/web_message/web_message_listener.dart +++ b/lib/src/web_message/web_message_listener.dart @@ -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 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 postMessage(String message) async { Map args = {}; args.putIfAbsent('message', () => message);