From ebea457b018dc43601fd1c3625331cc5945a5c3d Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Mon, 24 Oct 2022 12:34:18 +0200 Subject: [PATCH 1/3] initial implementation 6.0.0-beta.9 --- CHANGELOG.md | 16 + .../ActionBroadcastReceiver.java | 7 +- .../ChromeCustomTabsActivity.java | 110 +++++-- .../ChromeCustomTabsChannelDelegate.java | 97 +++++- .../ChromeCustomTabsSettings.java | 2 + .../ChromeSafariBrowserManager.java | 12 +- .../CustomTabActivityHelper.java | 2 + .../TrustedWebActivity.java | 27 +- .../ios/Flutter/flutter_export_environment.sh | 5 +- .../chrome_safari_browser_example.screen.dart | 2 +- .../ChromeSafariBrowserManager.swift | 46 +++ .../CustomUIActivity.swift | 2 +- .../SafariViewController.swift | 31 +- .../SafariViewControllerChannelDelegate.swift | 30 +- .../chrome_safari_browser.dart | 305 ++++++++++++++++-- .../custom_tabs_navigation_event_type.dart | 41 +++ .../custom_tabs_navigation_event_type.g.dart | 175 ++++++++++ lib/src/types/custom_tabs_relation_type.dart | 24 ++ .../types/custom_tabs_relation_type.g.dart | 103 ++++++ lib/src/types/main.dart | 3 + lib/src/types/prewarming_token.dart | 14 + lib/src/types/prewarming_token.g.dart | 42 +++ pubspec.yaml | 2 +- 23 files changed, 992 insertions(+), 106 deletions(-) create mode 100644 lib/src/types/custom_tabs_navigation_event_type.dart create mode 100644 lib/src/types/custom_tabs_navigation_event_type.g.dart create mode 100644 lib/src/types/custom_tabs_relation_type.dart create mode 100644 lib/src/types/custom_tabs_relation_type.g.dart create mode 100644 lib/src/types/prewarming_token.dart create mode 100644 lib/src/types/prewarming_token.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd74f9c4..a9482230e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 6.0.0-beta.9 + +- Added `headers`, `otherLikelyURLs` arguments on `ChromeSafariBrowser.open` method for Android +- Added `onNavigationEvent`, `onServiceConnected`, `onRelationshipValidationResult` events on `ChromeSafariBrowser` for Android +- Added `mayLaunchUrl`, `launchUrl`, `updateActionButton`, `validateRelationship` methods on `ChromeSafariBrowser` for Android +- Added `didLoadSuccessfully` optional argument on `ChromeSafariBrowser.onCompletedInitialLoad` event for iOS +- Added `onInitialLoadDidRedirect`, `onWillOpenInBrowser` events on `ChromeSafariBrowser` for iOS +- Added `clearWebsiteData`, `prewarmConnections`, `invalidatePrewarmingToken` static methods on `ChromeSafariBrowser` for iOS + +### BREAKING CHANGES + +- `ChromeSafariBrowser.onCompletedInitialLoad` event has an optional argument + +## 6.0.0-beta.8 + + ## 6.0.0-beta.7 - Updated Android hybrid composition implementation diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java index b9d06d6bd..e58ead517 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java @@ -5,11 +5,6 @@ import android.content.Intent; import android.os.Bundle; -import java.util.HashMap; -import java.util.Map; - -import io.flutter.plugin.common.MethodChannel; - public class ActionBroadcastReceiver extends BroadcastReceiver { protected static final String LOG_TAG = "ActionBroadcastReceiver"; public static final String KEY_ACTION_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_ID"; @@ -27,7 +22,7 @@ public void onReceive(Context context, Intent intent) { ChromeCustomTabsActivity browser = ChromeSafariBrowserManager.browsers.get(viewId); if (browser != null && browser.channelDelegate != null) { - browser.channelDelegate.onChromeSafariBrowserItemActionPerform(id, url, title); + browser.channelDelegate.onItemActionPerform(id, url, title); } } } 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 b2a4f9d2e..9b8908e65 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 @@ -12,6 +12,7 @@ import android.util.Log; import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsCallback; @@ -20,7 +21,6 @@ import androidx.browser.customtabs.CustomTabsSession; import com.pichillilorenzo.flutter_inappwebview.R; -import com.pichillilorenzo.flutter_inappwebview.headless_in_app_webview.HeadlessInAppWebViewManager; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsActionButton; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsMenuItem; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; @@ -44,11 +44,16 @@ public class ChromeCustomTabsActivity extends Activity implements Disposable { @Nullable public CustomTabsSession customTabsSession; protected final int CHROME_CUSTOM_TAB_REQUEST_CODE = 100; - protected boolean onChromeSafariBrowserOpened = false; - protected boolean onChromeSafariBrowserCompletedInitialLoad = false; + protected boolean onOpened = false; + protected boolean onCompletedInitialLoad = false; @Nullable public ChromeSafariBrowserManager manager; + @Nullable public String initialUrl; + @Nullable + public List initialOtherLikelyURLs; + @Nullable + public Map initialHeaders; public List menuItems = new ArrayList<>(); @Nullable public CustomTabsActionButton actionButton; @@ -69,7 +74,7 @@ protected void onCreate(Bundle savedInstanceState) { String managerId = b.getString("managerId"); manager = ChromeSafariBrowserManager.shared.get(managerId); - if (manager == null || manager.plugin == null|| manager.plugin.messenger == null) return; + if (manager == null || manager.plugin == null || manager.plugin.messenger == null) return; ChromeSafariBrowserManager.browsers.put(id, this); @@ -77,6 +82,8 @@ protected void onCreate(Bundle savedInstanceState) { channelDelegate = new ChromeCustomTabsChannelDelegate(this, channel); initialUrl = b.getString("url"); + initialHeaders = (Map) b.getSerializable("headers"); + initialOtherLikelyURLs = b.getStringArrayList("otherLikelyURLs"); customSettings = new ChromeCustomTabsSettings(); customSettings.parse((HashMap) b.getSerializable("settings")); @@ -92,6 +99,9 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onCustomTabsConnected() { customTabsConnected(); + if (channelDelegate != null) { + channelDelegate.onServiceConnected(); + } } @Override @@ -104,49 +114,49 @@ public void onCustomTabsDisconnected() { customTabActivityHelper.setCustomTabsCallback(new CustomTabsCallback() { @Override public void onNavigationEvent(int navigationEvent, Bundle extras) { - if (navigationEvent == TAB_SHOWN && !onChromeSafariBrowserOpened) { - onChromeSafariBrowserOpened = true; + if (navigationEvent == TAB_SHOWN && !onOpened) { + onOpened = true; if (channelDelegate != null) { - channelDelegate.onChromeSafariBrowserOpened(); + channelDelegate.onOpened(); } } - if (navigationEvent == NAVIGATION_FINISHED && !onChromeSafariBrowserCompletedInitialLoad) { - onChromeSafariBrowserCompletedInitialLoad = true; + if (navigationEvent == NAVIGATION_FINISHED && !onCompletedInitialLoad) { + onCompletedInitialLoad = true; if (channelDelegate != null) { - channelDelegate.onChromeSafariBrowserCompletedInitialLoad(); + channelDelegate.onCompletedInitialLoad(); } } - } - - @Override - public void extraCallback(String callbackName, Bundle args) { + if (channelDelegate != null) { + channelDelegate.onNavigationEvent(navigationEvent); + } } @Override - public void onMessageChannelReady(Bundle extras) { - - } + public void extraCallback(@NonNull String callbackName, Bundle args) {} @Override - public void onPostMessage(String message, Bundle extras) { + public void onMessageChannelReady(Bundle extras) {} - } + @Override + public void onPostMessage(@NonNull String message, Bundle extras) {} @Override - public void onRelationshipValidationResult(@CustomTabsService.Relation int relation, Uri requestedOrigin, + public void onRelationshipValidationResult(@CustomTabsService.Relation int relation, + @NonNull Uri requestedOrigin, boolean result, Bundle extras) { - + if (channelDelegate != null) { + channelDelegate.onRelationshipValidationResult(relation, requestedOrigin, result); + } } }); } - public void customTabsConnected() { - customTabsSession = customTabActivityHelper.getSession(); - Uri uri = Uri.parse(initialUrl); - customTabActivityHelper.mayLaunchUrl(uri, null, null); - + public void launchUrl(@NonNull String url, + @Nullable Map headers, + @Nullable List otherLikelyURLs) { + Uri uri = mayLaunchUrl(url, headers, otherLikelyURLs); builder = new CustomTabsIntent.Builder(customTabsSession); prepareCustomTabs(); @@ -156,7 +166,39 @@ public void customTabsConnected() { CustomTabActivityHelper.openCustomTab(this, customTabsIntent, uri, CHROME_CUSTOM_TAB_REQUEST_CODE); } + public Uri mayLaunchUrl(@NonNull String url, + @Nullable Map headers, + @Nullable List otherLikelyURLs) { + Uri uri = Uri.parse(url); + Bundle bundleHeaders = new Bundle(); + if (headers != null) { + for (Map.Entry header : headers.entrySet()) { + bundleHeaders.putString(header.getKey(), header.getValue()); + } + } + List bundleOtherLikelyURLs = new ArrayList<>(); + if (otherLikelyURLs != null) { + for (String otherLikelyURL : otherLikelyURLs) { + Bundle bundleOtherLikelyURL = new Bundle(); + bundleOtherLikelyURL.putString(CustomTabsService.KEY_URL, otherLikelyURL); + } + } + customTabActivityHelper.mayLaunchUrl(uri, bundleHeaders, bundleOtherLikelyURLs); + return uri; + } + + public void customTabsConnected() { + customTabsSession = customTabActivityHelper.getSession(); + if (initialUrl != null) { + launchUrl(initialUrl, initialHeaders, initialOtherLikelyURLs); + } + } + private void prepareCustomTabs() { + if (builder == null) { + return; + } + if (customSettings.addDefaultShareMenuItem != null) { builder.setShareState(customSettings.addDefaultShareMenuItem ? CustomTabsIntent.SHARE_STATE_ON : CustomTabsIntent.SHARE_STATE_OFF); @@ -203,6 +245,20 @@ private void prepareCustomTabsIntent(CustomTabsIntent customTabsIntent) { CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent); } + public void updateActionButton(@NonNull byte[] icon, @NonNull String description) { + if (customTabsSession == null || actionButton == null) { + return; + } + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inMutable = true; + Bitmap bmp = BitmapFactory.decodeByteArray( + icon, 0, icon.length, bitmapOptions + ); + customTabsSession.setActionButton(bmp, description); + actionButton.setIcon(icon); + actionButton.setDescription(description); + } + @Override protected void onStart() { super.onStart(); @@ -265,7 +321,7 @@ public void close() { customTabsSession = null; finish(); if (channelDelegate != null) { - channelDelegate.onChromeSafariBrowserClosed(); + channelDelegate.onClosed(); } } } 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 3b2353aba..22160e976 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 @@ -2,15 +2,17 @@ import android.app.Activity; import android.content.Intent; +import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.pichillilorenzo.flutter_inappwebview.headless_in_app_webview.HeadlessInAppWebView; import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; -import com.pichillilorenzo.flutter_inappwebview.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsActionButton; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import io.flutter.plugin.common.MethodCall; @@ -28,6 +30,56 @@ public ChromeCustomTabsChannelDelegate(@NonNull ChromeCustomTabsActivity chromeC @Override public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodChannel.Result result) { switch (call.method) { + case "launchUrl": + if (chromeCustomTabsActivity != null) { + String url = (String) call.argument("url"); + if (url != null) { + Map headers = (Map) call.argument("headers"); + List otherLikelyURLs = (List) call.argument("otherLikelyURLs"); + chromeCustomTabsActivity.launchUrl(url, headers, otherLikelyURLs); + result.success(true); + } else { + result.success(false); + } + } else { + result.success(false); + } + break; + case "mayLaunchUrl": + if (chromeCustomTabsActivity != null) { + String url = (String) call.argument("url"); + if (url != null) { + Map headers = (Map) call.argument("headers"); + List otherLikelyURLs = (List) call.argument("otherLikelyURLs"); + chromeCustomTabsActivity.mayLaunchUrl(url, headers, otherLikelyURLs); + result.success(true); + } else { + result.success(false); + } + } else { + result.success(false); + } + break; + case "updateActionButton": + if (chromeCustomTabsActivity != null) { + byte[] icon = (byte[]) call.argument("icon"); + String description = (String) call.argument("description"); + chromeCustomTabsActivity.updateActionButton(icon, description); + result.success(true); + } else { + result.success(false); + } + break; + case "validateRelationship": + if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { + Integer relation = (Integer) call.argument("relation"); + String origin = (String) call.argument("origin"); + chromeCustomTabsActivity.customTabsSession.validateRelationship(relation, Uri.parse(origin), null); + result.success(true); + } else { + result.success(false); + } + break; case "close": if (chromeCustomTabsActivity != null) { chromeCustomTabsActivity.onStop(); @@ -54,35 +106,60 @@ public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodCh } } - public void onChromeSafariBrowserOpened() { + public void onServiceConnected() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onServiceConnected", obj); + } + + public void onOpened() { MethodChannel channel = getChannel(); if (channel == null) return; Map obj = new HashMap<>(); - channel.invokeMethod("onChromeSafariBrowserOpened", obj); + channel.invokeMethod("onOpened", obj); } - public void onChromeSafariBrowserCompletedInitialLoad() { + public void onCompletedInitialLoad() { MethodChannel channel = getChannel(); if (channel == null) return; Map obj = new HashMap<>(); - channel.invokeMethod("onChromeSafariBrowserCompletedInitialLoad", obj); + channel.invokeMethod("onCompletedInitialLoad", obj); } - public void onChromeSafariBrowserClosed() { + public void onNavigationEvent(int navigationEvent) {; MethodChannel channel = getChannel(); if (channel == null) return; Map obj = new HashMap<>(); - channel.invokeMethod("onChromeSafariBrowserClosed", obj); + obj.put("navigationEvent", navigationEvent); + channel.invokeMethod("onNavigationEvent", obj); } - public void onChromeSafariBrowserItemActionPerform(int id, String url, String title) { + public void onClosed() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onClosed", obj); + } + + public void onItemActionPerform(int id, String url, String title) { MethodChannel channel = getChannel(); if (channel == null) return; Map obj = new HashMap<>(); obj.put("id", id); obj.put("url", url); obj.put("title", title); - channel.invokeMethod("onChromeSafariBrowserItemActionPerform", obj); + channel.invokeMethod("onItemActionPerform", obj); + } + + public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin, boolean result) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("relation", relation); + obj.put("requestedOrigin", requestedOrigin.toString()); + obj.put("result", result); + channel.invokeMethod("onRelationshipValidationResult", obj); } @Override diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java index 0f9f4e7bb..3867020e5 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java @@ -92,8 +92,10 @@ public ChromeCustomTabsSettings parse(@NonNull Map options) { boolean isSticky = (boolean) displayModeMap.get("isSticky"); int layoutInDisplayCutoutMode = (int) displayModeMap.get("displayCutoutMode"); displayMode = new TrustedWebActivityDisplayMode.ImmersiveMode(isSticky, layoutInDisplayCutoutMode); + break; case "DEFAULT_MODE": displayMode = new TrustedWebActivityDisplayMode.DefaultMode(); + break; } } break; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java index d9c5536f4..ccf1197c2 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java @@ -12,6 +12,7 @@ import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -46,10 +47,12 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result resul case "open": if (plugin != null && plugin.activity != null) { String url = (String) call.argument("url"); + HashMap headers = (HashMap) call.argument("headers"); + ArrayList otherLikelyURLs = (ArrayList) call.argument("otherLikelyURLs"); HashMap settings = (HashMap) call.argument("settings"); HashMap actionButton = (HashMap) call.argument("actionButton"); List> menuItemList = (List>) call.argument("menuItemList"); - open(plugin.activity, viewId, url, settings, actionButton, menuItemList, result); + open(plugin.activity, viewId, url, headers, otherLikelyURLs, settings, actionButton, menuItemList, result); } else { result.success(false); } @@ -66,8 +69,9 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result resul } } - public void open(Activity activity, String viewId, String url, HashMap settings, - HashMap actionButton, + public void open(Activity activity, String viewId, @Nullable String url, @Nullable HashMap headers, + @Nullable ArrayList otherLikelyURLs, + HashMap settings, HashMap actionButton, List> menuItemList, MethodChannel.Result result) { Intent intent = null; @@ -76,6 +80,8 @@ public void open(Activity activity, String viewId, String url, HashMap headers, + @Nullable List otherLikelyURLs) { + if (customTabsSession == null) { + return; + } + Uri uri = mayLaunchUrl(url, headers, otherLikelyURLs); builder = new TrustedWebActivityIntentBuilder(uri); prepareCustomTabs(); @@ -32,6 +42,15 @@ public void customTabsConnected() { CustomTabActivityHelper.openCustomTab(this, trustedWebActivityIntent, uri, CHROME_CUSTOM_TAB_REQUEST_CODE); } + @Override + public void customTabsConnected() { + customTabsSession = customTabActivityHelper.getSession(); + if (initialUrl != null) { + launchUrl(initialUrl, initialHeaders, initialOtherLikelyURLs); + } + + } + private void prepareCustomTabs() { if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) { CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh index f90e85052..392ebcba5 100755 --- a/example/ios/Flutter/flutter_export_environment.sh +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -3,11 +3,12 @@ export "FLUTTER_ROOT=/Users/lorenzopichilli/fvm/versions/3.3.5" export "FLUTTER_APPLICATION_PATH=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example" export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_TARGET=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/lib/main.dart" export "FLUTTER_BUILD_DIR=build" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" +export "DART_DEFINES=Zmx1dHRlci5pbnNwZWN0b3Iuc3RydWN0dXJlZEVycm9ycz10cnVl,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" +export "PACKAGE_CONFIG=/Users/lorenzopichilli/Desktop/flutter_inappwebview/example/.dart_tool/package_config.json" diff --git a/example/lib/chrome_safari_browser_example.screen.dart b/example/lib/chrome_safari_browser_example.screen.dart index ce94fa791..053f7a4c9 100755 --- a/example/lib/chrome_safari_browser_example.screen.dart +++ b/example/lib/chrome_safari_browser_example.screen.dart @@ -11,7 +11,7 @@ class MyChromeSafariBrowser extends ChromeSafariBrowser { } @override - void onCompletedInitialLoad() { + void onCompletedInitialLoad(didLoadSuccessfully) { print("ChromeSafari browser initial load completed"); } diff --git a/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift b/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift index 1c4a3b790..e1b40ef78 100755 --- a/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift +++ b/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift @@ -16,6 +16,8 @@ public class ChromeSafariBrowserManager: ChannelDelegate { static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_chromesafaribrowser" static var registrar: FlutterPluginRegistrar? static var browsers: [String: SafariViewController?] = [:] + @available(iOS 15.0, *) + static var prewarmingTokens: [String: SFSafariViewController.PrewarmingToken?] = [:] init(registrar: FlutterPluginRegistrar) { super.init(channel: FlutterMethodChannel(name: ChromeSafariBrowserManager.METHOD_CHANNEL_NAME, binaryMessenger: registrar.messenger())) @@ -40,6 +42,44 @@ public class ChromeSafariBrowserManager: ChannelDelegate { result(false) } break + case "clearWebsiteData": + if #available(iOS 16.0, *) { + SFSafariViewController.DataStore.default.clearWebsiteData { + result(true) + } + } else { + result(false) + } + case "prewarmConnections": + if #available(iOS 15.0, *) { + let stringURLs = arguments!["URLs"] as! [String] + var URLs: [URL] = [] + for stringURL in stringURLs { + if let url = URL(string: stringURL) { + URLs.append(url) + } + } + let prewarmingToken = SFSafariViewController.prewarmConnections(to: URLs) + let prewarmingTokenId = NSUUID().uuidString + ChromeSafariBrowserManager.prewarmingTokens[prewarmingTokenId] = prewarmingToken + result([ + "id": prewarmingTokenId + ]) + } else { + result(nil) + } + case "invalidatePrewarmingToken": + if #available(iOS 15.0, *) { + let prewarmingToken = arguments!["prewarmingToken"] as! [String:Any?] + if let prewarmingTokenId = prewarmingToken["id"] as? String, + let prewarmingToken = ChromeSafariBrowserManager.prewarmingTokens[prewarmingTokenId] { + prewarmingToken?.invalidate() + ChromeSafariBrowserManager.prewarmingTokens[prewarmingTokenId] = nil + } + result(true) + } else { + result(false) + } default: result(FlutterMethodNotImplemented) break @@ -95,6 +135,12 @@ public class ChromeSafariBrowserManager: ChannelDelegate { browser?.dispose() } ChromeSafariBrowserManager.browsers.removeAll() + if #available(iOS 15.0, *) { + ChromeSafariBrowserManager.prewarmingTokens.values.forEach { (prewarmingToken: SFSafariViewController.PrewarmingToken?) in + prewarmingToken?.invalidate() + } + ChromeSafariBrowserManager.prewarmingTokens.removeAll() + } } deinit { diff --git a/ios/Classes/SafariViewController/CustomUIActivity.swift b/ios/Classes/SafariViewController/CustomUIActivity.swift index b661a9ba9..7d50e7071 100644 --- a/ios/Classes/SafariViewController/CustomUIActivity.swift +++ b/ios/Classes/SafariViewController/CustomUIActivity.swift @@ -48,6 +48,6 @@ class CustomUIActivity : UIActivity { override func perform() { let browser = ChromeSafariBrowserManager.browsers[viewId] - browser??.channelDelegate?.onChromeSafariBrowserMenuItemActionPerform(id: id, url: url, title: title) + browser??.channelDelegate?.onItemActionPerform(id: id, url: url, title: title) } } diff --git a/ios/Classes/SafariViewController/SafariViewController.swift b/ios/Classes/SafariViewController/SafariViewController.swift index ee88280d6..b44568a32 100755 --- a/ios/Classes/SafariViewController/SafariViewController.swift +++ b/ios/Classes/SafariViewController/SafariViewController.swift @@ -42,12 +42,12 @@ public class SafariViewController: SFSafariViewController, SFSafariViewControlle public override func viewWillAppear(_ animated: Bool) { // prepareSafariBrowser() super.viewWillAppear(animated) - channelDelegate?.onChromeSafariBrowserOpened() + channelDelegate?.onOpened() } public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - channelDelegate?.onChromeSafariBrowserClosed() + channelDelegate?.onClosed() self.dispose() } @@ -87,12 +87,7 @@ public class SafariViewController: SFSafariViewController, SFSafariViewControlle public func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - if didLoadSuccessfully { - channelDelegate?.onChromeSafariBrowserCompletedInitialLoad() - } - else { - print("Cant load successfully the 'SafariViewController'.") - } + channelDelegate?.onCompletedInitialLoad(didLoadSuccessfully: didLoadSuccessfully) } public func safariViewController(_ controller: SFSafariViewController, activityItemsFor URL: URL, title: String?) -> [UIActivity] { @@ -103,18 +98,14 @@ public class SafariViewController: SFSafariViewController, SFSafariViewControlle } return uiActivities } -// -// public func safariViewController(_ controller: SFSafariViewController, excludedActivityTypesFor URL: URL, title: String?) -> [UIActivity.ActivityType] { -// print("excludedActivityTypesFor") -// print(URL) -// print(title) -// return [] -// } -// -// public func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) { -// print("initialLoadDidRedirectTo") -// print(URL) -// } + + public func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo url: URL) { + channelDelegate?.onInitialLoadDidRedirect(url: url) + } + + public func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) { + channelDelegate?.onWillOpenInBrowser() + } public func dispose() { channelDelegate?.dispose() diff --git a/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift b/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift index dcccd8036..c7fc5adb4 100644 --- a/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift +++ b/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift @@ -31,28 +31,42 @@ public class SafariViewControllerChannelDelegate : ChannelDelegate { } } - public func onChromeSafariBrowserOpened() { + public func onOpened() { let arguments: [String: Any?] = [:] - channel?.invokeMethod("onChromeSafariBrowserOpened", arguments: arguments) + channel?.invokeMethod("onOpened", arguments: arguments) } - public func onChromeSafariBrowserCompletedInitialLoad() { + public func onCompletedInitialLoad(didLoadSuccessfully: Bool) { + let arguments: [String: Any?] = [ + "didLoadSuccessfully": didLoadSuccessfully + ] + channel?.invokeMethod("onCompletedInitialLoad", arguments: arguments) + } + + public func onInitialLoadDidRedirect(url: URL) { + let arguments: [String: Any?] = [ + "url": url.absoluteString + ] + channel?.invokeMethod("onInitialLoadDidRedirect", arguments: arguments) + } + + public func onWillOpenInBrowser() { let arguments: [String: Any?] = [:] - channel?.invokeMethod("onChromeSafariBrowserCompletedInitialLoad", arguments: arguments) + channel?.invokeMethod("onWillOpenInBrowser", arguments: arguments) } - public func onChromeSafariBrowserClosed() { + public func onClosed() { let arguments: [String: Any?] = [:] - channel?.invokeMethod("onChromeSafariBrowserClosed", arguments: arguments) + channel?.invokeMethod("onClosed", arguments: arguments) } - public func onChromeSafariBrowserMenuItemActionPerform(id: Int64, url: URL, title: String?) { + public func onItemActionPerform(id: Int64, url: URL, title: String?) { let arguments: [String: Any?] = [ "id": id, "url": url.absoluteString, "title": title, ] - channel?.invokeMethod("onChromeSafariBrowserMenuItemActionPerform", arguments: arguments) + channel?.invokeMethod("onItemActionPerform", arguments: arguments) } public override func dispose() { diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index 2a8706c03..4ee837c04 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -4,6 +4,9 @@ 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_relation_type.dart'; +import '../types/prewarming_token.dart'; import '../util.dart'; import '../debug_logging_settings.dart'; @@ -41,7 +44,7 @@ class ChromeSafariBrowserNotOpenedException implements Exception { ///(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). /// ///**Supported Platforms/Implementations**: -///- Android native WebView +///- Android ///- iOS class ChromeSafariBrowser { ///Debug settings. @@ -85,17 +88,43 @@ class ChromeSafariBrowser { _debugLog(call.method, call.arguments); switch (call.method) { - case "onChromeSafariBrowserOpened": + case "onServiceConnected": + onServiceConnected(); + break; + case "onOpened": onOpened(); break; - case "onChromeSafariBrowserCompletedInitialLoad": - onCompletedInitialLoad(); + case "onCompletedInitialLoad": + final bool? didLoadSuccessfully = call.arguments["didLoadSuccessfully"]; + onCompletedInitialLoad(didLoadSuccessfully); + break; + case "onInitialLoadDidRedirect": + final String? url = call.arguments["url"]; + final Uri? uri = url != null ? Uri.tryParse(url) : null; + onInitialLoadDidRedirect(uri); + break; + case "onNavigationEvent": + final navigationEvent = CustomTabsNavigationEventType.fromNativeValue( + call.arguments["navigationEvent"]); + onNavigationEvent(navigationEvent); + break; + case "onRelationshipValidationResult": + final relation = + CustomTabsRelationType.fromNativeValue(call.arguments["relation"]); + final requestedOrigin = call.arguments["requestedOrigin"] != null + ? Uri.tryParse(call.arguments["requestedOrigin"]) + : null; + final bool result = call.arguments["result"]; + onRelationshipValidationResult(relation, requestedOrigin, result); break; - case "onChromeSafariBrowserClosed": + case "onWillOpenInBrowser": + onWillOpenInBrowser(); + break; + case "onClosed": onClosed(); this._isOpened = false; break; - case "onChromeSafariBrowserItemActionPerform": + case "onItemActionPerform": String url = call.arguments["url"]; String title = call.arguments["title"]; int id = call.arguments["id"].toInt(); @@ -112,25 +141,38 @@ class ChromeSafariBrowser { ///Opens the [ChromeSafariBrowser] instance with an [url]. /// - ///[url]: The [url] to load. On iOS, the [url] must use the `http` or `https` scheme. + ///[url] - The [url] to load. On iOS, the [url] is required and must use the `http` or `https` scheme. + /// + ///[headers] - extra request headers. Supported only on Android. + /// + ///[otherLikelyURLs] - Other likely destinations, sorted in decreasing likelihood order. Supported only on Android. /// - ///[options]: Options for the [ChromeSafariBrowser]. + ///[options] - Options for the [ChromeSafariBrowser]. /// - ///[settings]: Settings for the [ChromeSafariBrowser]. + ///[settings] - Settings for the [ChromeSafariBrowser]. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS Future open( - {required Uri url, + {Uri? url, + Map? headers, + List? otherLikelyURLs, @Deprecated('Use settings instead') // ignore: deprecated_member_use_from_same_package ChromeSafariBrowserClassOptions? options, ChromeSafariBrowserSettings? settings}) async { - assert(url.toString().isNotEmpty); - this.throwIsAlreadyOpened(message: 'Cannot open $url!'); - if (!kIsWeb && - (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS)) { - assert(['http', 'https'].contains(url.scheme), + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) { + assert(url != null, 'The specified URL must not be null on iOS.'); + assert(['http', 'https'].contains(url!.scheme), 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); } + if (url != null) { + assert(url.toString().isNotEmpty, 'The specified URL must not be empty.'); + } + this.throwIsAlreadyOpened(message: url != null ? 'Cannot open $url!' : ''); + + this._isOpened = true; List> menuItemList = []; _menuItems.forEach((key, value) { @@ -143,15 +185,87 @@ class ChromeSafariBrowser { Map args = {}; args.putIfAbsent('id', () => id); - args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('url', () => url?.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); args.putIfAbsent('settings', () => initialSettings); args.putIfAbsent('actionButton', () => _actionButton?.toMap()); args.putIfAbsent('menuItemList', () => menuItemList); await _sharedChannel.invokeMethod('open', args); - this._isOpened = true; + } + + ///Tells the browser to launch with [url]. + /// + ///[url] - initial url. + /// + ///[headers] - extra request headers. + /// + ///[otherLikelyURLs] - Other likely destinations, sorted in decreasing likelihood order. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Future launchUrl( + {required Uri url, + Map? headers, + List? otherLikelyURLs}) async { + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + await _channel.invokeMethod("launchUrl", args); + } + + ///Tells the browser of a likely future navigation to a URL. + ///The most likely URL has to be specified first. + ///Optionally, a list of other likely URLs can be provided. + ///They are treated as less likely than the first one, and have to be sorted in decreasing priority order. + ///These additional URLs may be ignored. All previous calls to this method will be deprioritized. + /// + ///[url] - Most likely URL, may be null if otherLikelyBundles is provided. + /// + ///[headers] - extra request headers. + /// + ///[otherLikelyURLs] - Other likely destinations, sorted in decreasing likelihood order. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsSession.mayLaunchUrl](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#mayLaunchUrl(android.net.Uri,android.os.Bundle,java.util.List%3Candroid.os.Bundle%3E))) + Future mayLaunchUrl( + {Uri? url, + Map? headers, + List? otherLikelyURLs}) async { + Map args = {}; + args.putIfAbsent('url', () => url?.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + await _channel.invokeMethod("mayLaunchUrl", args); + } + + ///Requests to validate a relationship between the application and an origin. + /// + ///See [here](https://developers.google.com/digital-asset-links/v1/getting-started) for documentation about Digital Asset Links. + ///This methods requests the browser to verify a relation with the calling application, to grant the associated rights. + /// + ///If this method returns `true`, the validation result will be provided through [onRelationshipValidationResult]. + ///Otherwise the request didn't succeed. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsSession.validateRelationship](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#validateRelationship(int,android.net.Uri,android.os.Bundle))) + Future validateRelationship( + {required CustomTabsRelationType relation, required Uri origin}) async { + Map args = {}; + args.putIfAbsent('relation', () => relation.toNativeValue()); + args.putIfAbsent('origin', () => origin.toString()); + await _channel.invokeMethod("validateRelationship", args); } ///Closes the [ChromeSafariBrowser] instance. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS Future close() async { Map args = {}; await _channel.invokeMethod("close", args); @@ -162,14 +276,34 @@ class ChromeSafariBrowser { ///**NOTE**: Not available in a Trusted Web Activity. /// ///**Supported Platforms/Implementations**: - ///- Android ([Official API - CustomTabsIntent.Builder.setActionButton ](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsIntent.Builder#setActionButton(android.graphics.Bitmap,%20java.lang.String,%20android.app.PendingIntent,%20boolean))) + ///- Android ([Official API - CustomTabsIntent.Builder.setActionButton](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsIntent.Builder#setActionButton(android.graphics.Bitmap,%20java.lang.String,%20android.app.PendingIntent,%20boolean))) void setActionButton(ChromeSafariBrowserActionButton actionButton) { this._actionButton = actionButton; } + ///Updates the [ChromeSafariBrowserActionButton.icon] and [ChromeSafariBrowserActionButton.description]. + /// + ///**NOTE**: Not available in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsSession.setActionButton](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#setActionButton(android.graphics.Bitmap,java.lang.String))) + Future updateActionButton( + {required Uint8List icon, required String description}) async { + Map args = {}; + args.putIfAbsent('icon', () => icon); + args.putIfAbsent('description', () => description); + await _channel.invokeMethod("updateActionButton", args); + _actionButton?.icon = icon; + _actionButton?.description = description; + } + ///Adds a [ChromeSafariBrowserMenuItem] to the menu. /// ///**NOTE**: Not available in an Android Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS void addMenuItem(ChromeSafariBrowserMenuItem menuItem) { this._menuItems[menuItem.id] = menuItem; } @@ -177,6 +311,10 @@ class ChromeSafariBrowser { ///Adds a list of [ChromeSafariBrowserMenuItem] to the menu. /// ///**NOTE**: Not available in an Android Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS void addMenuItems(List menuItems) { menuItems.forEach((menuItem) { this._menuItems[menuItem.id] = menuItem; @@ -186,21 +324,135 @@ class ChromeSafariBrowser { ///On Android, returns `true` if Chrome Custom Tabs is available. ///On iOS, returns `true` if SFSafariViewController is available. ///Otherwise returns `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS static Future isAvailable() async { Map args = {}; return await _sharedChannel.invokeMethod("isAvailable", args); } - ///Event fires when the [ChromeSafariBrowser] is opened. + ///Clear associated website data accrued from browsing activity within your app. + ///This includes all local storage, cached resources, and cookies. + /// + ///**NOTE for iOS**: available on iOS 16.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - SFSafariViewController.DataStore.clearWebsiteData](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/datastore/3981117-clearwebsitedata)) + static Future clearWebsiteData() async { + Map args = {}; + await _sharedChannel.invokeMethod("clearWebsiteData", args); + } + + ///Prewarms a connection to each URL. SFSafariViewController will automatically use a + ///prewarmed connection if possible when loading its initial URL. + /// + ///Returns a token object that corresponds to the requested URLs. You must keep a strong + ///reference to this token as long as you expect the prewarmed connections to remain open. If the same + ///server is requested in multiple calls to this method, all of the corresponding tokens must be + ///invalidated or released to end the prewarmed connection to that server. + /// + ///This method uses a best-effort approach to prewarming connections, but may delay + ///or drop requests based on the volume of requests made by your app. Use this method when you expect + ///to present the browser soon. Many HTTP servers time out connections after a few minutes. + ///After a timeout, prewarming delivers less performance benefit. + /// + ///[URLs] - the URLs of servers that the browser should prewarm connections to. + ///Only supports URLs with `http://` or `https://` schemes. + /// + ///**NOTE for iOS**: available on iOS 15.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - SFSafariViewController.prewarmConnections](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/3752133-prewarmconnections)) + static Future prewarmConnections(List URLs) async { + Map args = {}; + args.putIfAbsent('URLs', () => URLs.map((e) => e.toString()).toList()); + Map? result = + (await _sharedChannel.invokeMethod("prewarmConnections", args)) + ?.cast(); + return PrewarmingToken.fromMap(result); + } + + ///Ends all prewarmed connections associated with the token, except for connections that are also kept alive by other tokens. + /// + ///**NOTE for iOS**: available on iOS 15.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - SFSafariViewController.prewarmConnections](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller/3752133-prewarmconnections)) + static Future invalidatePrewarmingToken(PrewarmingToken prewarmingToken) async { + Map args = {}; + args.putIfAbsent('prewarmingToken', () => prewarmingToken.toMap()); + await _sharedChannel.invokeMethod("invalidatePrewarmingToken", args); + } + + ///Event fired when the when connecting from Android Custom Tabs Service. + /// + ///**Supported Platforms/Implementations**: + ///- Android + void onServiceConnected() {} + + ///Event fired when the [ChromeSafariBrowser] is opened. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS void onOpened() {} - ///Event fires when the initial URL load is complete. - void onCompletedInitialLoad() {} + ///Event fired when the initial URL load is complete. + /// + ///[didLoadSuccessfully] - `true` if loading completed successfully; otherwise, `false`. Supported only on iOS. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS ([Official API - SFSafariViewControllerDelegate.safariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/1621215-safariviewcontroller)) + void onCompletedInitialLoad(bool? didLoadSuccessfully) {} + + ///Event fired when the initial URL load is complete. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - SFSafariViewControllerDelegate.safariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/2923545-safariviewcontroller)) + void onInitialLoadDidRedirect(Uri? url) {} + + ///Event fired when a navigation event happens. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsCallback.onNavigationEvent](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsCallback#onNavigationEvent(int,android.os.Bundle))) + void onNavigationEvent(CustomTabsNavigationEventType? navigationEvent) {} + + ///Event fired when a relationship validation result is available. + /// + ///[relation] - Relation for which the result is available. Value previously passed to [validateRelationship]. + /// + ///[requestedOrigin] - Origin requested. Value previously passed to [validateRelationship]. + /// + ///[result] - Whether the relation was validated. + /// + ///**Supported Platforms/Implementations**: + ///- Android ([Official API - CustomTabsCallback.onRelationshipValidationResult](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsCallback#onRelationshipValidationResult(int,android.net.Uri,boolean,android.os.Bundle))) + void onRelationshipValidationResult( + CustomTabsRelationType? relation, Uri? requestedOrigin, bool result) {} - ///Event fires when the [ChromeSafariBrowser] is closed. + ///Event fired when the user opens the current page in the default browser by tapping the toolbar button. + /// + ///**NOTE for iOS**: available on iOS 14.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - SFSafariViewControllerDelegate.safariViewControllerWillOpenInBrowser](https://developer.apple.com/documentation/safariservices/sfsafariviewcontrollerdelegate/3650426-safariviewcontrollerwillopeninbr)) + void onWillOpenInBrowser() {} + + ///Event fired when the [ChromeSafariBrowser] is closed. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS void onClosed() {} ///Returns `true` if the [ChromeSafariBrowser] instance is opened, otherwise `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android + ///- iOS bool isOpened() { return this._isOpened; } @@ -225,6 +477,9 @@ class ChromeSafariBrowser { ///Class that represents a custom action button for a [ChromeSafariBrowser] instance. /// ///**NOTE**: Not available in an Android Trusted Web Activity. +/// +///**Supported Platforms/Implementations**: +///- Android class ChromeSafariBrowserActionButton { ///The action button id. It should be different from the [ChromeSafariBrowserMenuItem.id]. int id; @@ -270,6 +525,10 @@ class ChromeSafariBrowserActionButton { ///Class that represents a custom menu item for a [ChromeSafariBrowser] instance. /// ///**NOTE**: Not available in an Android Trusted Web Activity. +/// +///**Supported Platforms/Implementations**: +///- Android +///- iOS class ChromeSafariBrowserMenuItem { ///The menu item id. It should be different from [ChromeSafariBrowserActionButton.id]. int id; diff --git a/lib/src/types/custom_tabs_navigation_event_type.dart b/lib/src/types/custom_tabs_navigation_event_type.dart new file mode 100644 index 000000000..57c58efd8 --- /dev/null +++ b/lib/src/types/custom_tabs_navigation_event_type.dart @@ -0,0 +1,41 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; +import '../chrome_safari_browser/chrome_safari_browser.dart'; + +part 'custom_tabs_navigation_event_type.g.dart'; + +///The type corresponding to the navigation event of [ChromeSafariBrowser.onNavigationEvent]. +@ExchangeableEnum() +class CustomTabsNavigationEventType_ { + // ignore: unused_field + final int _value; + + // ignore: unused_field + final int? _nativeValue = null; + + const CustomTabsNavigationEventType_._internal(this._value); + + ///Sent when the tab has started loading a page. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 1)]) + static const STARTED = const CustomTabsNavigationEventType_._internal(1); + + ///Sent when the tab has finished loading a page. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 2)]) + static const FINISHED = const CustomTabsNavigationEventType_._internal(2); + + ///Sent when the tab couldn't finish loading due to a failure. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 3)]) + static const FAILED = const CustomTabsNavigationEventType_._internal(3); + + ///Sent when loading was aborted by a user action before it finishes like clicking on a link or refreshing the page. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 4)]) + static const ABORTED = const CustomTabsNavigationEventType_._internal(4); + + ///Sent when the tab becomes visible. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 5)]) + static const TAB_SHOWN = const CustomTabsNavigationEventType_._internal(5); + + ///Sent when the tab becomes hidden. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 6)]) + static const TAB_HIDDEN = const CustomTabsNavigationEventType_._internal(6); +} diff --git a/lib/src/types/custom_tabs_navigation_event_type.g.dart b/lib/src/types/custom_tabs_navigation_event_type.g.dart new file mode 100644 index 000000000..b4dfe78e2 --- /dev/null +++ b/lib/src/types/custom_tabs_navigation_event_type.g.dart @@ -0,0 +1,175 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'custom_tabs_navigation_event_type.dart'; + +// ************************************************************************** +// ExchangeableEnumGenerator +// ************************************************************************** + +///The type corresponding to the navigation event of [ChromeSafariBrowser.onNavigationEvent]. +class CustomTabsNavigationEventType { + final int _value; + final int? _nativeValue; + const CustomTabsNavigationEventType._internal(this._value, this._nativeValue); +// ignore: unused_element + factory CustomTabsNavigationEventType._internalMultiPlatform( + int value, Function nativeValue) => + CustomTabsNavigationEventType._internal(value, nativeValue()); + + ///Sent when the tab has started loading a page. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final STARTED = + CustomTabsNavigationEventType._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 1; + default: + break; + } + return null; + }); + + ///Sent when the tab has finished loading a page. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final FINISHED = + CustomTabsNavigationEventType._internalMultiPlatform(2, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 2; + default: + break; + } + return null; + }); + + ///Sent when the tab couldn't finish loading due to a failure. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final FAILED = + CustomTabsNavigationEventType._internalMultiPlatform(3, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 3; + default: + break; + } + return null; + }); + + ///Sent when loading was aborted by a user action before it finishes like clicking on a link or refreshing the page. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final ABORTED = + CustomTabsNavigationEventType._internalMultiPlatform(4, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 4; + default: + break; + } + return null; + }); + + ///Sent when the tab becomes visible. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final TAB_SHOWN = + CustomTabsNavigationEventType._internalMultiPlatform(5, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 5; + default: + break; + } + return null; + }); + + ///Sent when the tab becomes hidden. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final TAB_HIDDEN = + CustomTabsNavigationEventType._internalMultiPlatform(6, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 6; + default: + break; + } + return null; + }); + + ///Set of all values of [CustomTabsNavigationEventType]. + static final Set values = [ + CustomTabsNavigationEventType.STARTED, + CustomTabsNavigationEventType.FINISHED, + CustomTabsNavigationEventType.FAILED, + CustomTabsNavigationEventType.ABORTED, + CustomTabsNavigationEventType.TAB_SHOWN, + CustomTabsNavigationEventType.TAB_HIDDEN, + ].toSet(); + + ///Gets a possible [CustomTabsNavigationEventType] instance from [int] value. + static CustomTabsNavigationEventType? fromValue(int? value) { + if (value != null) { + try { + return CustomTabsNavigationEventType.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets a possible [CustomTabsNavigationEventType] instance from a native value. + static CustomTabsNavigationEventType? fromNativeValue(int? value) { + if (value != null) { + try { + return CustomTabsNavigationEventType.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 'STARTED'; + case 2: + return 'FINISHED'; + case 3: + return 'FAILED'; + case 4: + return 'ABORTED'; + case 5: + return 'TAB_SHOWN'; + case 6: + return 'TAB_HIDDEN'; + } + return _value.toString(); + } +} diff --git a/lib/src/types/custom_tabs_relation_type.dart b/lib/src/types/custom_tabs_relation_type.dart new file mode 100644 index 000000000..2809bee56 --- /dev/null +++ b/lib/src/types/custom_tabs_relation_type.dart @@ -0,0 +1,24 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +part 'custom_tabs_relation_type.g.dart'; + +///Custom Tabs relation for which the result is available. +@ExchangeableEnum() +class CustomTabsRelationType_ { + // ignore: unused_field + final int _value; + + // ignore: unused_field + final int? _nativeValue = null; + + const CustomTabsRelationType_._internal(this._value); + + ///For App -> Web transitions, requests the app to use the declared origin to be used as origin for the client app in the web APIs context. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 1)]) + static const USE_AS_ORIGIN = const CustomTabsRelationType_._internal(1); + + ///Requests the ability to handle all URLs from a given origin. + @EnumSupportedPlatforms(platforms: [EnumAndroidPlatform(value: 2)]) + static const HANDLE_ALL_URLS = const CustomTabsRelationType_._internal(2); +} diff --git a/lib/src/types/custom_tabs_relation_type.g.dart b/lib/src/types/custom_tabs_relation_type.g.dart new file mode 100644 index 000000000..f2b0e38f4 --- /dev/null +++ b/lib/src/types/custom_tabs_relation_type.g.dart @@ -0,0 +1,103 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'custom_tabs_relation_type.dart'; + +// ************************************************************************** +// ExchangeableEnumGenerator +// ************************************************************************** + +///Custom Tabs relation for which the result is available. +class CustomTabsRelationType { + final int _value; + final int? _nativeValue; + const CustomTabsRelationType._internal(this._value, this._nativeValue); +// ignore: unused_element + factory CustomTabsRelationType._internalMultiPlatform( + int value, Function nativeValue) => + CustomTabsRelationType._internal(value, nativeValue()); + + ///For App -> Web transitions, requests the app to use the declared origin to be used as origin for the client app in the web APIs context. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final USE_AS_ORIGIN = + CustomTabsRelationType._internalMultiPlatform(1, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 1; + default: + break; + } + return null; + }); + + ///Requests the ability to handle all URLs from a given origin. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView + static final HANDLE_ALL_URLS = + CustomTabsRelationType._internalMultiPlatform(2, () { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 2; + default: + break; + } + return null; + }); + + ///Set of all values of [CustomTabsRelationType]. + static final Set values = [ + CustomTabsRelationType.USE_AS_ORIGIN, + CustomTabsRelationType.HANDLE_ALL_URLS, + ].toSet(); + + ///Gets a possible [CustomTabsRelationType] instance from [int] value. + static CustomTabsRelationType? fromValue(int? value) { + if (value != null) { + try { + return CustomTabsRelationType.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets a possible [CustomTabsRelationType] instance from a native value. + static CustomTabsRelationType? fromNativeValue(int? value) { + if (value != null) { + try { + return CustomTabsRelationType.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 'USE_AS_ORIGIN'; + case 2: + return 'HANDLE_ALL_URLS'; + } + return _value.toString(); + } +} diff --git a/lib/src/types/main.dart b/lib/src/types/main.dart index a1e3078ce..29e25979a 100644 --- a/lib/src/types/main.dart +++ b/lib/src/types/main.dart @@ -215,3 +215,6 @@ export 'printer.dart' show Printer; export 'window_type.dart' show WindowType; export 'window_style_mask.dart' show WindowStyleMask; export 'window_titlebar_separator_style.dart' show WindowTitlebarSeparatorStyle; +export 'custom_tabs_navigation_event_type.dart' show CustomTabsNavigationEventType; +export 'custom_tabs_relation_type.dart' show CustomTabsRelationType; +export 'prewarming_token.dart' show PrewarmingToken; \ No newline at end of file diff --git a/lib/src/types/prewarming_token.dart b/lib/src/types/prewarming_token.dart new file mode 100644 index 000000000..605f9a5c9 --- /dev/null +++ b/lib/src/types/prewarming_token.dart @@ -0,0 +1,14 @@ +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +import '../chrome_safari_browser/chrome_safari_browser.dart'; + +part 'prewarming_token.g.dart'; + +///Class that represents the Prewarming Token returned by [ChromeSafariBrowser.prewarmConnections]. +@ExchangeableObject() +class PrewarmingToken_ { + ///Prewarming Token id. + final String id; + + PrewarmingToken_({required this.id}); +} diff --git a/lib/src/types/prewarming_token.g.dart b/lib/src/types/prewarming_token.g.dart new file mode 100644 index 000000000..92cf7130e --- /dev/null +++ b/lib/src/types/prewarming_token.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'prewarming_token.dart'; + +// ************************************************************************** +// ExchangeableObjectGenerator +// ************************************************************************** + +///Class that represents the Prewarming Token returned by [ChromeSafariBrowser.prewarmConnections]. +class PrewarmingToken { + ///Prewarming Token id. + final String id; + PrewarmingToken({required this.id}); + + ///Gets a possible [PrewarmingToken] instance from a [Map] value. + static PrewarmingToken? fromMap(Map? map) { + if (map == null) { + return null; + } + final instance = PrewarmingToken( + id: map['id'], + ); + return instance; + } + + ///Converts instance to a map. + Map toMap() { + return { + "id": id, + }; + } + + ///Converts instance to a map. + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'PrewarmingToken{id: $id}'; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 89d154ff8..b8377c1da 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.7 +version: 6.0.0-beta.9 homepage: https://inappwebview.dev/ repository: https://github.com/pichillilorenzo/flutter_inappwebview issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues From 8e9c10246aa18e833be590b467eab828465c56f4 Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Tue, 25 Oct 2022 11:18:53 +0200 Subject: [PATCH 2/3] Added startAnimations, exitAnimations, navigationBarColor, navigationBarDividerColor, secondaryToolbarColor ChromeSafariBrowser settings for Android, Added getVariationsHeader WebView static method, All ChromeSafariBrowserSettings properties are optionals --- CHANGELOG.md | 5 +- .../InAppWebViewStatic.java | 8 + .../ChromeCustomTabsActivity.java | 49 ++- .../ChromeCustomTabsChannelDelegate.java | 13 +- .../ChromeCustomTabsSettings.java | 39 +++ .../CustomTabActivityHelper.java | 19 + .../TrustedWebActivity.java | 6 +- .../types/AndroidResource.java | 103 ++++++ .../custom_action_button.dart | 11 +- .../custom_menu_item.dart | 4 +- .../chrome_safari_browser/custom_tabs.dart | 40 ++- .../chrome_safari_browser/main.dart | 2 + .../chrome_safari_browser/open_and_close.dart | 33 +- .../sf_safari_view_controller.dart | 43 +++ .../trusted_web_activity.dart | 26 +- example/integration_test/util.dart | 36 +- .../SafariBrowserSettings.swift | 2 +- lib/src/android/webview_feature.dart | 5 + lib/src/android/webview_feature.g.dart | 5 + .../chrome_safari_browser.dart | 26 +- .../chrome_safari_browser_settings.dart | 203 +++++------ .../chrome_safari_browser_settings.g.dart | 330 ++++++++++++++++++ lib/src/chrome_safari_browser/main.dart | 6 +- .../in_app_webview_controller.dart | 18 + lib/src/types/android_resource.dart | 33 ++ lib/src/types/android_resource.g.dart | 66 ++++ lib/src/types/main.dart | 3 +- scripts/test.sh | 6 +- test_node_server/index.js | 13 + 29 files changed, 987 insertions(+), 166 deletions(-) create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java create mode 100644 example/integration_test/chrome_safari_browser/sf_safari_view_controller.dart create mode 100644 lib/src/chrome_safari_browser/chrome_safari_browser_settings.g.dart create mode 100644 lib/src/types/android_resource.dart create mode 100644 lib/src/types/android_resource.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 857d48418..a5e6418d3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,16 @@ - Added `headers`, `otherLikelyURLs` arguments on `ChromeSafariBrowser.open` method for Android - Added `onNavigationEvent`, `onServiceConnected`, `onRelationshipValidationResult` events on `ChromeSafariBrowser` for Android - Added `mayLaunchUrl`, `launchUrl`, `updateActionButton`, `validateRelationship` methods on `ChromeSafariBrowser` for Android +- Added `startAnimations`, `exitAnimations`, `navigationBarColor`, `navigationBarDividerColor`, `secondaryToolbarColor` ChromeSafariBrowser settings for Android - Added `didLoadSuccessfully` optional argument on `ChromeSafariBrowser.onCompletedInitialLoad` event for iOS - Added `onInitialLoadDidRedirect`, `onWillOpenInBrowser` events on `ChromeSafariBrowser` for iOS - Added `clearWebsiteData`, `prewarmConnections`, `invalidatePrewarmingToken` static methods on `ChromeSafariBrowser` for iOS +- Added `getVariationsHeader` WebView static method ### BREAKING CHANGES - `ChromeSafariBrowser.onCompletedInitialLoad` event has an optional argument +- All `ChromeSafariBrowserSettings` properties are optionals ## 6.0.0-beta.8 @@ -78,7 +81,7 @@ - Added `PullToRefreshController.isEnabled` method - Updated `getMetaThemeColor` on iOS 15.0+ - Deprecated `onLoadError` for `onReceivedError`. `onReceivedError` will be called also for subframes -- Deprecated `onLoadHttpError` for `onReceivedError`. `onReceivedHttpError` will be called also for subframes +- Deprecated `onLoadHttpError` for `onReceivedHttpError`. `onReceivedHttpError` will be called also for subframes ### BREAKING CHANGES 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 816adb88f..8ceea9784 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewStatic.java @@ -114,6 +114,14 @@ public void onReceiveValue(Boolean value) { } result.success(true); break; + case "getVariationsHeader": + if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_VARIATIONS_HEADER)) { + result.success(WebViewCompat.getVariationsHeader()); + } + else { + result.success(null); + } + break; default: result.notImplemented(); } 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 9b8908e65..0216c3a55 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 @@ -21,6 +21,7 @@ import androidx.browser.customtabs.CustomTabsSession; import com.pichillilorenzo.flutter_inappwebview.R; +import com.pichillilorenzo.flutter_inappwebview.types.AndroidResource; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsActionButton; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsMenuItem; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; @@ -156,35 +157,27 @@ public void onRelationshipValidationResult(@CustomTabsService.Relation int relat public void launchUrl(@NonNull String url, @Nullable Map headers, @Nullable List otherLikelyURLs) { - Uri uri = mayLaunchUrl(url, headers, otherLikelyURLs); + mayLaunchUrl(url, otherLikelyURLs); builder = new CustomTabsIntent.Builder(customTabsSession); prepareCustomTabs(); CustomTabsIntent customTabsIntent = builder.build(); prepareCustomTabsIntent(customTabsIntent); - CustomTabActivityHelper.openCustomTab(this, customTabsIntent, uri, CHROME_CUSTOM_TAB_REQUEST_CODE); + CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), headers, CHROME_CUSTOM_TAB_REQUEST_CODE); } - public Uri mayLaunchUrl(@NonNull String url, - @Nullable Map headers, - @Nullable List otherLikelyURLs) { - Uri uri = Uri.parse(url); - Bundle bundleHeaders = new Bundle(); - if (headers != null) { - for (Map.Entry header : headers.entrySet()) { - bundleHeaders.putString(header.getKey(), header.getValue()); - } - } + public boolean mayLaunchUrl(@Nullable String url, @Nullable List otherLikelyURLs) { + Uri uri = url != null ? Uri.parse(url) : null; + List bundleOtherLikelyURLs = new ArrayList<>(); if (otherLikelyURLs != null) { + Bundle bundleOtherLikelyURL = new Bundle(); for (String otherLikelyURL : otherLikelyURLs) { - Bundle bundleOtherLikelyURL = new Bundle(); bundleOtherLikelyURL.putString(CustomTabsService.KEY_URL, otherLikelyURL); } } - customTabActivityHelper.mayLaunchUrl(uri, bundleHeaders, bundleOtherLikelyURLs); - return uri; + return customTabActivityHelper.mayLaunchUrl(uri, null, bundleOtherLikelyURLs); } public void customTabsConnected() { @@ -206,16 +199,34 @@ private void prepareCustomTabs() { builder.setShareState(customSettings.shareState); } + CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) { - CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); - builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder - .setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor)) - .build()); + defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor)); } + if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor)); + } + if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor)); + } + if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) { + defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor)); + } + builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build()); builder.setShowTitle(customSettings.showTitle); builder.setUrlBarHidingEnabled(customSettings.enableUrlBarHiding); builder.setInstantAppsEnabled(customSettings.instantAppsEnabled); + if (customSettings.startAnimations.size() == 2) { + builder.setStartAnimations(this, + customSettings.startAnimations.get(0).getIdentifier(this), + customSettings.startAnimations.get(1).getIdentifier(this)); + } + if (customSettings.exitAnimations.size() == 2) { + builder.setExitAnimations(this, + customSettings.exitAnimations.get(0).getIdentifier(this), + customSettings.exitAnimations.get(1).getIdentifier(this)); + } for (CustomTabsMenuItem menuItem : menuItems) { builder.addMenuItem(menuItem.getLabel(), 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 22160e976..1af27d3ba 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 @@ -48,14 +48,8 @@ public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodCh case "mayLaunchUrl": if (chromeCustomTabsActivity != null) { String url = (String) call.argument("url"); - if (url != null) { - Map headers = (Map) call.argument("headers"); - List otherLikelyURLs = (List) call.argument("otherLikelyURLs"); - chromeCustomTabsActivity.mayLaunchUrl(url, headers, otherLikelyURLs); - result.success(true); - } else { - result.success(false); - } + List otherLikelyURLs = (List) call.argument("otherLikelyURLs"); + result.success(chromeCustomTabsActivity.mayLaunchUrl(url, otherLikelyURLs)); } else { result.success(false); } @@ -74,8 +68,7 @@ public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodCh if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) { Integer relation = (Integer) call.argument("relation"); String origin = (String) call.argument("origin"); - chromeCustomTabsActivity.customTabsSession.validateRelationship(relation, Uri.parse(origin), null); - result.success(true); + result.success(chromeCustomTabsActivity.customTabsSession.validateRelationship(relation, Uri.parse(origin), null)); } else { result.success(false); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java index 3867020e5..10f3e707e 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java @@ -9,6 +9,7 @@ import androidx.browser.trusted.TrustedWebActivityDisplayMode; import com.pichillilorenzo.flutter_inappwebview.ISettings; +import com.pichillilorenzo.flutter_inappwebview.types.AndroidResource; import java.util.ArrayList; import java.util.HashMap; @@ -25,6 +26,12 @@ public class ChromeCustomTabsSettings implements ISettings additionalTrustedOrigins = new ArrayList<>(); public TrustedWebActivityDisplayMode displayMode = null; public Integer screenOrientation = ScreenOrientation.DEFAULT; + public List startAnimations = new ArrayList<>(); + public List exitAnimations = new ArrayList<>(); @NonNull @Override @@ -59,6 +68,15 @@ public ChromeCustomTabsSettings parse(@NonNull Map options) { case "toolbarBackgroundColor": toolbarBackgroundColor = (String) value; break; + case "navigationBarColor": + navigationBarColor = (String) value; + break; + case "navigationBarDividerColor": + navigationBarDividerColor = (String) value; + break; + case "secondaryToolbarColor": + secondaryToolbarColor = (String) value; + break; case "enableUrlBarHiding": enableUrlBarHiding = (Boolean) value; break; @@ -102,6 +120,24 @@ public ChromeCustomTabsSettings parse(@NonNull Map options) { case "screenOrientation": screenOrientation = (Integer) value; break; + case "startAnimations": + List> startAnimationsList = (List>) value; + for (Map startAnimation : startAnimationsList) { + AndroidResource androidResource = AndroidResource.fromMap(startAnimation); + if (androidResource != null) { + startAnimations.add(AndroidResource.fromMap(startAnimation)); + } + } + break; + case "exitAnimations": + List> exitAnimationsList = (List>) value; + for (Map exitAnimation : exitAnimationsList) { + AndroidResource androidResource = AndroidResource.fromMap(exitAnimation); + if (androidResource != null) { + exitAnimations.add(AndroidResource.fromMap(exitAnimation)); + } + } + break; } } @@ -115,6 +151,9 @@ public Map toMap() { options.put("addDefaultShareMenuItem", addDefaultShareMenuItem); options.put("showTitle", showTitle); options.put("toolbarBackgroundColor", toolbarBackgroundColor); + options.put("navigationBarColor", navigationBarColor); + options.put("navigationBarDividerColor", navigationBarDividerColor); + options.put("secondaryToolbarColor", secondaryToolbarColor); options.put("enableUrlBarHiding", enableUrlBarHiding); options.put("instantAppsEnabled", instantAppsEnabled); options.put("packageName", packageName); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java index 67e903ce2..57e78f077 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java @@ -4,6 +4,7 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; +import android.provider.Browser; import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabsCallback; @@ -14,6 +15,7 @@ import androidx.browser.trusted.TrustedWebActivityIntent; import java.util.List; +import java.util.Map; /** * This is a helper class to manage the connection to the Custom Tabs Service. @@ -35,18 +37,35 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback { public static void openCustomTab(Activity activity, CustomTabsIntent customTabsIntent, Uri uri, + @Nullable Map headers, int requestCode) { customTabsIntent.intent.setData(uri); + if (headers != null) { + Bundle bundleHeaders = new Bundle(); + for (Map.Entry header : headers.entrySet()) { + bundleHeaders.putString(header.getKey(), header.getValue()); + } + customTabsIntent.intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders); + } activity.startActivityForResult(customTabsIntent.intent, requestCode); } public static void openCustomTab(Activity activity, TrustedWebActivityIntent trustedWebActivityIntent, Uri uri, + @Nullable Map headers, int requestCode) { trustedWebActivityIntent.getIntent().setData(uri); + if (headers != null) { + Bundle bundleHeaders = new Bundle(); + for (Map.Entry header : headers.entrySet()) { + bundleHeaders.putString(header.getKey(), header.getValue()); + } + trustedWebActivityIntent.getIntent().putExtra(Browser.EXTRA_HEADERS, bundleHeaders); + } activity.startActivityForResult(trustedWebActivityIntent.getIntent(), requestCode); } + public static boolean isAvailable(Activity activity) { return CustomTabsHelper.getPackageNameToUse(activity) != null; 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 cd8721e21..b86578a07 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 @@ -31,15 +31,16 @@ public void launchUrl(@NonNull String url, if (customTabsSession == null) { return; } + Uri uri = Uri.parse(url); - Uri uri = mayLaunchUrl(url, headers, otherLikelyURLs); + mayLaunchUrl(url, otherLikelyURLs); builder = new TrustedWebActivityIntentBuilder(uri); prepareCustomTabs(); TrustedWebActivityIntent trustedWebActivityIntent = builder.build(customTabsSession); prepareCustomTabsIntent(trustedWebActivityIntent); - CustomTabActivityHelper.openCustomTab(this, trustedWebActivityIntent, uri, CHROME_CUSTOM_TAB_REQUEST_CODE); + CustomTabActivityHelper.openCustomTab(this, trustedWebActivityIntent, uri, headers, CHROME_CUSTOM_TAB_REQUEST_CODE); } @Override @@ -48,7 +49,6 @@ public void customTabsConnected() { if (initialUrl != null) { launchUrl(initialUrl, initialHeaders, initialOtherLikelyURLs); } - } private void prepareCustomTabs() { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java new file mode 100644 index 000000000..ba1453594 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java @@ -0,0 +1,103 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class AndroidResource { + @NonNull + String name; + @Nullable + String defType; + @Nullable + String defPackage; + + public AndroidResource(@NonNull String name, @Nullable String defType, @Nullable String defPackage) { + this.name = name; + this.defType = defType; + this.defPackage = defPackage; + } + + @Nullable + public static AndroidResource fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String name = (String) map.get("name"); + String defType = (String) map.get("defType"); + String defPackage = (String) map.get("defPackage"); + return new AndroidResource(name, defType, defPackage); + } + + public Map toMap() { + Map urlRequestMap = new HashMap<>(); + urlRequestMap.put("name", name); + urlRequestMap.put("defType", defType); + urlRequestMap.put("defPackage", defPackage); + return urlRequestMap; + } + + @NonNull + public String getName() { + return name; + } + + public void setName(@NonNull String name) { + this.name = name; + } + + @Nullable + public String getDefType() { + return defType; + } + + public void setDefType(@Nullable String defType) { + this.defType = defType; + } + + @Nullable + public String getDefPackage() { + return defPackage; + } + + public void setDefPackage(@Nullable String defPackage) { + this.defPackage = defPackage; + } + + public int getIdentifier(@NonNull Context ctx) { + return ctx.getResources().getIdentifier(name, defType, defPackage); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AndroidResource that = (AndroidResource) o; + + if (!name.equals(that.name)) return false; + if (defType != null ? !defType.equals(that.defType) : that.defType != null) return false; + return defPackage != null ? defPackage.equals(that.defPackage) : that.defPackage == null; + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + (defType != null ? defType.hashCode() : 0); + result = 31 * result + (defPackage != null ? defPackage.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "AndroidResource{" + + "name='" + name + '\'' + + ", type='" + defType + '\'' + + ", defPackage='" + defPackage + '\'' + + '}'; + } +} diff --git a/example/integration_test/chrome_safari_browser/custom_action_button.dart b/example/integration_test/chrome_safari_browser/custom_action_button.dart index 7b7712312..db3ffa47a 100644 --- a/example/integration_test/chrome_safari_browser/custom_action_button.dart +++ b/example/integration_test/chrome_safari_browser/custom_action_button.dart @@ -13,10 +13,12 @@ void customActionButton() { TargetPlatform.android, ].contains(defaultTargetPlatform); - test('add custom action button', () async { + test('add custom action button and update icon', () async { var chromeSafariBrowser = MyChromeSafariBrowser(); var actionButtonIcon = await rootBundle.load('test_assets/images/flutter-logo.png'); + var actionButtonIcon2 = + await rootBundle.load('test_assets/images/flutter-logo.jpg'); chromeSafariBrowser.setActionButton(ChromeSafariBrowserActionButton( id: 1, description: 'Action Button description', @@ -25,15 +27,18 @@ void customActionButton() { expect(chromeSafariBrowser.isOpened(), false); await chromeSafariBrowser.open(url: TEST_URL_1); - await chromeSafariBrowser.browserCreated.future; + await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); }, throwsA(isInstanceOf())); await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); + await chromeSafariBrowser.updateActionButton( + icon: actionButtonIcon2.buffer.asUint8List(), + description: 'New Action Button description'); await chromeSafariBrowser.close(); - await chromeSafariBrowser.browserClosed.future; + await chromeSafariBrowser.closed.future; expect(chromeSafariBrowser.isOpened(), false); }, skip: shouldSkip); } diff --git a/example/integration_test/chrome_safari_browser/custom_menu_item.dart b/example/integration_test/chrome_safari_browser/custom_menu_item.dart index 3f002f766..02bc43509 100644 --- a/example/integration_test/chrome_safari_browser/custom_menu_item.dart +++ b/example/integration_test/chrome_safari_browser/custom_menu_item.dart @@ -18,7 +18,7 @@ void customMenuItem() { expect(chromeSafariBrowser.isOpened(), false); await chromeSafariBrowser.open(url: TEST_URL_1); - await chromeSafariBrowser.browserCreated.future; + await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); @@ -26,7 +26,7 @@ void customMenuItem() { await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); await chromeSafariBrowser.close(); - await chromeSafariBrowser.browserClosed.future; + await chromeSafariBrowser.closed.future; expect(chromeSafariBrowser.isOpened(), false); }, skip: shouldSkip); } diff --git a/example/integration_test/chrome_safari_browser/custom_tabs.dart b/example/integration_test/chrome_safari_browser/custom_tabs.dart index fbfe54963..dd1a419c3 100644 --- a/example/integration_test/chrome_safari_browser/custom_tabs.dart +++ b/example/integration_test/chrome_safari_browser/custom_tabs.dart @@ -20,7 +20,7 @@ void customTabs() { await chromeSafariBrowser.open( url: TEST_URL_1, settings: ChromeSafariBrowserSettings(isSingleInstance: true)); - await chromeSafariBrowser.browserCreated.future; + await expectLater(chromeSafariBrowser.opened.future, completes); expect(chromeSafariBrowser.isOpened(), true); expect(() async { await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); @@ -28,7 +28,43 @@ void customTabs() { await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); await chromeSafariBrowser.close(); - await chromeSafariBrowser.browserClosed.future; + await expectLater(chromeSafariBrowser.closed.future, completes); + expect(chromeSafariBrowser.isOpened(), false); + }); + + test('mayLaunchUrl and launchUrl', () async { + var chromeSafariBrowser = MyChromeSafariBrowser(); + expect(chromeSafariBrowser.isOpened(), false); + + await chromeSafariBrowser.open(); + await expectLater(chromeSafariBrowser.serviceConnected.future, completes); + expect(chromeSafariBrowser.isOpened(), true); + expect( + await chromeSafariBrowser.mayLaunchUrl( + url: TEST_URL_1, otherLikelyURLs: [TEST_CROSS_PLATFORM_URL_1]), + true); + await chromeSafariBrowser.launchUrl( + url: TEST_URL_1, + headers: {'accept-language': 'it-IT'}, + otherLikelyURLs: [TEST_CROSS_PLATFORM_URL_1]); + await expectLater(chromeSafariBrowser.opened.future, completes); + await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); + await chromeSafariBrowser.close(); + await expectLater(chromeSafariBrowser.closed.future, completes); + expect(chromeSafariBrowser.isOpened(), false); + }); + + test('onNavigationEvent', () async { + var chromeSafariBrowser = MyChromeSafariBrowser(); + expect(chromeSafariBrowser.isOpened(), false); + + await chromeSafariBrowser.open(url: TEST_URL_1); + await expectLater(chromeSafariBrowser.opened.future, completes); + expect(chromeSafariBrowser.isOpened(), true); + await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); + expect(await chromeSafariBrowser.navigationEvent.future, isNotNull); + await chromeSafariBrowser.close(); + await expectLater(chromeSafariBrowser.closed.future, completes); expect(chromeSafariBrowser.isOpened(), false); }); }, skip: shouldSkip); diff --git a/example/integration_test/chrome_safari_browser/main.dart b/example/integration_test/chrome_safari_browser/main.dart index a3cd7c44d..9acbe7418 100644 --- a/example/integration_test/chrome_safari_browser/main.dart +++ b/example/integration_test/chrome_safari_browser/main.dart @@ -6,6 +6,7 @@ import 'custom_menu_item.dart'; import 'custom_tabs.dart'; import 'open_and_close.dart'; import 'trusted_web_activity.dart'; +import 'sf_safari_view_controller.dart'; void main() { final shouldSkip = @@ -17,5 +18,6 @@ void main() { customActionButton(); customTabs(); trustedWebActivity(); + sfSafariViewController(); }, skip: shouldSkip); } diff --git a/example/integration_test/chrome_safari_browser/open_and_close.dart b/example/integration_test/chrome_safari_browser/open_and_close.dart index 7ebdb5dd8..02d349760 100644 --- a/example/integration_test/chrome_safari_browser/open_and_close.dart +++ b/example/integration_test/chrome_safari_browser/open_and_close.dart @@ -15,8 +15,35 @@ void openAndClose() { var chromeSafariBrowser = MyChromeSafariBrowser(); expect(chromeSafariBrowser.isOpened(), false); - await chromeSafariBrowser.open(url: TEST_URL_1); - await chromeSafariBrowser.browserCreated.future; + await chromeSafariBrowser.open( + url: TEST_URL_1, + settings: ChromeSafariBrowserSettings( + shareState: CustomTabsShareState.SHARE_STATE_OFF, + startAnimations: [ + AndroidResource( + name: "slide_in_left", + defType: "anim", + defPackage: "android"), + AndroidResource( + name: "slide_out_right", + defType: "anim", + defPackage: "android") + ], + exitAnimations: [ + AndroidResource( + name: "abc_slide_in_top", + defType: "anim", + defPackage: + "com.pichillilorenzo.flutter_inappwebviewexample"), + AndroidResource( + name: "abc_slide_out_top", + defType: "anim", + defPackage: "com.pichillilorenzo.flutter_inappwebviewexample") + ], + keepAliveEnabled: true, + dismissButtonStyle: DismissButtonStyle.CLOSE, + presentationStyle: ModalPresentationStyle.OVER_FULL_SCREEN)); + await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); @@ -24,7 +51,7 @@ void openAndClose() { await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); await chromeSafariBrowser.close(); - await chromeSafariBrowser.browserClosed.future; + await chromeSafariBrowser.closed.future; expect(chromeSafariBrowser.isOpened(), false); }, skip: shouldSkip); } diff --git a/example/integration_test/chrome_safari_browser/sf_safari_view_controller.dart b/example/integration_test/chrome_safari_browser/sf_safari_view_controller.dart new file mode 100644 index 000000000..fc8ff9153 --- /dev/null +++ b/example/integration_test/chrome_safari_browser/sf_safari_view_controller.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../constants.dart'; +import '../util.dart'; + +void sfSafariViewController() { + final shouldSkip = kIsWeb + ? true + : ![ + TargetPlatform.android, + ].contains(defaultTargetPlatform); + + group('SF Safari View Controller', () { + test('onCompletedInitialLoad did load successfully', () async { + var chromeSafariBrowser = MyChromeSafariBrowser(); + expect(chromeSafariBrowser.isOpened(), false); + + await chromeSafariBrowser.open(url: TEST_URL_1); + await expectLater(chromeSafariBrowser.opened.future, completes); + expect(chromeSafariBrowser.isOpened(), true); + expect(() async { + await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); + }, throwsA(isInstanceOf())); + + expect(await chromeSafariBrowser.firstPageLoaded.future, true); + await chromeSafariBrowser.close(); + await expectLater(chromeSafariBrowser.closed.future, completes); + expect(chromeSafariBrowser.isOpened(), false); + }); + + test('clearWebsiteData', () async { + await expectLater(ChromeSafariBrowser.clearWebsiteData(), completes); + }); + + test('create and invalidate Prewarming Token', () async { + final prewarmingToken = await ChromeSafariBrowser.prewarmConnections([TEST_URL_1]); + expect(prewarmingToken, isNotNull); + await expectLater(ChromeSafariBrowser.invalidatePrewarmingToken(prewarmingToken!), completes); + }); + }, 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 99b23b278..09cc6e085 100644 --- a/example/integration_test/chrome_safari_browser/trusted_web_activity.dart +++ b/example/integration_test/chrome_safari_browser/trusted_web_activity.dart @@ -20,7 +20,7 @@ void trustedWebActivity() { await chromeSafariBrowser.open( url: TEST_URL_1, settings: ChromeSafariBrowserSettings(isTrustedWebActivity: true)); - await chromeSafariBrowser.browserCreated.future; + await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); @@ -28,7 +28,7 @@ void trustedWebActivity() { await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); await chromeSafariBrowser.close(); - await chromeSafariBrowser.browserClosed.future; + await chromeSafariBrowser.closed.future; expect(chromeSafariBrowser.isOpened(), false); }); @@ -40,7 +40,7 @@ void trustedWebActivity() { url: TEST_URL_1, settings: ChromeSafariBrowserSettings( isTrustedWebActivity: true, isSingleInstance: true)); - await chromeSafariBrowser.browserCreated.future; + await chromeSafariBrowser.opened.future; expect(chromeSafariBrowser.isOpened(), true); expect(() async { await chromeSafariBrowser.open(url: TEST_CROSS_PLATFORM_URL_1); @@ -48,7 +48,25 @@ void trustedWebActivity() { await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); await chromeSafariBrowser.close(); - await chromeSafariBrowser.browserClosed.future; + await chromeSafariBrowser.closed.future; + expect(chromeSafariBrowser.isOpened(), false); + }); + + test('validate relationship', () async { + var chromeSafariBrowser = MyChromeSafariBrowser(); + expect(chromeSafariBrowser.isOpened(), false); + + await chromeSafariBrowser.open( + settings: ChromeSafariBrowserSettings(isTrustedWebActivity: true)); + await chromeSafariBrowser.serviceConnected.future; + expect(await chromeSafariBrowser.validateRelationship(relation: CustomTabsRelationType.USE_AS_ORIGIN, origin: TEST_CROSS_PLATFORM_URL_1), true); + expect(await chromeSafariBrowser.relationshipValidationResult.future, true); + await chromeSafariBrowser.launchUrl(url: TEST_CROSS_PLATFORM_URL_1); + await chromeSafariBrowser.opened.future; + expect(chromeSafariBrowser.isOpened(), true); + await expectLater(chromeSafariBrowser.firstPageLoaded.future, completes); + await chromeSafariBrowser.close(); + await chromeSafariBrowser.closed.future; expect(chromeSafariBrowser.isOpened(), false); }); }, skip: shouldSkip); diff --git a/example/integration_test/util.dart b/example/integration_test/util.dart index 530cadf20..5188d75f3 100644 --- a/example/integration_test/util.dart +++ b/example/integration_test/util.dart @@ -54,22 +54,44 @@ class MyInAppBrowser extends InAppBrowser { } class MyChromeSafariBrowser extends ChromeSafariBrowser { - final Completer browserCreated = Completer(); - final Completer firstPageLoaded = Completer(); - final Completer browserClosed = Completer(); + final Completer serviceConnected = Completer(); + final Completer opened = Completer(); + final Completer firstPageLoaded = Completer(); + final Completer closed = Completer(); + final Completer navigationEvent = Completer(); + final Completer relationshipValidationResult = Completer(); + + @override + void onServiceConnected() { + serviceConnected.complete(); + } @override void onOpened() { - browserCreated.complete(); + opened.complete(); + } + + @override + void onCompletedInitialLoad(didLoadSuccessfully) { + firstPageLoaded.complete(didLoadSuccessfully); } @override - void onCompletedInitialLoad() { - firstPageLoaded.complete(); + void onNavigationEvent(CustomTabsNavigationEventType? type) { + if (!navigationEvent.isCompleted) { + navigationEvent.complete(type); + } + } + + + @override + void onRelationshipValidationResult( + CustomTabsRelationType? relation, Uri? requestedOrigin, bool result) { + relationshipValidationResult.complete(result); } @override void onClosed() { - browserClosed.complete(); + closed.complete(); } } diff --git a/ios/Classes/SafariViewController/SafariBrowserSettings.swift b/ios/Classes/SafariViewController/SafariBrowserSettings.swift index dd9d1f8a8..3ff9c07c1 100755 --- a/ios/Classes/SafariViewController/SafariBrowserSettings.swift +++ b/ios/Classes/SafariViewController/SafariBrowserSettings.swift @@ -17,7 +17,7 @@ public class SafariBrowserSettings: ISettings { var preferredBarTintColor: String? var preferredControlTintColor: String? var presentationStyle = 0 //fullscreen - var transitionStyle = 0 //crossDissolve + var transitionStyle = 0 //coverVertical override init(){ super.init() diff --git a/lib/src/android/webview_feature.dart b/lib/src/android/webview_feature.dart index 64510f7c1..1b6e30e20 100644 --- a/lib/src/android/webview_feature.dart +++ b/lib/src/android/webview_feature.dart @@ -217,6 +217,11 @@ class WebViewFeature_ { const WebViewFeature_._internal( "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY"); + ///This feature covers [InAppWebViewController.getVariationsHeader]. + static const GET_VARIATIONS_HEADER = + const WebViewFeature_._internal( + "GET_VARIATIONS_HEADER"); + ///Return whether a feature is supported at run-time. On devices running Android version `Build.VERSION_CODES.LOLLIPOP` and higher, ///this will check whether a feature is supported, depending on the combination of the desired feature, the Android version of device, ///and the WebView APK on the device. If running on a device with a lower API level, this will always return `false`. diff --git a/lib/src/android/webview_feature.g.dart b/lib/src/android/webview_feature.g.dart index 77964d4fd..0ca4c03ec 100644 --- a/lib/src/android/webview_feature.g.dart +++ b/lib/src/android/webview_feature.g.dart @@ -217,6 +217,10 @@ class WebViewFeature { WebViewFeature._internal('ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY', 'ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY'); + ///This feature covers [InAppWebViewController.getVariationsHeader]. + static const GET_VARIATIONS_HEADER = WebViewFeature._internal( + 'GET_VARIATIONS_HEADER', 'GET_VARIATIONS_HEADER'); + ///Set of all values of [WebViewFeature]. static final Set values = [ WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL, @@ -266,6 +270,7 @@ class WebViewFeature { WebViewFeature.ALGORITHMIC_DARKENING, WebViewFeature.REQUESTED_WITH_HEADER_CONTROL, WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, + WebViewFeature.GET_VARIATIONS_HEADER, ].toSet(); ///Gets a possible [WebViewFeature] instance from [String] value. diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/lib/src/chrome_safari_browser/chrome_safari_browser.dart index 4ee837c04..cea4f6340 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -143,7 +143,9 @@ class ChromeSafariBrowser { /// ///[url] - The [url] to load. On iOS, the [url] is required and must use the `http` or `https` scheme. /// - ///[headers] - extra request headers. Supported only on Android. + ///[headers] (Supported only on Android) - [whitelisted](https://fetch.spec.whatwg.org/#cors-safelisted-request-header) cross-origin request headers. + ///It is possible to attach non-whitelisted headers to cross-origin requests, when the server and client are related using a + ///[digital asset link](https://developers.google.com/digital-asset-links/v1/getting-started). /// ///[otherLikelyURLs] - Other likely destinations, sorted in decreasing likelihood order. Supported only on Android. /// @@ -199,7 +201,9 @@ class ChromeSafariBrowser { /// ///[url] - initial url. /// - ///[headers] - extra request headers. + ///[headers] (Supported only on Android) - [whitelisted](https://fetch.spec.whatwg.org/#cors-safelisted-request-header) cross-origin request headers. + ///It is possible to attach non-whitelisted headers to cross-origin requests, when the server and client are related using a + ///[digital asset link](https://developers.google.com/digital-asset-links/v1/getting-started). /// ///[otherLikelyURLs] - Other likely destinations, sorted in decreasing likelihood order. /// @@ -225,22 +229,18 @@ class ChromeSafariBrowser { /// ///[url] - Most likely URL, may be null if otherLikelyBundles is provided. /// - ///[headers] - extra request headers. - /// ///[otherLikelyURLs] - Other likely destinations, sorted in decreasing likelihood order. /// ///**Supported Platforms/Implementations**: ///- Android ([Official API - CustomTabsSession.mayLaunchUrl](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#mayLaunchUrl(android.net.Uri,android.os.Bundle,java.util.List%3Candroid.os.Bundle%3E))) - Future mayLaunchUrl( + Future mayLaunchUrl( {Uri? url, - Map? headers, List? otherLikelyURLs}) async { Map args = {}; args.putIfAbsent('url', () => url?.toString()); - args.putIfAbsent('headers', () => headers); args.putIfAbsent('otherLikelyURLs', () => otherLikelyURLs?.map((e) => e.toString()).toList()); - await _channel.invokeMethod("mayLaunchUrl", args); + return await _channel.invokeMethod("mayLaunchUrl", args); } ///Requests to validate a relationship between the application and an origin. @@ -251,14 +251,20 @@ class ChromeSafariBrowser { ///If this method returns `true`, the validation result will be provided through [onRelationshipValidationResult]. ///Otherwise the request didn't succeed. /// + ///[relation] – Relation to check, must be one of the [CustomTabsRelationType] constants. + /// + ///[origin] – Origin. + /// + ///[extras] – Reserved for future use. + /// ///**Supported Platforms/Implementations**: ///- Android ([Official API - CustomTabsSession.validateRelationship](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsSession#validateRelationship(int,android.net.Uri,android.os.Bundle))) - Future validateRelationship( + Future validateRelationship( {required CustomTabsRelationType relation, required Uri origin}) async { Map args = {}; args.putIfAbsent('relation', () => relation.toNativeValue()); args.putIfAbsent('origin', () => origin.toString()); - await _channel.invokeMethod("validateRelationship", args); + return await _channel.invokeMethod("validateRelationship", args); } ///Closes the [ChromeSafariBrowser] instance. diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser_settings.dart b/lib/src/chrome_safari_browser/chrome_safari_browser_settings.dart index 3794f7100..0127849a2 100755 --- a/lib/src/chrome_safari_browser/chrome_safari_browser_settings.dart +++ b/lib/src/chrome_safari_browser/chrome_safari_browser_settings.dart @@ -1,11 +1,35 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; +import '../types/android_resource.dart'; +import '../types/custom_tabs_share_state.dart'; +import '../types/dismiss_button_style.dart'; +import '../types/main.dart'; +import '../types/modal_presentation_style.dart'; +import '../types/modal_transition_style.dart'; +import '../types/trusted_web_activity_display_mode.dart'; +import '../types/trusted_web_activity_screen_orientation.dart'; import '../util.dart'; import 'android/chrome_custom_tabs_options.dart'; import 'apple/safari_options.dart'; -import '../types/main.dart'; + +part 'chrome_safari_browser_settings.g.dart'; + +TrustedWebActivityDisplayMode? _deserializeDisplayMode( + Map? displayMode) { + if (displayMode == null) { + return null; + } + switch (displayMode["type"]) { + case "IMMERSIVE_MODE": + return TrustedWebActivityImmersiveDisplayMode.fromMap(displayMode); + case "DEFAULT_MODE": + default: + return TrustedWebActivityDefaultDisplayMode(); + } +} class ChromeSafariBrowserOptions { Map toMap() { @@ -31,14 +55,15 @@ class ChromeSafariBrowserOptions { } ///Class that represents the settings that can be used for an [ChromeSafariBrowser] window. -class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { +@ExchangeableObject(copyMethod: true) +class ChromeSafariBrowserSettings_ implements ChromeSafariBrowserOptions { ///The share state that should be applied to the custom tab. The default value is [CustomTabsShareState.SHARE_STATE_DEFAULT]. /// ///**NOTE**: Not available in a Trusted Web Activity. /// ///**Supported Platforms/Implementations**: ///- Android - CustomTabsShareState shareState; + CustomTabsShareState_? shareState; ///Set to `false` if the title shouldn't be shown in the custom tab. The default value is `true`. /// @@ -46,7 +71,7 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- Android - bool showTitle; + bool? showTitle; ///Set the custom background color of the toolbar. /// @@ -54,13 +79,31 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { ///- Android Color? toolbarBackgroundColor; + ///Sets the navigation bar color. Has no effect on Android API versions below L. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Color? navigationBarColor; + + ///Sets the navigation bar divider color. Has no effect on Android API versions below P. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Color? navigationBarDividerColor; + + ///Sets the color of the secondary toolbar. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Color? secondaryToolbarColor; + ///Set to `true` to enable the url bar to hide as the user scrolls down on the page. The default value is `false`. /// ///**NOTE**: Not available in a Trusted Web Activity. /// ///**Supported Platforms/Implementations**: ///- Android - bool enableUrlBarHiding; + bool? enableUrlBarHiding; ///Set to `true` to enable Instant Apps. The default value is `false`. /// @@ -68,7 +111,7 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- Android - bool instantAppsEnabled; + bool? instantAppsEnabled; ///Set an explicit application package name that limits ///the components this Intent will resolve to. If left to the default @@ -84,25 +127,25 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- Android - bool keepAliveEnabled; + bool? keepAliveEnabled; ///Set to `true` to launch the Android activity in `singleInstance` mode. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android - bool isSingleInstance; + bool? isSingleInstance; ///Set to `true` to launch the Android intent with the flag `FLAG_ACTIVITY_NO_HISTORY`. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android - bool noHistory; + bool? noHistory; ///Set to `true` to launch the Custom Tab as a Trusted Web Activity. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- Android - bool isTrustedWebActivity; + bool? isTrustedWebActivity; ///Sets a list of additional trusted origins that the user may navigate or be redirected to from the starting uri. /// @@ -110,7 +153,7 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- Android - List additionalTrustedOrigins; + List? additionalTrustedOrigins; ///Sets a display mode of a Trusted Web Activity. /// @@ -118,7 +161,8 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- Android - TrustedWebActivityDisplayMode? displayMode; + @ExchangeableObjectProperty(deserializer: _deserializeDisplayMode) + TrustedWebActivityDisplayMode_? displayMode; ///Sets a screen orientation. This can be used e.g. to enable the locking of an orientation lock type. /// @@ -126,19 +170,35 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- Android - TrustedWebActivityScreenOrientation screenOrientation; + TrustedWebActivityScreenOrientation_? screenOrientation; + + ///Sets the start animations. + ///It must contain 2 [AndroidResource], where the first one represents the "enter" animation for the browser + ///and the second one represents the "exit" animation for the application. + /// + ///**Supported Platforms/Implementations**: + ///- Android + List? startAnimations; + + ///Sets the exit animations. + ///It must contain 2 [AndroidResource], where the first one represents the "enter" animation for the application + ///and the second one represents the "exit" animation for the browser. + /// + ///**Supported Platforms/Implementations**: + ///- Android + List? exitAnimations; ///Set to `true` if Reader mode should be entered automatically when it is available for the webpage. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool entersReaderIfAvailable; + bool? entersReaderIfAvailable; ///Set to `true` to enable bar collapsing. The default value is `false`. /// ///**Supported Platforms/Implementations**: ///- iOS - bool barCollapsingEnabled; + bool? barCollapsingEnabled; ///Set the custom style for the dismiss button. The default value is [DismissButtonStyle.DONE]. /// @@ -146,7 +206,7 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- iOS - DismissButtonStyle dismissButtonStyle; + DismissButtonStyle_? dismissButtonStyle; ///Set the custom background color of the navigation bar and the toolbar. /// @@ -168,18 +228,22 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { /// ///**Supported Platforms/Implementations**: ///- iOS - ModalPresentationStyle presentationStyle; + ModalPresentationStyle_? presentationStyle; ///Set to the custom transition style when presenting the WebView. The default value is [ModalTransitionStyle.COVER_VERTICAL]. /// ///**Supported Platforms/Implementations**: ///- iOS - ModalTransitionStyle transitionStyle; + ModalTransitionStyle_? transitionStyle; - ChromeSafariBrowserSettings( - {this.shareState = CustomTabsShareState.SHARE_STATE_DEFAULT, + @ExchangeableObjectConstructor() + ChromeSafariBrowserSettings_( + {this.shareState = CustomTabsShareState_.SHARE_STATE_DEFAULT, this.showTitle = true, this.toolbarBackgroundColor, + this.navigationBarColor, + this.navigationBarDividerColor, + this.secondaryToolbarColor, this.enableUrlBarHiding = false, this.instantAppsEnabled = false, this.packageName, @@ -189,99 +253,42 @@ class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { this.isTrustedWebActivity = false, this.additionalTrustedOrigins = const [], this.displayMode, - this.screenOrientation = TrustedWebActivityScreenOrientation.DEFAULT, + this.screenOrientation = TrustedWebActivityScreenOrientation_.DEFAULT, + this.startAnimations, + this.exitAnimations, this.entersReaderIfAvailable = false, this.barCollapsingEnabled = false, - this.dismissButtonStyle = DismissButtonStyle.DONE, + this.dismissButtonStyle = DismissButtonStyle_.DONE, this.preferredBarTintColor, this.preferredControlTintColor, - this.presentationStyle = ModalPresentationStyle.FULL_SCREEN, - this.transitionStyle = ModalTransitionStyle.COVER_VERTICAL}); - - @override - Map toMap() { - return { - "shareState": shareState.toNativeValue(), - "showTitle": showTitle, - "toolbarBackgroundColor": toolbarBackgroundColor?.toHex(), - "enableUrlBarHiding": enableUrlBarHiding, - "instantAppsEnabled": instantAppsEnabled, - "packageName": packageName, - "keepAliveEnabled": keepAliveEnabled, - "isSingleInstance": isSingleInstance, - "noHistory": noHistory, - "isTrustedWebActivity": isTrustedWebActivity, - "additionalTrustedOrigins": additionalTrustedOrigins, - "displayMode": displayMode?.toMap(), - "screenOrientation": screenOrientation.toNativeValue(), - "entersReaderIfAvailable": entersReaderIfAvailable, - "barCollapsingEnabled": barCollapsingEnabled, - "dismissButtonStyle": dismissButtonStyle.toNativeValue(), - "preferredBarTintColor": preferredBarTintColor?.toHex(), - "preferredControlTintColor": preferredControlTintColor?.toHex(), - "presentationStyle": presentationStyle.toNativeValue(), - "transitionStyle": transitionStyle.toNativeValue() - }; - } - - static ChromeSafariBrowserSettings fromMap(Map map) { - ChromeSafariBrowserSettings settings = new ChromeSafariBrowserSettings(); - if (defaultTargetPlatform == TargetPlatform.android) { - settings.shareState = map["shareState"]; - settings.showTitle = map["showTitle"]; - settings.toolbarBackgroundColor = - UtilColor.fromHex(map["toolbarBackgroundColor"]); - settings.enableUrlBarHiding = map["enableUrlBarHiding"]; - settings.instantAppsEnabled = map["instantAppsEnabled"]; - settings.packageName = map["packageName"]; - settings.keepAliveEnabled = map["keepAliveEnabled"]; - settings.isSingleInstance = map["isSingleInstance"]; - settings.noHistory = map["noHistory"]; - settings.isTrustedWebActivity = map["isTrustedWebActivity"]; - settings.additionalTrustedOrigins = map["additionalTrustedOrigins"]; - switch (map["displayMode"]["type"]) { - case "IMMERSIVE_MODE": - settings.displayMode = TrustedWebActivityImmersiveDisplayMode.fromMap( - map["displayMode"]); - break; - case "DEFAULT_MODE": - default: - settings.displayMode = TrustedWebActivityDefaultDisplayMode(); - break; - } - settings.screenOrientation = map["screenOrientation"]; + this.presentationStyle = ModalPresentationStyle_.FULL_SCREEN, + this.transitionStyle = ModalTransitionStyle_.COVER_VERTICAL}) { + if (startAnimations != null) { + assert(startAnimations!.length == 2, + "start animations must be have 2 android resources"); } - if (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS) { - settings.entersReaderIfAvailable = map["entersReaderIfAvailable"]; - settings.barCollapsingEnabled = map["barCollapsingEnabled"]; - settings.dismissButtonStyle = - DismissButtonStyle.fromNativeValue(map["dismissButtonStyle"])!; - settings.preferredBarTintColor = - UtilColor.fromHex(map["preferredBarTintColor"]); - settings.preferredControlTintColor = - UtilColor.fromHex(map["preferredControlTintColor"]); - settings.presentationStyle = - ModalPresentationStyle.fromNativeValue(map["presentationStyle"])!; - settings.transitionStyle = - ModalTransitionStyle.fromNativeValue(map["transitionStyle"])!; + if (exitAnimations != null) { + assert(exitAnimations!.length == 2, + "exit animations must be have 2 android resources"); } - return settings; } @override - Map toJson() { - return this.toMap(); + @ExchangeableObjectMethod(ignore: true) + ChromeSafariBrowserSettings_ copy() { + throw UnimplementedError(); } @override - String toString() { - return toMap().toString(); + @ExchangeableObjectMethod(ignore: true) + Map toJson() { + throw UnimplementedError(); } @override - ChromeSafariBrowserSettings copy() { - return ChromeSafariBrowserSettings.fromMap(this.toMap()); + @ExchangeableObjectMethod(ignore: true) + Map toMap() { + throw UnimplementedError(); } } diff --git a/lib/src/chrome_safari_browser/chrome_safari_browser_settings.g.dart b/lib/src/chrome_safari_browser/chrome_safari_browser_settings.g.dart new file mode 100644 index 000000000..8db91d414 --- /dev/null +++ b/lib/src/chrome_safari_browser/chrome_safari_browser_settings.g.dart @@ -0,0 +1,330 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chrome_safari_browser_settings.dart'; + +// ************************************************************************** +// ExchangeableObjectGenerator +// ************************************************************************** + +///Class that represents the settings that can be used for an [ChromeSafariBrowser] window. +class ChromeSafariBrowserSettings implements ChromeSafariBrowserOptions { + ///The share state that should be applied to the custom tab. The default value is [CustomTabsShareState.SHARE_STATE_DEFAULT]. + /// + ///**NOTE**: Not available in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + CustomTabsShareState? shareState; + + ///Set to `false` if the title shouldn't be shown in the custom tab. The default value is `true`. + /// + ///**NOTE**: Not available in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + bool? showTitle; + + ///Set the custom background color of the toolbar. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Color? toolbarBackgroundColor; + + ///Sets the navigation bar color. Has no effect on Android API versions below L. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Color? navigationBarColor; + + ///Sets the navigation bar divider color. Has no effect on Android API versions below P. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Color? navigationBarDividerColor; + + ///Sets the color of the secondary toolbar. + /// + ///**Supported Platforms/Implementations**: + ///- Android + Color? secondaryToolbarColor; + + ///Set to `true` to enable the url bar to hide as the user scrolls down on the page. The default value is `false`. + /// + ///**NOTE**: Not available in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + bool? enableUrlBarHiding; + + ///Set to `true` to enable Instant Apps. The default value is `false`. + /// + ///**NOTE**: Not available in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + bool? instantAppsEnabled; + + ///Set an explicit application package name that limits + ///the components this Intent will resolve to. If left to the default + ///value of null, all components in all applications will considered. + ///If non-null, the Intent can only match the components in the given + ///application package. + /// + ///**Supported Platforms/Implementations**: + ///- Android + String? packageName; + + ///Set to `true` to enable Keep Alive. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android + bool? keepAliveEnabled; + + ///Set to `true` to launch the Android activity in `singleInstance` mode. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android + bool? isSingleInstance; + + ///Set to `true` to launch the Android intent with the flag `FLAG_ACTIVITY_NO_HISTORY`. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android + bool? noHistory; + + ///Set to `true` to launch the Custom Tab as a Trusted Web Activity. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- Android + bool? isTrustedWebActivity; + + ///Sets a list of additional trusted origins that the user may navigate or be redirected to from the starting uri. + /// + ///**NOTE**: Available only in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + List? additionalTrustedOrigins; + + ///Sets a display mode of a Trusted Web Activity. + /// + ///**NOTE**: Available only in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + TrustedWebActivityDisplayMode? displayMode; + + ///Sets a screen orientation. This can be used e.g. to enable the locking of an orientation lock type. + /// + ///**NOTE**: Available only in a Trusted Web Activity. + /// + ///**Supported Platforms/Implementations**: + ///- Android + TrustedWebActivityScreenOrientation? screenOrientation; + + ///Sets the start animations. + ///It must contain 2 [AndroidResource], where the first one represents the "enter" animation for the browser + ///and the second one represents the "exit" animation for the application. + /// + ///**Supported Platforms/Implementations**: + ///- Android + List? startAnimations; + + ///Sets the exit animations. + ///It must contain 2 [AndroidResource], where the first one represents the "enter" animation for the application + ///and the second one represents the "exit" animation for the browser. + /// + ///**Supported Platforms/Implementations**: + ///- Android + List? exitAnimations; + + ///Set to `true` if Reader mode should be entered automatically when it is available for the webpage. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool? entersReaderIfAvailable; + + ///Set to `true` to enable bar collapsing. The default value is `false`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool? barCollapsingEnabled; + + ///Set the custom style for the dismiss button. The default value is [DismissButtonStyle.DONE]. + /// + ///**NOTE**: available on iOS 11.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + DismissButtonStyle? dismissButtonStyle; + + ///Set the custom background color of the navigation bar and the toolbar. + /// + ///**NOTE**: available on iOS 10.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Color? preferredBarTintColor; + + ///Set the custom color of the control buttons on the navigation bar and the toolbar. + /// + ///**NOTE**: available on iOS 10.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + Color? preferredControlTintColor; + + ///Set the custom modal presentation style when presenting the WebView. The default value is [ModalPresentationStyle.FULL_SCREEN]. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ModalPresentationStyle? presentationStyle; + + ///Set to the custom transition style when presenting the WebView. The default value is [ModalTransitionStyle.COVER_VERTICAL]. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + ModalTransitionStyle? transitionStyle; + ChromeSafariBrowserSettings( + {this.shareState = CustomTabsShareState.SHARE_STATE_DEFAULT, + this.showTitle = true, + this.toolbarBackgroundColor, + this.navigationBarColor, + this.navigationBarDividerColor, + this.secondaryToolbarColor, + this.enableUrlBarHiding = false, + this.instantAppsEnabled = false, + this.packageName, + this.keepAliveEnabled = false, + this.isSingleInstance = false, + this.noHistory = false, + this.isTrustedWebActivity = false, + this.additionalTrustedOrigins = const [], + this.displayMode, + this.screenOrientation = TrustedWebActivityScreenOrientation.DEFAULT, + this.startAnimations, + this.exitAnimations, + this.entersReaderIfAvailable = false, + this.barCollapsingEnabled = false, + this.dismissButtonStyle = DismissButtonStyle.DONE, + this.preferredBarTintColor, + this.preferredControlTintColor, + this.presentationStyle = ModalPresentationStyle.FULL_SCREEN, + this.transitionStyle = ModalTransitionStyle.COVER_VERTICAL}) { + if (startAnimations != null) { + assert(startAnimations!.length == 2, + "start animations must be have 2 android resources"); + } + if (exitAnimations != null) { + assert(exitAnimations!.length == 2, + "exit animations must be have 2 android resources"); + } + } + + ///Gets a possible [ChromeSafariBrowserSettings] instance from a [Map] value. + static ChromeSafariBrowserSettings? fromMap(Map? map) { + if (map == null) { + return null; + } + final instance = ChromeSafariBrowserSettings( + toolbarBackgroundColor: map['toolbarBackgroundColor'] != null + ? UtilColor.fromStringRepresentation(map['toolbarBackgroundColor']) + : null, + navigationBarColor: map['navigationBarColor'] != null + ? UtilColor.fromStringRepresentation(map['navigationBarColor']) + : null, + navigationBarDividerColor: map['navigationBarDividerColor'] != null + ? UtilColor.fromStringRepresentation(map['navigationBarDividerColor']) + : null, + secondaryToolbarColor: map['secondaryToolbarColor'] != null + ? UtilColor.fromStringRepresentation(map['secondaryToolbarColor']) + : null, + packageName: map['packageName'], + displayMode: _deserializeDisplayMode(map['displayMode']), + startAnimations: map['startAnimations'] != null + ? List.from(map['startAnimations'] + .map((e) => AndroidResource.fromMap(e?.cast())!)) + : null, + exitAnimations: map['exitAnimations'] != null + ? List.from(map['exitAnimations'] + .map((e) => AndroidResource.fromMap(e?.cast())!)) + : null, + preferredBarTintColor: map['preferredBarTintColor'] != null + ? UtilColor.fromStringRepresentation(map['preferredBarTintColor']) + : null, + preferredControlTintColor: map['preferredControlTintColor'] != null + ? UtilColor.fromStringRepresentation(map['preferredControlTintColor']) + : null, + ); + instance.shareState = + CustomTabsShareState.fromNativeValue(map['shareState']); + instance.showTitle = map['showTitle']; + instance.enableUrlBarHiding = map['enableUrlBarHiding']; + instance.instantAppsEnabled = map['instantAppsEnabled']; + instance.keepAliveEnabled = map['keepAliveEnabled']; + instance.isSingleInstance = map['isSingleInstance']; + instance.noHistory = map['noHistory']; + instance.isTrustedWebActivity = map['isTrustedWebActivity']; + instance.additionalTrustedOrigins = + map['additionalTrustedOrigins']?.cast(); + instance.screenOrientation = + TrustedWebActivityScreenOrientation.fromNativeValue( + map['screenOrientation']); + instance.entersReaderIfAvailable = map['entersReaderIfAvailable']; + instance.barCollapsingEnabled = map['barCollapsingEnabled']; + instance.dismissButtonStyle = + DismissButtonStyle.fromNativeValue(map['dismissButtonStyle']); + instance.presentationStyle = + ModalPresentationStyle.fromNativeValue(map['presentationStyle']); + instance.transitionStyle = + ModalTransitionStyle.fromNativeValue(map['transitionStyle']); + return instance; + } + + ///Converts instance to a map. + Map toMap() { + return { + "shareState": shareState?.toNativeValue(), + "showTitle": showTitle, + "toolbarBackgroundColor": toolbarBackgroundColor?.toHex(), + "navigationBarColor": navigationBarColor?.toHex(), + "navigationBarDividerColor": navigationBarDividerColor?.toHex(), + "secondaryToolbarColor": secondaryToolbarColor?.toHex(), + "enableUrlBarHiding": enableUrlBarHiding, + "instantAppsEnabled": instantAppsEnabled, + "packageName": packageName, + "keepAliveEnabled": keepAliveEnabled, + "isSingleInstance": isSingleInstance, + "noHistory": noHistory, + "isTrustedWebActivity": isTrustedWebActivity, + "additionalTrustedOrigins": additionalTrustedOrigins, + "displayMode": displayMode?.toMap(), + "screenOrientation": screenOrientation?.toNativeValue(), + "startAnimations": startAnimations?.map((e) => e.toMap()).toList(), + "exitAnimations": exitAnimations?.map((e) => e.toMap()).toList(), + "entersReaderIfAvailable": entersReaderIfAvailable, + "barCollapsingEnabled": barCollapsingEnabled, + "dismissButtonStyle": dismissButtonStyle?.toNativeValue(), + "preferredBarTintColor": preferredBarTintColor?.toHex(), + "preferredControlTintColor": preferredControlTintColor?.toHex(), + "presentationStyle": presentationStyle?.toNativeValue(), + "transitionStyle": transitionStyle?.toNativeValue(), + }; + } + + ///Converts instance to a map. + Map toJson() { + return toMap(); + } + + ///Returns a copy of ChromeSafariBrowserSettings. + ChromeSafariBrowserSettings copy() { + return ChromeSafariBrowserSettings.fromMap(toMap()) ?? + ChromeSafariBrowserSettings(); + } + + @override + String toString() { + return 'ChromeSafariBrowserSettings{shareState: $shareState, showTitle: $showTitle, toolbarBackgroundColor: $toolbarBackgroundColor, navigationBarColor: $navigationBarColor, navigationBarDividerColor: $navigationBarDividerColor, secondaryToolbarColor: $secondaryToolbarColor, enableUrlBarHiding: $enableUrlBarHiding, instantAppsEnabled: $instantAppsEnabled, packageName: $packageName, keepAliveEnabled: $keepAliveEnabled, isSingleInstance: $isSingleInstance, noHistory: $noHistory, isTrustedWebActivity: $isTrustedWebActivity, additionalTrustedOrigins: $additionalTrustedOrigins, displayMode: $displayMode, screenOrientation: $screenOrientation, startAnimations: $startAnimations, exitAnimations: $exitAnimations, entersReaderIfAvailable: $entersReaderIfAvailable, barCollapsingEnabled: $barCollapsingEnabled, dismissButtonStyle: $dismissButtonStyle, preferredBarTintColor: $preferredBarTintColor, preferredControlTintColor: $preferredControlTintColor, presentationStyle: $presentationStyle, transitionStyle: $transitionStyle}'; + } +} diff --git a/lib/src/chrome_safari_browser/main.dart b/lib/src/chrome_safari_browser/main.dart index c11753f10..9d23040d2 100644 --- a/lib/src/chrome_safari_browser/main.dart +++ b/lib/src/chrome_safari_browser/main.dart @@ -1,4 +1,8 @@ export 'chrome_safari_browser.dart'; -export 'chrome_safari_browser_settings.dart'; +export 'chrome_safari_browser_settings.dart' + show + ChromeSafariBrowserOptions, + ChromeSafariBrowserSettings, + ChromeSafariBrowserClassOptions; export 'android/main.dart'; export 'apple/main.dart'; 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 7cff21397..4bd7e244f 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -3683,6 +3683,24 @@ class InAppWebViewController { 'setWebContentsDebuggingEnabled', args); } + ///Gets the WebView variations encoded to be used as the X-Client-Data HTTP header. + /// + ///The app is responsible for adding the X-Client-Data header to any request + ///that may use variations metadata, such as requests to Google web properties. + ///The returned string will be a base64 encoded ClientVariations proto: + ///https://source.chromium.org/chromium/chromium/src/+/main:components/variations/proto/client_variations.proto + /// + ///The string may be empty if the header is not available. + /// + ///**NOTE for Android native WebView**: This method should only be called if [WebViewFeature.isFeatureSupported] returns `true` for [WebViewFeature.GET_VARIATIONS_HEADER]. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView ([Official API - WebViewCompat.getVariationsHeader](https://developer.android.com/reference/androidx/webkit/WebViewCompat#getVariationsHeader())) + static Future getVariationsHeader() async { + Map args = {}; + return await _staticChannel.invokeMethod('getVariationsHeader', args); + } + ///Returns a Boolean value that indicates whether WebKit natively supports resources with the specified URL scheme. /// ///[urlScheme] represents the URL scheme associated with the resource. diff --git a/lib/src/types/android_resource.dart b/lib/src/types/android_resource.dart new file mode 100644 index 000000000..27907ad10 --- /dev/null +++ b/lib/src/types/android_resource.dart @@ -0,0 +1,33 @@ +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; + +part 'android_resource.g.dart'; + +///Class that represents an android resource. +@ExchangeableObject() +class AndroidResource_ { + ///Android resource name. + /// + ///A list of available `android.R.anim` can be found + ///[here](https://developer.android.com/reference/android/R.anim). + /// + ///A list of available `androidx.appcompat.R.anim` can be found + ///[here](https://android.googlesource.com/platform/frameworks/support/+/HEAD/appcompat/appcompat/src/main/res/anim/) + ///(abc_*.xml files). + ///In this case, [defPackage] must match your App Android package name. + String name; + + ///Optional default resource type to find, if "type/" is not included in the name. + ///Can be `null` to require an explicit type. + /// + ///Example: "anim" + String? defType; + + ///Optional default package to find, if "package:" is not included in the name. + ///Can be `null` to require an explicit package. + /// + ///Example: "android" if you want use resources from `android.R.` + String? defPackage; + + AndroidResource_({required this.name, + this.defType, this.defPackage}); +} \ No newline at end of file diff --git a/lib/src/types/android_resource.g.dart b/lib/src/types/android_resource.g.dart new file mode 100644 index 000000000..6c48d8ffe --- /dev/null +++ b/lib/src/types/android_resource.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'android_resource.dart'; + +// ************************************************************************** +// ExchangeableObjectGenerator +// ************************************************************************** + +///Class that represents an android resource. +class AndroidResource { + ///Android resource name. + /// + ///A list of available `android.R.anim` can be found + ///[here](https://developer.android.com/reference/android/R.anim). + /// + ///A list of available `androidx.appcompat.R.anim` can be found + ///[here](https://android.googlesource.com/platform/frameworks/support/+/HEAD/appcompat/appcompat/src/main/res/anim/) + ///(abc_*.xml files). + ///In this case, [defPackage] must match your App Android package name. + String name; + + ///Optional default resource type to find, if "type/" is not included in the name. + ///Can be `null` to require an explicit type. + /// + ///Example: "anim" + String? defType; + + ///Optional default package to find, if "package:" is not included in the name. + ///Can be `null` to require an explicit package. + /// + ///Example: "android" if you want use resources from `android.R.` + String? defPackage; + AndroidResource({required this.name, this.defType, this.defPackage}); + + ///Gets a possible [AndroidResource] instance from a [Map] value. + static AndroidResource? fromMap(Map? map) { + if (map == null) { + return null; + } + final instance = AndroidResource( + name: map['name'], + defType: map['defType'], + defPackage: map['defPackage'], + ); + return instance; + } + + ///Converts instance to a map. + Map toMap() { + return { + "name": name, + "defType": defType, + "defPackage": defPackage, + }; + } + + ///Converts instance to a map. + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'AndroidResource{name: $name, defType: $defType, defPackage: $defPackage}'; + } +} diff --git a/lib/src/types/main.dart b/lib/src/types/main.dart index 29e25979a..72f077a4a 100644 --- a/lib/src/types/main.dart +++ b/lib/src/types/main.dart @@ -217,4 +217,5 @@ export 'window_style_mask.dart' show WindowStyleMask; export 'window_titlebar_separator_style.dart' show WindowTitlebarSeparatorStyle; export 'custom_tabs_navigation_event_type.dart' show CustomTabsNavigationEventType; export 'custom_tabs_relation_type.dart' show CustomTabsRelationType; -export 'prewarming_token.dart' show PrewarmingToken; \ No newline at end of file +export 'prewarming_token.dart' show PrewarmingToken; +export 'android_resource.dart' show AndroidResource; \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh index e38cca118..dcbb98c76 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -34,13 +34,17 @@ dart $PROJECT_DIR/tool/env.dart cd $PROJECT_DIR/test_node_server node index.js & +# Only for Android +# Open Chrome on the development device, navigate to chrome://flags, search for an item called Enable command line on non-rooted devices and change it to ENABLED and then restart the browser. +adb shell "echo '_ --disable-digital-asset-link-verification-for-url=\"https://flutter.dev\"' > /data/local/tmp/chrome-command-line" || true + flutter --version flutter clean flutter pub get cd $PROJECT_DIR/example flutter clean if [ ! -z "$2" ] && [ $PLATFORM = "web" ]; then - flutter driver --driver=test_driver/integration_test.dart --target=integration_test/webview_flutter_test.dart --device-id=chrome + flutter driver --driver=test_driver/integration_test.dart --target=integration_test/webview_flutter_test.dart --device-id=chrome else flutter driver --driver=test_driver/integration_test.dart --target=integration_test/webview_flutter_test.dart fi diff --git a/test_node_server/index.js b/test_node_server/index.js index 820dfa4be..aeb07c7d9 100755 --- a/test_node_server/index.js +++ b/test_node_server/index.js @@ -153,6 +153,19 @@ app.get("/", (req, res) => { res.end() }) +app.get("/echo-headers", (req, res) => { + res.send(` + + + + +
${JSON.stringify(req.headers)}
+ + + `); + res.end() +}) + app.get('/test-index', (req, res) => { res.sendFile(__dirname + '/public/index.html'); }) From cfd70fda6eb0a5bde51fca4fd3b0fe9b0c0eadb0 Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Wed, 26 Oct 2022 17:52:35 +0200 Subject: [PATCH 3/3] release 6.0.0-beta.9 --- CHANGELOG.md | 11 +- .../InAppWebViewFileProvider.java | 2 +- .../ActionBroadcastReceiver.java | 22 +- .../ChromeCustomTabsActivity.java | 64 +++++- .../ChromeCustomTabsChannelDelegate.java | 22 +- .../ChromeCustomTabsSettings.java | 5 + .../ChromeSafariBrowserManager.java | 14 +- .../CustomTabActivityHelper.java | 42 ++-- .../TrustedWebActivity.java | 31 ++- .../types/AndroidResource.java | 6 +- .../types/CustomTabsSecondaryToolbar.java | 83 +++++++ .../InAppWebViewChromeClient.java | 10 +- .../app/src/main/res/layout/remote_view.xml | 21 ++ .../app/src/main/res/layout/remote_view_2.xml | 14 ++ .../custom_action_button.dart | 44 ---- .../custom_menu_item.dart | 5 +- .../chrome_safari_browser/custom_tabs.dart | 109 +++++++++ .../chrome_safari_browser/main.dart | 2 - .../chrome_safari_browser/open_and_close.dart | 29 +-- .../sf_safari_view_controller.dart | 18 +- .../trusted_web_activity.dart | 9 +- example/integration_test/util.dart | 6 +- .../ios/Flutter/flutter_export_environment.sh | 5 +- example/ios/Runner.xcodeproj/project.pbxproj | 184 ++++++++++++++- example/ios/test/ActionViewController.swift | 58 +++++ .../test/Base.lproj/MainInterface.storyboard | 53 +++++ example/ios/test/Info.plist | 28 +++ example/ios/test/Media.xcassets/Contents.json | 6 + .../TouchBarBezel.colorset/Contents.json | 14 ++ .../chrome_safari_browser_example.screen.dart | 24 +- .../ChromeSafariBrowserManager.swift | 3 - .../SafariBrowserSettings.swift | 9 + .../SafariViewController.swift | 35 ++- ios/Classes/Types/ActivityButton.swift | 24 ++ ios/Classes/Types/UIEventAttribution.swift | 28 +++ ios/Classes/Types/UIImage.swift | 27 +++ lib/src/android/webview_feature.dart | 3 +- .../chrome_safari_browser.dart | 209 ++++++++++++++++-- .../chrome_safari_browser_settings.dart | 34 ++- .../chrome_safari_browser_settings.g.dart | 42 +++- lib/src/in_app_webview/in_app_webview.dart | 19 -- lib/src/types/activity_button.dart | 24 ++ lib/src/types/activity_button.g.dart | 54 +++++ lib/src/types/android_resource.dart | 19 +- lib/src/types/android_resource.g.dart | 13 ++ lib/src/types/main.dart | 8 +- lib/src/types/ui_event_attribution.dart | 35 +++ lib/src/types/ui_event_attribution.g.dart | 70 ++++++ lib/src/types/ui_image.dart | 28 +++ lib/src/types/ui_image.g.dart | 59 +++++ 50 files changed, 1485 insertions(+), 199 deletions(-) create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/CustomTabsSecondaryToolbar.java create mode 100644 example/android/app/src/main/res/layout/remote_view.xml create mode 100644 example/android/app/src/main/res/layout/remote_view_2.xml delete mode 100644 example/integration_test/chrome_safari_browser/custom_action_button.dart create mode 100644 example/ios/test/ActionViewController.swift create mode 100644 example/ios/test/Base.lproj/MainInterface.storyboard create mode 100644 example/ios/test/Info.plist create mode 100644 example/ios/test/Media.xcassets/Contents.json create mode 100644 example/ios/test/Media.xcassets/TouchBarBezel.colorset/Contents.json create mode 100644 ios/Classes/Types/ActivityButton.swift create mode 100644 ios/Classes/Types/UIEventAttribution.swift create mode 100644 ios/Classes/Types/UIImage.swift create mode 100644 lib/src/types/activity_button.dart create mode 100644 lib/src/types/activity_button.g.dart create mode 100644 lib/src/types/ui_event_attribution.dart create mode 100644 lib/src/types/ui_event_attribution.g.dart create mode 100644 lib/src/types/ui_image.dart create mode 100644 lib/src/types/ui_image.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e6418d3..9c586758e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,20 @@ ## 6.0.0-beta.9 -- Added `headers`, `otherLikelyURLs` arguments on `ChromeSafariBrowser.open` method for Android +- Added `headers`, `otherLikelyURLs`, `referrer` arguments on `ChromeSafariBrowser.open` method for Android - Added `onNavigationEvent`, `onServiceConnected`, `onRelationshipValidationResult` events on `ChromeSafariBrowser` for Android -- Added `mayLaunchUrl`, `launchUrl`, `updateActionButton`, `validateRelationship` methods on `ChromeSafariBrowser` for Android -- Added `startAnimations`, `exitAnimations`, `navigationBarColor`, `navigationBarDividerColor`, `secondaryToolbarColor` ChromeSafariBrowser settings for Android +- Added `mayLaunchUrl`, `launchUrl`, `updateActionButton`, `validateRelationship`, `setSecondaryToolbar`, `updateSecondaryToolbar` methods on `ChromeSafariBrowser` for Android +- Added `startAnimations`, `exitAnimations`, `navigationBarColor`, `navigationBarDividerColor`, `secondaryToolbarColor`, `alwaysUseBrowserUI` ChromeSafariBrowser settings for Android +- Added `ChromeSafariBrowserMenuItem.image` property for iOS - Added `didLoadSuccessfully` optional argument on `ChromeSafariBrowser.onCompletedInitialLoad` event for iOS - Added `onInitialLoadDidRedirect`, `onWillOpenInBrowser` events on `ChromeSafariBrowser` for iOS -- Added `clearWebsiteData`, `prewarmConnections`, `invalidatePrewarmingToken` static methods on `ChromeSafariBrowser` for iOS +- Added `activityButton`, `eventAttribution` ChromeSafariBrowser settings for iOS +- Added `clearWebsiteData`, `prewarmConnections`, `invalidatePrewarmingToken`, `getMaxToolbarItems` static methods on `ChromeSafariBrowser` for iOS - Added `getVariationsHeader` WebView static method ### BREAKING CHANGES - `ChromeSafariBrowser.onCompletedInitialLoad` event has an optional argument +- `ChromeSafariBrowserMenuItem.action` and `ChromeSafariBrowserActionButton.action` can be null - All `ChromeSafariBrowserSettings` properties are optionals ## 6.0.0-beta.8 diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFileProvider.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFileProvider.java index b94a2cac2..7322b8b55 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFileProvider.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFileProvider.java @@ -4,6 +4,6 @@ public class InAppWebViewFileProvider extends FileProvider { - // This class intentionally left blank. + public static final String fileProviderAuthorityExtension = "flutter_inappwebview.fileprovider"; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java index e58ead517..1b6ea8595 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ActionBroadcastReceiver.java @@ -4,6 +4,9 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.util.Log; + +import androidx.browser.customtabs.CustomTabsIntent; public class ActionBroadcastReceiver extends BroadcastReceiver { protected static final String LOG_TAG = "ActionBroadcastReceiver"; @@ -13,16 +16,25 @@ public class ActionBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { + int clickedId = intent.getIntExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_CLICKED_ID, -1); String url = intent.getDataString(); if (url != null) { Bundle b = intent.getExtras(); String viewId = b.getString(KEY_ACTION_VIEW_ID); - int id = b.getInt(KEY_ACTION_ID); - String title = b.getString(KEY_URL_TITLE); - ChromeCustomTabsActivity browser = ChromeSafariBrowserManager.browsers.get(viewId); - if (browser != null && browser.channelDelegate != null) { - browser.channelDelegate.onItemActionPerform(id, url, title); + if (clickedId == -1) { + int id = b.getInt(KEY_ACTION_ID); + String title = b.getString(KEY_URL_TITLE); + + ChromeCustomTabsActivity browser = ChromeSafariBrowserManager.browsers.get(viewId); + if (browser != null && browser.channelDelegate != null) { + browser.channelDelegate.onItemActionPerform(id, url, title); + } + } else { + ChromeCustomTabsActivity browser = ChromeSafariBrowserManager.browsers.get(viewId); + if (browser != null && browser.channelDelegate != null) { + browser.channelDelegate.onSecondaryItemActionPerform(browser.getResources().getResourceName(clickedId), url); + } } } } 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 0216c3a55..7842a67b3 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,7 +9,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.util.Log; +import android.widget.RemoteViews; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; @@ -24,6 +24,7 @@ import com.pichillilorenzo.flutter_inappwebview.types.AndroidResource; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsActionButton; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsMenuItem; +import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsSecondaryToolbar; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; import java.util.ArrayList; @@ -55,10 +56,14 @@ public class ChromeCustomTabsActivity extends Activity implements Disposable { public List initialOtherLikelyURLs; @Nullable public Map initialHeaders; + @Nullable + public String initialReferrer; public List menuItems = new ArrayList<>(); @Nullable public CustomTabsActionButton actionButton; @Nullable + public CustomTabsSecondaryToolbar secondaryToolbar; + @Nullable public ChromeCustomTabsChannelDelegate channelDelegate; @CallSuper @@ -84,11 +89,13 @@ protected void onCreate(Bundle savedInstanceState) { initialUrl = b.getString("url"); initialHeaders = (Map) b.getSerializable("headers"); + initialReferrer = b.getString("referrer"); initialOtherLikelyURLs = b.getStringArrayList("otherLikelyURLs"); customSettings = new ChromeCustomTabsSettings(); customSettings.parse((HashMap) b.getSerializable("settings")); actionButton = CustomTabsActionButton.fromMap((Map) b.getSerializable("actionButton")); + secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map) b.getSerializable("secondaryToolbar")); List> menuItemList = (List>) b.getSerializable("menuItemList"); for (Map menuItem : menuItemList) { menuItems.add(CustomTabsMenuItem.fromMap(menuItem)); @@ -155,8 +162,9 @@ public void onRelationshipValidationResult(@CustomTabsService.Relation int relat } public void launchUrl(@NonNull String url, - @Nullable Map headers, - @Nullable List otherLikelyURLs) { + @Nullable Map headers, + @Nullable String referrer, + @Nullable List otherLikelyURLs) { mayLaunchUrl(url, otherLikelyURLs); builder = new CustomTabsIntent.Builder(customTabsSession); prepareCustomTabs(); @@ -164,7 +172,8 @@ public void launchUrl(@NonNull String url, CustomTabsIntent customTabsIntent = builder.build(); prepareCustomTabsIntent(customTabsIntent); - CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), headers, CHROME_CUSTOM_TAB_REQUEST_CODE); + CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), headers, + referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE); } public boolean mayLaunchUrl(@Nullable String url, @Nullable List otherLikelyURLs) { @@ -183,7 +192,7 @@ public boolean mayLaunchUrl(@Nullable String url, @Nullable List otherLi public void customTabsConnected() { customTabsSession = customTabActivityHelper.getSession(); if (initialUrl != null) { - launchUrl(initialUrl, initialHeaders, initialOtherLikelyURLs); + launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs); } } @@ -244,6 +253,33 @@ private void prepareCustomTabs() { createPendingIntent(actionButton.getId()), actionButton.isShouldTint()); } + + if (secondaryToolbar != null) { + AndroidResource layout = secondaryToolbar.getLayout(); + RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this)); + int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()]; + for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) { + AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i); + clickableIDs[i] = clickableID.getIdentifier(this); + } + builder.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent()); + } + } + + public PendingIntent getSecondaryToolbarOnClickPendingIntent() { + Intent broadcastIntent = new Intent(this, ActionBroadcastReceiver.class); + + Bundle extras = new Bundle(); + extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id); + broadcastIntent.putExtras(extras); + + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return PendingIntent.getBroadcast( + this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + } else { + return PendingIntent.getBroadcast( + this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } } private void prepareCustomTabsIntent(CustomTabsIntent customTabsIntent) { @@ -254,6 +290,9 @@ private void prepareCustomTabsIntent(CustomTabsIntent customTabsIntent) { if (customSettings.keepAliveEnabled) CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent); + + if (customSettings.alwaysUseBrowserUI) + CustomTabsIntent.setAlwaysUseBrowserUI(customTabsIntent.intent); } public void updateActionButton(@NonNull byte[] icon, @NonNull String description) { @@ -270,6 +309,21 @@ public void updateActionButton(@NonNull byte[] icon, @NonNull String description actionButton.setDescription(description); } + public void updateSecondaryToolbar(CustomTabsSecondaryToolbar secondaryToolbar) { + if (customTabsSession == null) { + return; + } + AndroidResource layout = secondaryToolbar.getLayout(); + RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this)); + int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()]; + for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) { + AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i); + clickableIDs[i] = clickableID.getIdentifier(this); + } + customTabsSession.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent()); + this.secondaryToolbar = secondaryToolbar; + } + @Override protected void onStart() { super.onStart(); 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 1af27d3ba..137624a8a 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 @@ -9,6 +9,7 @@ import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsActionButton; +import com.pichillilorenzo.flutter_inappwebview.types.CustomTabsSecondaryToolbar; import java.util.ArrayList; import java.util.HashMap; @@ -35,8 +36,9 @@ public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodCh String url = (String) call.argument("url"); if (url != null) { Map headers = (Map) call.argument("headers"); + String referrer = (String) call.argument("referrer"); List otherLikelyURLs = (List) call.argument("otherLikelyURLs"); - chromeCustomTabsActivity.launchUrl(url, headers, otherLikelyURLs); + chromeCustomTabsActivity.launchUrl(url, headers, referrer, otherLikelyURLs); result.success(true); } else { result.success(false); @@ -73,6 +75,15 @@ public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodCh result.success(false); } break; + case "updateSecondaryToolbar": + if (chromeCustomTabsActivity != null) { + CustomTabsSecondaryToolbar secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map) call.argument("secondaryToolbar")); + chromeCustomTabsActivity.updateSecondaryToolbar(secondaryToolbar); + result.success(true); + } else { + result.success(false); + } + break; case "close": if (chromeCustomTabsActivity != null) { chromeCustomTabsActivity.onStop(); @@ -145,6 +156,15 @@ public void onItemActionPerform(int id, String url, String title) { channel.invokeMethod("onItemActionPerform", obj); } + public void onSecondaryItemActionPerform(String name, String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("name", name); + obj.put("url", url); + channel.invokeMethod("onSecondaryItemActionPerform", obj); + } + public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin, boolean result) { MethodChannel channel = getChannel(); if (channel == null) return; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java index 10f3e707e..8be604a0b 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsSettings.java @@ -44,6 +44,7 @@ public class ChromeCustomTabsSettings implements ISettings startAnimations = new ArrayList<>(); public List exitAnimations = new ArrayList<>(); + public Boolean alwaysUseBrowserUI = false; @NonNull @Override @@ -138,6 +139,9 @@ public ChromeCustomTabsSettings parse(@NonNull Map options) { } } break; + case "alwaysUseBrowserUI": + alwaysUseBrowserUI = (Boolean) value; + break; } } @@ -163,6 +167,7 @@ public Map toMap() { options.put("isTrustedWebActivity", isTrustedWebActivity); options.put("additionalTrustedOrigins", additionalTrustedOrigins); options.put("screenOrientation", screenOrientation); + options.put("alwaysUseBrowserUI", alwaysUseBrowserUI); return options; } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java index ccf1197c2..fa1d168f0 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeSafariBrowserManager.java @@ -5,6 +5,7 @@ import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabsIntent; import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin; import com.pichillilorenzo.flutter_inappwebview.Util; @@ -48,11 +49,13 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result resul if (plugin != null && plugin.activity != null) { String url = (String) call.argument("url"); HashMap headers = (HashMap) call.argument("headers"); + String referrer = (String) call.argument("referrer"); ArrayList otherLikelyURLs = (ArrayList) call.argument("otherLikelyURLs"); HashMap settings = (HashMap) call.argument("settings"); HashMap actionButton = (HashMap) call.argument("actionButton"); + HashMap secondaryToolbar = (HashMap) call.argument("secondaryToolbar"); List> menuItemList = (List>) call.argument("menuItemList"); - open(plugin.activity, viewId, url, headers, otherLikelyURLs, settings, actionButton, menuItemList, result); + open(plugin.activity, viewId, url, headers, referrer, otherLikelyURLs, settings, actionButton, secondaryToolbar, menuItemList, result); } else { result.success(false); } @@ -64,26 +67,31 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result resul result.success(false); } break; + case "getMaxToolbarItems": + result.success(CustomTabsIntent.getMaxToolbarItems()); + break; default: result.notImplemented(); } } public void open(Activity activity, String viewId, @Nullable String url, @Nullable HashMap headers, - @Nullable ArrayList otherLikelyURLs, + @Nullable String referrer, @Nullable ArrayList otherLikelyURLs, HashMap settings, HashMap actionButton, + HashMap secondaryToolbar, List> menuItemList, MethodChannel.Result result) { Intent intent = null; Bundle extras = new Bundle(); extras.putString("url", url); - extras.putBoolean("isData", false); extras.putString("id", viewId); extras.putString("managerId", this.id); extras.putSerializable("headers", headers); + extras.putString("referrer", referrer); extras.putSerializable("otherLikelyURLs", otherLikelyURLs); extras.putSerializable("settings", settings); extras.putSerializable("actionButton", (Serializable) actionButton); + extras.putSerializable("secondaryToolbar", (Serializable) secondaryToolbar); extras.putSerializable("menuItemList", (Serializable) menuItemList); Boolean isSingleInstance = Util.getOrDefault(settings, "isSingleInstance", false); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java index 57e78f077..10d930eb3 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/CustomTabActivityHelper.java @@ -1,7 +1,7 @@ package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; import android.app.Activity; -import android.content.pm.PackageManager; +import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.provider.Browser; @@ -28,42 +28,50 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback { private CustomTabsCallback mCustomTabsCallback; /** - * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView. + * Opens the URL on a Custom Tab if possible. * * @param activity The host activity. - * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available. + * @param intent a intent to be used if Custom Tabs is available. * @param uri the Uri to be opened. */ public static void openCustomTab(Activity activity, - CustomTabsIntent customTabsIntent, + Intent intent, Uri uri, @Nullable Map headers, + @Nullable Uri referrer, int requestCode) { - customTabsIntent.intent.setData(uri); + intent.setData(uri); if (headers != null) { Bundle bundleHeaders = new Bundle(); for (Map.Entry header : headers.entrySet()) { bundleHeaders.putString(header.getKey(), header.getValue()); } - customTabsIntent.intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders); + intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders); + } + if (referrer != null) { + intent.putExtra(Intent.EXTRA_REFERRER, referrer); } - activity.startActivityForResult(customTabsIntent.intent, requestCode); + activity.startActivityForResult(intent, requestCode); } public static void openCustomTab(Activity activity, - TrustedWebActivityIntent trustedWebActivityIntent, + CustomTabsIntent customTabsIntent, Uri uri, @Nullable Map headers, + @Nullable Uri referrer, int requestCode) { - trustedWebActivityIntent.getIntent().setData(uri); - if (headers != null) { - Bundle bundleHeaders = new Bundle(); - for (Map.Entry header : headers.entrySet()) { - bundleHeaders.putString(header.getKey(), header.getValue()); - } - trustedWebActivityIntent.getIntent().putExtra(Browser.EXTRA_HEADERS, bundleHeaders); - } - activity.startActivityForResult(trustedWebActivityIntent.getIntent(), requestCode); + CustomTabActivityHelper.openCustomTab(activity, customTabsIntent.intent, uri, + headers, referrer, requestCode); + } + + public static void openTrustedWebActivity(Activity activity, + TrustedWebActivityIntent trustedWebActivityIntent, + Uri uri, + @Nullable Map headers, + @Nullable Uri referrer, + int requestCode) { + CustomTabActivityHelper.openCustomTab(activity, trustedWebActivityIntent.getIntent(), uri, + headers, referrer, requestCode); } 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 b86578a07..aee48e5b3 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 @@ -3,18 +3,14 @@ import android.content.Intent; import android.graphics.Color; import android.net.Uri; -import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; -import androidx.browser.customtabs.CustomTabsService; import androidx.browser.trusted.TrustedWebActivityIntent; import androidx.browser.trusted.TrustedWebActivityIntentBuilder; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,6 +23,7 @@ public class TrustedWebActivity extends ChromeCustomTabsActivity { @Override public void launchUrl(@NonNull String url, @Nullable Map headers, + @Nullable String referrer, @Nullable List otherLikelyURLs) { if (customTabsSession == null) { return; @@ -40,24 +37,33 @@ public void launchUrl(@NonNull String url, TrustedWebActivityIntent trustedWebActivityIntent = builder.build(customTabsSession); prepareCustomTabsIntent(trustedWebActivityIntent); - CustomTabActivityHelper.openCustomTab(this, trustedWebActivityIntent, uri, headers, CHROME_CUSTOM_TAB_REQUEST_CODE); + CustomTabActivityHelper.openTrustedWebActivity(this, trustedWebActivityIntent, uri, headers, + referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE); } @Override public void customTabsConnected() { customTabsSession = customTabActivityHelper.getSession(); if (initialUrl != null) { - launchUrl(initialUrl, initialHeaders, initialOtherLikelyURLs); + launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs); } } private void prepareCustomTabs() { + CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) { - CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); - builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder - .setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor)) - .build()); + defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor)); } + if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor)); + } + if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) { + defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor)); + } + if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) { + defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor)); + } + builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build()); if (customSettings.additionalTrustedOrigins != null && !customSettings.additionalTrustedOrigins.isEmpty()) { builder.setAdditionalTrustedOrigins(customSettings.additionalTrustedOrigins); @@ -66,7 +72,7 @@ private void prepareCustomTabs() { if (customSettings.displayMode != null) { builder.setDisplayMode(customSettings.displayMode); } - + builder.setScreenOrientation(customSettings.screenOrientation); } @@ -79,5 +85,8 @@ private void prepareCustomTabsIntent(TrustedWebActivityIntent trustedWebActivity if (customSettings.keepAliveEnabled) CustomTabsHelper.addKeepAliveExtra(this, intent); + + if (customSettings.alwaysUseBrowserUI) + CustomTabsIntent.setAlwaysUseBrowserUI(intent); } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java index ba1453594..86353deef 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/AndroidResource.java @@ -10,11 +10,11 @@ public class AndroidResource { @NonNull - String name; + private String name; @Nullable - String defType; + private String defType; @Nullable - String defPackage; + private String defPackage; public AndroidResource(@NonNull String name, @Nullable String defType, @Nullable String defPackage) { this.name = name; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/CustomTabsSecondaryToolbar.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/CustomTabsSecondaryToolbar.java new file mode 100644 index 000000000..6a21267d7 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/CustomTabsSecondaryToolbar.java @@ -0,0 +1,83 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CustomTabsSecondaryToolbar { + @NonNull + private AndroidResource layout; + @NonNull + private List clickableIDs = new ArrayList<>(); + + public CustomTabsSecondaryToolbar(@NonNull AndroidResource layout, @NonNull List clickableIDs) { + this.layout = layout; + this.clickableIDs = clickableIDs; + } + + @Nullable + public static CustomTabsSecondaryToolbar fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + AndroidResource layout = AndroidResource.fromMap((Map) map.get("layout")); + List clickableIDs = new ArrayList<>(); + List> clickableIDList = (List>) map.get("clickableIDs"); + if (clickableIDList != null) { + for (Map clickableIDMap : clickableIDList) { + AndroidResource clickableID = AndroidResource.fromMap((Map) clickableIDMap.get("id")); + if (clickableID != null) { + clickableIDs.add(clickableID); + } + } + } + return new CustomTabsSecondaryToolbar(layout, clickableIDs); + } + + @NonNull + public AndroidResource getLayout() { + return layout; + } + + public void setLayout(@NonNull AndroidResource layout) { + this.layout = layout; + } + + @NonNull + public List getClickableIDs() { + return clickableIDs; + } + + public void setClickableIDs(@NonNull List clickableIDs) { + this.clickableIDs = clickableIDs; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomTabsSecondaryToolbar that = (CustomTabsSecondaryToolbar) o; + + if (!layout.equals(that.layout)) return false; + return clickableIDs.equals(that.clickableIDs); + } + + @Override + public int hashCode() { + int result = layout.hashCode(); + result = 31 * result + clickableIDs.hashCode(); + return result; + } + + @Override + public String toString() { + return "CustomTabsSecondaryToolbar{" + + "layout=" + layout + + ", clickableIDs=" + clickableIDs + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebViewChromeClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebViewChromeClient.java index 70ce80c62..95291151e 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebViewChromeClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebViewChromeClient.java @@ -41,6 +41,7 @@ import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; +import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFileProvider; import com.pichillilorenzo.flutter_inappwebview.types.CreateWindowAction; import com.pichillilorenzo.flutter_inappwebview.in_app_browser.ActivityResultListener; import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserDelegate; @@ -73,8 +74,6 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR public static Map windowWebViewMessages = new HashMap<>(); private static int windowAutoincrementId = 0; - private static final String fileProviderAuthorityExtension = "flutter_inappwebview.fileprovider"; - private static final int PICKER = 1; private static final int PICKER_LEGACY = 3; final String DEFAULT_MIME_TYPES = "*/*"; @@ -1148,8 +1147,11 @@ private Uri getOutputUri(String intentType) { return null; } // for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions - String packageName = activity.getApplicationContext().getPackageName(); - return FileProvider.getUriForFile(activity.getApplicationContext(), packageName + "." + fileProviderAuthorityExtension, capturedFile); + String fileProviderAuthority = activity.getApplicationContext().getPackageName() + "." + + InAppWebViewFileProvider.fileProviderAuthorityExtension; + return FileProvider.getUriForFile(activity.getApplicationContext(), + fileProviderAuthority, + capturedFile); } @Nullable diff --git a/example/android/app/src/main/res/layout/remote_view.xml b/example/android/app/src/main/res/layout/remote_view.xml new file mode 100644 index 000000000..ae9402472 --- /dev/null +++ b/example/android/app/src/main/res/layout/remote_view.xml @@ -0,0 +1,21 @@ + + + +