From 6a486b2fa93b721607624d16cdd7326c743a112b Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Sun, 12 Nov 2023 18:56:11 +0100 Subject: [PATCH] Added requestPostMessageChannel, postMessage, isEngagementSignalsApiAvailable methods on ChromeSafariBrowser for Android, Added onMessageChannelReady, onPostMessage, onVerticalScrollEvent, onGreatestScrollPercentageIncreased, onSessionEnded events on ChromeSafariBrowser for Android --- CHANGELOG.md | 5 + .../ChromeCustomTabsActivity.java | 57 ++++++- .../ChromeCustomTabsChannelDelegate.java | 73 ++++++++- .../TrustedWebActivity.java | 8 - .../android/app/src/main/AndroidManifest.xml | 3 +- .../app/src/main/res/values/strings.xml | 6 + .../chrome_safari_browser/custom_tabs.dart | 47 ++++++ .../trusted_web_activity.dart | 12 +- example/integration_test/constants.dart | 4 + example/integration_test/util.dart | 21 +++ .../chrome_safari_browser_example.screen.dart | 17 +++ .../chrome_safari_browser.dart | 135 ++++++++++++++++- .../custom_tabs_post_message_result_type.dart | 36 +++++ ...ustom_tabs_post_message_result_type.g.dart | 141 ++++++++++++++++++ lib/src/types/main.dart | 2 + pubspec.yaml | 2 +- 16 files changed, 545 insertions(+), 24 deletions(-) create mode 100644 lib/src/types/custom_tabs_post_message_result_type.dart create mode 100644 lib/src/types/custom_tabs_post_message_result_type.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 419fb13b..dc6727a4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.0.0-beta.27 + +- Added `requestPostMessageChannel`, `postMessage`, `isEngagementSignalsApiAvailable` methods on `ChromeSafariBrowser` for Android +- Added `onMessageChannelReady`, `onPostMessage`, `onVerticalScrollEvent`, `onGreatestScrollPercentageIncreased`, `onSessionEnded` events on `ChromeSafariBrowser` for Android + ## 6.0.0-beta.26 - Throw an error if any controller is used after being disposed diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsActivity.java index 5ff051ad..c3aa5e2d 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsActivity.java @@ -9,6 +9,7 @@ import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.RemoteException; import android.widget.RemoteViews; import androidx.annotation.CallSuper; @@ -19,6 +20,7 @@ import androidx.browser.customtabs.CustomTabsCallback; import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsService; import androidx.browser.customtabs.CustomTabsSession; +import androidx.browser.customtabs.EngagementSignalsCallback; import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.types.AndroidResource; @@ -32,12 +34,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import io.flutter.Log; import io.flutter.plugin.common.MethodChannel; public class ChromeCustomTabsActivity extends Activity implements Disposable { protected static final String LOG_TAG = "CustomTabsActivity"; public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_chromesafaribrowser_"; - + public String id; @Nullable public CustomTabsIntent.Builder builder; @@ -76,7 +79,7 @@ public class ChromeCustomTabsActivity extends Activity implements Disposable { Bundle b = getIntent().getExtras(); if (b == null) return; - + id = b.getString("id"); String managerId = b.getString("managerId"); @@ -143,13 +146,22 @@ public class ChromeCustomTabsActivity extends Activity implements Disposable { } @Override - public void extraCallback(@NonNull String callbackName, Bundle args) {} + public void extraCallback(@NonNull String callbackName, Bundle args) { + } @Override - public void onMessageChannelReady(Bundle extras) {} + public void onMessageChannelReady(Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onMessageChannelReady(); + } + } @Override - public void onPostMessage(@NonNull String message, Bundle extras) {} + public void onPostMessage(@NonNull String message, Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onPostMessage(message); + } + } @Override public void onRelationshipValidationResult(@CustomTabsService.Relation int relation, @@ -198,8 +210,41 @@ public class ChromeCustomTabsActivity extends Activity implements Disposable { return customTabActivityHelper.mayLaunchUrl(uri, null, bundleOtherLikelyURLs); } + @CallSuper public void customTabsConnected() { customTabsSession = customTabActivityHelper.getSession(); + + if (customTabsSession != null) { + try { + Bundle bundle = new Bundle(); + if (customTabsSession.isEngagementSignalsApiAvailable(bundle)) { + customTabsSession.setEngagementSignalsCallback(new EngagementSignalsCallback() { + @Override + public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onVerticalScrollEvent(isDirectionUp); + } + } + + @Override + public void onGreatestScrollPercentageIncreased(int scrollPercentage, @NonNull Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onGreatestScrollPercentageIncreased(scrollPercentage); + } + } + + @Override + public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras) { + if (channelDelegate != null) { + channelDelegate.onSessionEnded(didUserInteract); + } + } + }, bundle); + } + } catch (Exception ignored) { + } + } + // avoid webpage reopen if isBindSuccess is false: onServiceConnected->launchUrl if (isBindSuccess && initialUrl != null) { launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs); @@ -248,7 +293,7 @@ public class ChromeCustomTabsActivity extends Activity implements Disposable { } for (CustomTabsMenuItem menuItem : menuItems) { - builder.addMenuItem(menuItem.getLabel(), + builder.addMenuItem(menuItem.getLabel(), createPendingIntent(menuItem.getId())); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java index 137624a8..aabfb159 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java @@ -3,9 +3,12 @@ package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.app.Activity; import android.content.Intent; import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabsService; import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsActionButton; @@ -84,6 +87,35 @@ public class ChromeCustomTabsChannelDelegate extends ChannelDelegateImpl { result.success(false); } break; + case "requestPostMessageChannel": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + String sourceOrigin = (String) call.argument("sourceOrigin"); + String targetOrigin = (String) call.argument("targetOrigin"); + result.success(chromeCustomTabsActivity.customTabsSession.requestPostMessageChannel(Uri.parse(sourceOrigin), + targetOrigin != null ? Uri.parse(targetOrigin) : null, new Bundle())); + } else { + result.success(false); + } + break; + case "postMessage": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + String message = (String) call.argument("message"); + result.success(chromeCustomTabsActivity.customTabsSession.postMessage(message, new Bundle())); + } else { + result.success(CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR); + } + break; + case "isEngagementSignalsApiAvailable": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + try { + result.success(chromeCustomTabsActivity.customTabsSession.isEngagementSignalsApiAvailable(new Bundle())); + } catch (Exception e) { + result.success(false); + } + } else { + result.success(false); + } + break; case "close": if (chromeCustomTabsActivity != null) { chromeCustomTabsActivity.onStop(); @@ -131,7 +163,7 @@ public class ChromeCustomTabsChannelDelegate extends ChannelDelegateImpl { channel.invokeMethod("onCompletedInitialLoad", obj); } - public void onNavigationEvent(int navigationEvent) {; + public void onNavigationEvent(int navigationEvent) { MethodChannel channel = getChannel(); if (channel == null) return; Map obj = new HashMap<>(); @@ -175,6 +207,45 @@ public class ChromeCustomTabsChannelDelegate extends ChannelDelegateImpl { channel.invokeMethod("onRelationshipValidationResult", obj); } + public void onMessageChannelReady() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onMessageChannelReady", obj); + } + + public void onPostMessage(@NonNull String message) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("message", message); + channel.invokeMethod("onPostMessage", obj); + } + + public void onVerticalScrollEvent(boolean isDirectionUp) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("isDirectionUp", isDirectionUp); + channel.invokeMethod("onVerticalScrollEvent", obj); + } + + public void onGreatestScrollPercentageIncreased(int scrollPercentage) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("scrollPercentage", scrollPercentage); + channel.invokeMethod("onGreatestScrollPercentageIncreased", obj); + } + + public void onSessionEnded(boolean didUserInteract) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("didUserInteract", didUserInteract); + channel.invokeMethod("onSessionEnded", obj); + } + @Override public void dispose() { super.dispose(); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/TrustedWebActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/TrustedWebActivity.java index aee48e5b..2b1dff68 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/TrustedWebActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/TrustedWebActivity.java @@ -41,14 +41,6 @@ public class TrustedWebActivity extends ChromeCustomTabsActivity { referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE); } - @Override - public void customTabsConnected() { - customTabsSession = customTabActivityHelper.getSession(); - if (initialUrl != null) { - launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs); - } - } - private void prepareCustomTabs() { CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) { diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 4e09bbbb..bb9ccad5 100755 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -93,6 +93,7 @@ android:name="io.flutter.embedded_views_preview" android:value="true" /> - + diff --git a/example/android/app/src/main/res/values/strings.xml b/example/android/app/src/main/res/values/strings.xml index 0d428552..73aa4128 100755 --- a/example/android/app/src/main/res/values/strings.xml +++ b/example/android/app/src/main/res/values/strings.xml @@ -6,6 +6,12 @@ \"target\": { \"namespace\": \"web\", \"site\": \"https://flutter.dev\"} + }, + { + \"relation\": [\"delegate_permission/common.handle_all_urls\"], + \"target\": { + \"namespace\": \"web\", + \"site\": \"https://inappwebview.dev\"} }] \ No newline at end of file diff --git a/example/integration_test/chrome_safari_browser/custom_tabs.dart b/example/integration_test/chrome_safari_browser/custom_tabs.dart index 80b64d3d..7982a181 100644 --- a/example/integration_test/chrome_safari_browser/custom_tabs.dart +++ b/example/integration_test/chrome_safari_browser/custom_tabs.dart @@ -170,5 +170,52 @@ void customTabs() { expect(await ChromeSafariBrowser.getMaxToolbarItems(), greaterThanOrEqualTo(0)); }); + + skippableTest('request and send post messages', () async { + var chromeSafariBrowser = MyChromeSafariBrowser(); + expect(chromeSafariBrowser.isOpened(), false); + + await chromeSafariBrowser.open( + url: TEST_CUSTOM_TABS_POST_MESSAGE_URL, + settings: ChromeSafariBrowserSettings(isSingleInstance: true)); + await expectLater(chromeSafariBrowser.opened.future, completes); + expect(chromeSafariBrowser.isOpened(), true); + + await expectLater( + chromeSafariBrowser.navigationFinished.future, completes); + expect( + await chromeSafariBrowser.requestPostMessageChannel( + sourceOrigin: WebUri(TEST_CUSTOM_TABS_POST_MESSAGE_URL.origin)), + true); + await expectLater( + chromeSafariBrowser.messageChannelReady.future, completes); + expect(await chromeSafariBrowser.postMessage("Message from Flutter"), + CustomTabsPostMessageResultType.SUCCESS); + await expectLater(chromeSafariBrowser.postMessageReceived.future, + completion("Message from JavaScript")); + + await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); + await chromeSafariBrowser.close(); + await expectLater(chromeSafariBrowser.closed.future, completes); + expect(chromeSafariBrowser.isOpened(), false); + }); + + skippableTest('Engagement Signals Api', () async { + var chromeSafariBrowser = MyChromeSafariBrowser(); + expect(chromeSafariBrowser.isOpened(), false); + + await chromeSafariBrowser.open( + url: TEST_URL_1, + settings: ChromeSafariBrowserSettings(isSingleInstance: true)); + await expectLater(chromeSafariBrowser.opened.future, completes); + + await expectLater( + chromeSafariBrowser.isEngagementSignalsApiAvailable(), completes); + + await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); + await chromeSafariBrowser.close(); + await expectLater(chromeSafariBrowser.closed.future, completes); + expect(chromeSafariBrowser.isOpened(), false); + }); }, skip: shouldSkip); } diff --git a/example/integration_test/chrome_safari_browser/trusted_web_activity.dart b/example/integration_test/chrome_safari_browser/trusted_web_activity.dart index 8e5844f7..fe5ef829 100644 --- a/example/integration_test/chrome_safari_browser/trusted_web_activity.dart +++ b/example/integration_test/chrome_safari_browser/trusted_web_activity.dart @@ -13,12 +13,12 @@ void trustedWebActivity() { expect(chromeSafariBrowser.isOpened(), false); await chromeSafariBrowser.open( - url: TEST_URL_1, + url: TEST_TWA_URL, settings: ChromeSafariBrowserSettings(isTrustedWebActivity: true)); await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { - await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); + await chromeSafariBrowser.open(url: TEST_TWA_URL); }, throwsAssertionError); await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); @@ -32,13 +32,13 @@ void trustedWebActivity() { expect(chromeSafariBrowser.isOpened(), false); await chromeSafariBrowser.open( - url: TEST_URL_1, + url: TEST_TWA_URL, settings: ChromeSafariBrowserSettings( isTrustedWebActivity: true, isSingleInstance: true)); await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { - await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); + await chromeSafariBrowser.open(url: TEST_TWA_URL); }, throwsAssertionError); await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); @@ -57,11 +57,11 @@ void trustedWebActivity() { expect( await chromeSafariBrowser.validateRelationship( relation: CustomTabsRelationType.USE_AS_ORIGIN, - origin: TEST_CROSS_PLATFORM_URL_1), + origin: TEST_TWA_URL), true); expect( await chromeSafariBrowser.relationshipValidationResult.future, true); - await chromeSafariBrowser.launchUrl(url: TEST_CROSS_PLATFORM_URL_1); + await chromeSafariBrowser.launchUrl(url: TEST_TWA_URL); await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); diff --git a/example/integration_test/constants.dart b/example/integration_test/constants.dart index 3b882ee0..180bcdfd 100644 --- a/example/integration_test/constants.dart +++ b/example/integration_test/constants.dart @@ -28,3 +28,7 @@ final TEST_SERVICE_WORKER_URL = WebUri( final TEST_WEBVIEW_ASSET_LOADER_DOMAIN = 'my.custom.domain.com'; final TEST_WEBVIEW_ASSET_LOADER_URL = WebUri( 'https://$TEST_WEBVIEW_ASSET_LOADER_DOMAIN/assets/flutter_assets/test_assets/website/index.html'); +final TEST_TWA_URL = + WebUri('https://inappwebview.dev/test-twa-postmessage.html'); +final TEST_CUSTOM_TABS_POST_MESSAGE_URL = + WebUri('https://inappwebview.dev/test-twa-postmessage.html'); diff --git a/example/integration_test/util.dart b/example/integration_test/util.dart index ca1f24d2..3bf631e6 100644 --- a/example/integration_test/util.dart +++ b/example/integration_test/util.dart @@ -118,6 +118,9 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser { final Completer closed = Completer(); final Completer navigationEvent = Completer(); + final Completer navigationFinished = Completer(); + final Completer messageChannelReady = Completer(); + final Completer postMessageReceived = Completer(); final Completer relationshipValidationResult = Completer(); @override @@ -140,6 +143,24 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser { if (!navigationEvent.isCompleted) { navigationEvent.complete(type); } + if (!navigationFinished.isCompleted && + type == CustomTabsNavigationEventType.FINISHED) { + navigationFinished.complete(); + } + } + + @override + void onMessageChannelReady() async { + if (!messageChannelReady.isCompleted) { + messageChannelReady.complete(); + } + } + + @override + void onPostMessage(String message) { + if (!postMessageReceived.isCompleted) { + postMessageReceived.complete(); + } } @override diff --git a/example/lib/chrome_safari_browser_example.screen.dart b/example/lib/chrome_safari_browser_example.screen.dart index 99995909..5360667c 100755 --- a/example/lib/chrome_safari_browser_example.screen.dart +++ b/example/lib/chrome_safari_browser_example.screen.dart @@ -19,6 +19,21 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser { void onClosed() { print("ChromeSafari browser closed"); } + + @override + void onVerticalScrollEvent(bool isDirectionUp) { + print("onVerticalScrollEvent $isDirectionUp"); + } + + @override + void onGreatestScrollPercentageIncreased(int scrollPercentage) { + print("onGreatestScrollPercentageIncreased $scrollPercentage"); + } + + @override + void onSessionEnded(bool didUserInteract) { + print("onSessionEnded $didUserInteract"); + } } class ChromeSafariBrowserExampleScreen extends StatefulWidget { @@ -103,6 +118,8 @@ class _ChromeSafariBrowserExampleScreenState dismissButtonStyle: DismissButtonStyle.CLOSE, presentationStyle: ModalPresentationStyle.OVER_FULL_SCREEN)); + await Future.delayed(Duration(seconds: 5)); + widget.browser.close(); }, child: Text("Open Chrome Safari Browser")), )); diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index 4f2e94a9..0067875f 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../types/custom_tabs_navigation_event_type.dart'; +import '../types/custom_tabs_post_message_result_type.dart'; import '../types/custom_tabs_relation_type.dart'; import '../types/prewarming_token.dart'; import '../util.dart'; @@ -153,6 +154,25 @@ class ChromeSafariBrowser extends ChannelController { } } break; + case "onMessageChannelReady": + onMessageChannelReady(); + break; + case "onPostMessage": + final String message = call.arguments["message"]; + onPostMessage(message); + break; + case "onVerticalScrollEvent": + final bool isDirectionUp = call.arguments["isDirectionUp"]; + onVerticalScrollEvent(isDirectionUp); + break; + case "onGreatestScrollPercentageIncreased": + final int scrollPercentage = call.arguments["scrollPercentage"]; + onGreatestScrollPercentageIncreased(scrollPercentage); + break; + case "onSessionEnded": + final bool didUserInteract = call.arguments["didUserInteract"]; + onSessionEnded(didUserInteract); + break; default: throw UnimplementedError("Unimplemented ${call.method} method"); } @@ -384,6 +404,67 @@ class ChromeSafariBrowser extends ChannelController { }); } + ///Sends a request to create a two way postMessage channel between the client + ///and the browser. + ///If you want to specifying the target origin to communicate with, set the [targetOrigin]. + /// + ///[sourceOrigin] - A origin that the client is requesting to be + ///identified as during the postMessage communication. + ///It has to either start with http or https. + /// + ///[targetOrigin] - The target Origin to establish the postMessage communication with. + ///This can be the app's package name, it has to either start with http or https. + /// + ///Returns whether the implementation accepted the request. + ///Note that returning true here doesn't mean an origin has already been + ///assigned as the validation is asynchronous. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsSession.requestPostMessageChannel](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#requestPostMessageChannel(android.net.Uri,android.net.Uri,android.os.Bundle))) + Future requestPostMessageChannel( + {required WebUri sourceOrigin, WebUri? targetOrigin}) async { + Map args = {}; + args.putIfAbsent("sourceOrigin", () => sourceOrigin.toString()); + args.putIfAbsent("targetOrigin", () => targetOrigin.toString()); + return await channel?.invokeMethod( + "requestPostMessageChannel", args) ?? + false; + } + + ///Sends a postMessage request using the origin communicated via [requestPostMessageChannel]. + ///Fails when called before [onMessageChannelReady] event. + /// + ///[message] – The message that is being sent. + /// + ///Returns an integer constant about the postMessage request result. + ///Will return CustomTabsService.RESULT_SUCCESS if successful. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsSession.postMessage](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#postMessage(java.lang.String,android.os.Bundle))) + Future postMessage(String message) async { + Map args = {}; + args.putIfAbsent("message", () => message); + return CustomTabsPostMessageResultType.fromNativeValue( + await channel?.invokeMethod("postMessage", args)) ?? + CustomTabsPostMessageResultType.FAILURE_MESSAGING_ERROR; + } + + ///Returns whether the Engagement Signals API is available. + ///The availability of the Engagement Signals API may change at runtime. + ///If an EngagementSignalsCallback has been set, an [onSessionEnded] + ///signal will be sent if the API becomes unavailable later. + /// + ///Returns whether the Engagement Signals API is available. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsSession.isEngagementSignalsApiAvailable](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#isEngagementSignalsApiAvailable(android.os.Bundle))) + Future isEngagementSignalsApiAvailable() async { + Map args = {}; + return await channel?.invokeMethod( + "isEngagementSignalsApiAvailable", args) ?? + false; + } + ///On Android, returns `true` if Chrome Custom Tabs is available. ///On iOS, returns `true` if SFSafariViewController is available. ///Otherwise returns `false`. @@ -393,7 +474,8 @@ class ChromeSafariBrowser extends ChannelController { ///- iOS static Future isAvailable() async { Map args = {}; - return await _sharedChannel.invokeMethod("isAvailable", args); + return await _sharedChannel.invokeMethod("isAvailable", args) ?? + false; } ///The maximum number of allowed secondary toolbar items. @@ -515,6 +597,57 @@ class ChromeSafariBrowser extends ChannelController { ///- iOS ([Official API - SFSafariViewControllerDelegate.safariViewControllerWillOpenInBrowser](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/3650426-safariviewcontrollerwillopeninbr)) void onWillOpenInBrowser() {} + ///Called when the [ChromeSafariBrowser] has requested a postMessage channel through + ///[requestPostMessageChannel] and the channel is ready for sending and receiving messages on both ends. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsCallback.onMessageChannelReady](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsCallback#onMessageChannelReady(android.os.Bundle))) + void onMessageChannelReady() {} + + ///Called when a tab controlled by this [ChromeSafariBrowser] has sent a postMessage. + ///If [postMessage] is called from a single thread, then the messages will be posted in the same order. + ///When received on the client side, it is the client's responsibility to preserve the ordering further. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsCallback.onPostMessage](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsCallback#onPostMessage(java.lang.String,android.os.Bundle))) + void onPostMessage(String message) {} + + ///Called when a user scrolls the tab. + /// + ///[isDirectionUp] - `false` when the user scrolls farther down the page, + ///and `true` when the user scrolls back up toward the top of the page. + /// + ///**NOTE**: available only if [isEngagementSignalsApiAvailable] returns `true`. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - EngagementSignalsCallback.onVerticalScrollEvent](https://developer.android.com/reference/androidx/browser/customtabs/EngagementSignalsCallback#onVerticalScrollEvent(boolean,android.os.Bundle))) + void onVerticalScrollEvent(bool isDirectionUp) {} + + ///Called when a user has reached a greater scroll percentage on the page. The greatest scroll + ///percentage is reset if the user navigates to a different page. If the current page's total + ///height changes, this method will be called again only if the scroll progress reaches a + ///higher percentage based on the new and current height of the page. + /// + ///[scrollPercentage] - An integer indicating the percent of scrollable progress + ///the user hasmade down the current page. + /// + ///**NOTE**: available only if [isEngagementSignalsApiAvailable] returns `true`. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - EngagementSignalsCallback.onGreatestScrollPercentageIncreased](https://developer.android.com/reference/androidx/browser/customtabs/EngagementSignalsCallback#onGreatestScrollPercentageIncreased(int,android.os.Bundle))) + void onGreatestScrollPercentageIncreased(int scrollPercentage) {} + + ///Called when a `CustomTabsSession` is ending or when no further Engagement Signals + ///callbacks are expected to report whether any user action has occurred during the session. + /// + ///[didUserInteract] - Whether the user has interacted with the page in any way, e.g. scrolling. + /// + ///**NOTE**: available only if [isEngagementSignalsApiAvailable] returns `true`. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - EngagementSignalsCallback.onSessionEnded](https://developer.android.com/reference/androidx/browser/customtabs/EngagementSignalsCallback#onSessionEnded(boolean,android.os.Bundle))) + void onSessionEnded(bool didUserInteract) {} + ///Event fired when the [ChromeSafariBrowser] is closed. /// ///**Supported Platforms/Implementations**: diff --git a/lib/src/types/custom_tabs_post_message_result_type.dart b/lib/src/types/custom_tabs_post_message_result_type.dart new file mode 100644 index 00000000..9a8892c2 --- /dev/null +++ b/lib/src/types/custom_tabs_post_message_result_type.dart @@ -0,0 +1,36 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +part 'custom_tabs_post_message_result_type.g.dart'; + +///Custom Tabs postMessage result type. +@ExchangeableEnum() +class CustomTabsPostMessageResultType_ { + // ignore: unused_field + final int _value; + + // ignore: unused_field + final int? _nativeValue = null; + + const CustomTabsPostMessageResultType_._internal(this._value); + + ///Indicates that the postMessage request was accepted. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 0)]) + static const SUCCESS = const CustomTabsPostMessageResultType_._internal(0); + + ///Indicates that the postMessage request was not allowed due to a bad argument + ///or requesting at a disallowed time like when in background. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: -1)]) + static const FAILURE_DISALLOWED = + const CustomTabsPostMessageResultType_._internal(-1); + + ///Indicates that the postMessage request has failed due to a `RemoteException`. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: -2)]) + static const FAILURE_REMOTE_ERROR = + const CustomTabsPostMessageResultType_._internal(-2); + + ///Indicates that the postMessage request has failed due to an internal error on the browser message channel. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: -3)]) + static const FAILURE_MESSAGING_ERROR = + const CustomTabsPostMessageResultType_._internal(-3); +} diff --git a/lib/src/types/custom_tabs_post_message_result_type.g.dart b/lib/src/types/custom_tabs_post_message_result_type.g.dart new file mode 100644 index 00000000..5db8d998 --- /dev/null +++ b/lib/src/types/custom_tabs_post_message_result_type.g.dart @@ -0,0 +1,141 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'custom_tabs_post_message_result_type.dart'; + +// ************************************************************************** +// ExchangeableEnumGenerator +// ************************************************************************** + +///Custom Tabs postMessage result type. +class CustomTabsPostMessageResultType { + final int _value; + final int? _nativeValue; + const CustomTabsPostMessageResultType._internal( + this._value, this._nativeValue); +// ignore: unused_element + factory CustomTabsPostMessageResultType._internalMultiPlatform( + int value, Function nativeValue) => + CustomTabsPostMessageResultType._internal(value, nativeValue()); + + ///Indicates that the postMessage request was not allowed due to a bad argument + ///or requesting at a disallowed time like when in background. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final FAILURE_DISALLOWED = + CustomTabsPostMessageResultType._internalMultiPlatform(-1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return -1; + default: + break; + } + return null; + }); + + ///Indicates that the postMessage request has failed due to an internal error on the browser message channel. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final FAILURE_MESSAGING_ERROR = + CustomTabsPostMessageResultType._internalMultiPlatform(-3, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return -3; + default: + break; + } + return null; + }); + + ///Indicates that the postMessage request has failed due to a `RemoteException`. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final FAILURE_REMOTE_ERROR = + CustomTabsPostMessageResultType._internalMultiPlatform(-2, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return -2; + default: + break; + } + return null; + }); + + ///Indicates that the postMessage request was accepted. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final SUCCESS = + CustomTabsPostMessageResultType._internalMultiPlatform(0, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 0; + default: + break; + } + return null; + }); + + ///Set of all values of [CustomTabsPostMessageResultType]. + static final Set values = [ + CustomTabsPostMessageResultType.FAILURE_DISALLOWED, + CustomTabsPostMessageResultType.FAILURE_MESSAGING_ERROR, + CustomTabsPostMessageResultType.FAILURE_REMOTE_ERROR, + CustomTabsPostMessageResultType.SUCCESS, + ].toSet(); + + ///Gets a possible [CustomTabsPostMessageResultType] instance from [int] value. + static CustomTabsPostMessageResultType? fromValue(int? value) { + if (value != null) { + try { + return CustomTabsPostMessageResultType.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets a possible [CustomTabsPostMessageResultType] instance from a native value. + static CustomTabsPostMessageResultType? fromNativeValue(int? value) { + if (value != null) { + try { + return CustomTabsPostMessageResultType.values + .firstWhere((element) => element.toNativeValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets [int] value. + int toValue() => _value; + + ///Gets [int?] native value. + int? toNativeValue() => _nativeValue; + + @override + int get hashCode => _value.hashCode; + + @override + bool operator ==(value) => value == _value; + + @override + String toString() { + switch (_value) { + case -1: + return 'FAILURE_DISALLOWED'; + case -3: + return 'FAILURE_MESSAGING_ERROR'; + case -2: + return 'FAILURE_REMOTE_ERROR'; + case 0: + return 'SUCCESS'; + } + return _value.toString(); + } +} diff --git a/lib/src/types/main.dart b/lib/src/types/main.dart index fbd30d34..5c167f3b 100644 --- a/lib/src/types/main.dart +++ b/lib/src/types/main.dart @@ -223,3 +223,5 @@ export 'activity_button.dart' show ActivityButton; export 'ui_event_attribution.dart' show UIEventAttribution; export 'tracing_mode.dart' show TracingMode; export 'tracing_category.dart' show TracingCategory; +export 'custom_tabs_post_message_result_type.dart' + show CustomTabsPostMessageResultType; diff --git a/pubspec.yaml b/pubspec.yaml index c488de21..8ebd56da 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview description: A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. -version: 6.0.0-beta.26 +version: 6.0.0-beta.27 homepage: https://inappwebview.dev/ repository: https://github.com/pichillilorenzo/flutter_inappwebview issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues