diff --git a/CHANGELOG.md b/CHANGELOG.md index d26f5722..67deeed6 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added `singleInstance` option for Android `ChromeSafariBrowser` implementation - Added `onDownloadStartRequest` event and deprecated old `onDownloadStart` event - Added `shareState` Android option for `ChromeSafariBrowser` class +- Added support for Android TWA (Trusted Web Activity) - Fixed missing `onZoomScaleChanged` call for `InAppBrowser` class - Fixed `requestImageRef` method always `null` on iOS - Fixed "applicationNameForUserAgent is not work in ios" [#525](https://github.com/pichillilorenzo/flutter_inappwebview/issues/525) diff --git a/android/build.gradle b/android/build.gradle index 9cc60378..a6642d58 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -46,8 +46,8 @@ android { } dependencies { implementation 'androidx.webkit:webkit:1.4.0' - implementation 'androidx.browser:browser:1.3.0' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.browser:browser:1.4.0' + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.squareup.okhttp3:okhttp:3.14.9' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 52fb1c85..19c76a1d 100755 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -11,11 +11,20 @@ android:theme="@style/ThemeTransparent" android:exported="true" android:name="com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs.ChromeCustomTabsActivity" /> + + > menuItemList) { + customTabsSession = customTabActivityHelper.getSession(); + Uri uri = Uri.parse(url); + customTabActivityHelper.mayLaunchUrl(uri, null, null); + + builder = new CustomTabsIntent.Builder(customTabsSession); + prepareCustomTabs(menuItemList); + + CustomTabsIntent customTabsIntent = builder.build(); + prepareCustomTabsIntent(customTabsIntent); + + CustomTabActivityHelper.openCustomTab(this, customTabsIntent, uri, CHROME_CUSTOM_TAB_REQUEST_CODE); + } + private void prepareCustomTabs(List> menuItemList) { if (options.addDefaultShareMenuItem != null) { builder.setShareState(options.addDefaultShareMenuItem ? diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsOptions.java index 1ba09751..4d5c076b 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/ChromeCustomTabsOptions.java @@ -4,10 +4,14 @@ import android.content.Intent; import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.trusted.ScreenOrientation; +import androidx.browser.trusted.TrustedWebActivityDisplayMode; import com.pichillilorenzo.flutter_inappwebview.Options; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class ChromeCustomTabsOptions implements Options { @@ -24,8 +28,12 @@ public class ChromeCustomTabsOptions implements Options additionalTrustedOrigins = new ArrayList<>(); + public TrustedWebActivityDisplayMode displayMode = null; + public Integer screenOrientation = ScreenOrientation.DEFAULT; @Override public ChromeCustomTabsOptions parse(Map options) { @@ -61,12 +69,35 @@ public class ChromeCustomTabsOptions implements Options) value; + break; + case "displayMode": + Map displayModeMap = (Map) value; + String displayModeType = (String) displayModeMap.get("type"); + if (displayModeType != null) { + switch (displayModeType) { + case "IMMERSIVE_MODE": + boolean isSticky = (boolean) displayModeMap.get("isSticky"); + int layoutInDisplayCutoutMode = (int) displayModeMap.get("layoutInDisplayCutoutMode"); + displayMode = new TrustedWebActivityDisplayMode.ImmersiveMode(isSticky, layoutInDisplayCutoutMode); + case "DEFAULT_MODE": + displayMode = new TrustedWebActivityDisplayMode.DefaultMode(); + } + } + break; + case "screenOrientation": + screenOrientation = (Integer) value; + break; } } @@ -83,8 +114,11 @@ public class ChromeCustomTabsOptions implements Options> menuItemList) { + customTabsSession = customTabActivityHelper.getSession(); + Uri uri = Uri.parse(url); + customTabActivityHelper.mayLaunchUrl(uri, null, null); + + builder = new TrustedWebActivityIntentBuilder(uri); + prepareCustomTabs(); + + TrustedWebActivityIntent trustedWebActivityIntent = builder.build(customTabsSession); + prepareCustomTabsIntent(trustedWebActivityIntent); + + CustomTabActivityHelper.openCustomTab(this, trustedWebActivityIntent, uri, CHROME_CUSTOM_TAB_REQUEST_CODE); + } + + private void prepareCustomTabs() { + if (options.toolbarBackgroundColor != null && !options.toolbarBackgroundColor.isEmpty()) { + CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder(); + builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder + .setToolbarColor(Color.parseColor(options.toolbarBackgroundColor)) + .build()); + } + + if (options.additionalTrustedOrigins != null && !options.additionalTrustedOrigins.isEmpty()) { + builder.setAdditionalTrustedOrigins(options.additionalTrustedOrigins); + } + + if (options.displayMode != null) { + builder.setDisplayMode(options.displayMode); + } + + builder.setScreenOrientation(options.screenOrientation); + } + + private void prepareCustomTabsIntent(TrustedWebActivityIntent trustedWebActivityIntent) { + Intent intent = trustedWebActivityIntent.getIntent(); + if (options.packageName != null) + intent.setPackage(options.packageName); + else + intent.setPackage(CustomTabsHelper.getPackageNameToUse(this)); + + if (options.keepAliveEnabled) + CustomTabsHelper.addKeepAliveExtra(this, intent); + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/TrustedWebActivitySingleInstance.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/TrustedWebActivitySingleInstance.java new file mode 100755 index 00000000..15017510 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/chrome_custom_tabs/TrustedWebActivitySingleInstance.java @@ -0,0 +1,7 @@ +package com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs; + +public class TrustedWebActivitySingleInstance extends TrustedWebActivity { + + protected static final String LOG_TAG = "TrustedWebActivitySingleInstance"; + +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 8e93da90..1f6b71c0 100755 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,10 @@ android:label="flutter_inappwebview_example" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher"> + + + Flutter Trusted Web Activity + + [{ + \"relation\": [\"delegate_permission/common.handle_all_urls\"], + \"target\": { + \"namespace\": \"web\", + \"site\": \"https://flutter.dev\"} + }] + + \ No newline at end of file diff --git a/example/lib/chrome_safari_browser_example.screen.dart b/example/lib/chrome_safari_browser_example.screen.dart index 08c0486f..c2fb9a3c 100755 --- a/example/lib/chrome_safari_browser_example.screen.dart +++ b/example/lib/chrome_safari_browser_example.screen.dart @@ -67,7 +67,8 @@ class _ChromeSafariBrowserExampleScreenState options: ChromeSafariBrowserClassOptions( android: AndroidChromeCustomTabsOptions( shareState: CustomTabsShareState.SHARE_STATE_OFF, - singleInstance: false, + isSingleInstance: false, + isTrustedWebActivity: false, keepAliveEnabled: true), ios: IOSSafariOptions( dismissButtonStyle: diff --git a/lib/src/chrome_safari_browser/android/chrome_custom_tabs_options.dart b/lib/src/chrome_safari_browser/android/chrome_custom_tabs_options.dart index e46dc572..2496ade2 100755 --- a/lib/src/chrome_safari_browser/android/chrome_custom_tabs_options.dart +++ b/lib/src/chrome_safari_browser/android/chrome_custom_tabs_options.dart @@ -11,23 +11,31 @@ import '../../in_app_webview/android/in_app_webview_options.dart'; ///This class represents all the Android-only [ChromeSafariBrowser] options available. class AndroidChromeCustomTabsOptions implements ChromeSafariBrowserOptions, AndroidOptions { - ///Set to `false` if you don't want the default share item to the menu. The default value is `true`. + ///Use `shareState` instead. @Deprecated('Use `shareState` instead') bool? addDefaultShareMenuItem; ///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. 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. bool showTitle; ///Set the custom background color of the toolbar. Color? toolbarBackgroundColor; ///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. bool enableUrlBarHiding; ///Set to `true` to enable Instant Apps. The default value is `false`. + /// + ///**NOTE**: Not available in a Trusted Web Activity. bool instantAppsEnabled; ///Set an explicit application package name that limits @@ -41,11 +49,29 @@ class AndroidChromeCustomTabsOptions bool keepAliveEnabled; ///Set to `true` to launch the Android activity in `singleInstance` mode. The default value is `false`. - bool singleInstance; + bool isSingleInstance; ///Set to `true` to launch the Android intent with the flag `FLAG_ACTIVITY_NO_HISTORY`. The default value is `false`. bool noHistory; + ///Set to `true` to launch the Custom Tab as a Trusted Web Activity. The default value is `false`. + 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. + List additionalTrustedOrigins; + + ///Sets a display mode of a Trusted Web Activity. + /// + ///**NOTE**: Available only in a Trusted Web Activity. + 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. + TrustedWebActivityScreenOrientation screenOrientation; + AndroidChromeCustomTabsOptions( {@Deprecated('Use `shareState` instead') this.addDefaultShareMenuItem, this.shareState = CustomTabsShareState.SHARE_STATE_DEFAULT, @@ -55,8 +81,12 @@ class AndroidChromeCustomTabsOptions this.instantAppsEnabled = false, this.packageName, this.keepAliveEnabled = false, - this.singleInstance = false, - this.noHistory = false}); + this.isSingleInstance = false, + this.noHistory = false, + this.isTrustedWebActivity = false, + this.additionalTrustedOrigins = const [], + this.displayMode, + this.screenOrientation = TrustedWebActivityScreenOrientation.DEFAULT}); @override Map toMap() { @@ -70,8 +100,12 @@ class AndroidChromeCustomTabsOptions "instantAppsEnabled": instantAppsEnabled, "packageName": packageName, "keepAliveEnabled": keepAliveEnabled, - "singleInstance": singleInstance, - "noHistory": noHistory + "isSingleInstance": isSingleInstance, + "noHistory": noHistory, + "isTrustedWebActivity": isTrustedWebActivity, + "additionalTrustedOrigins": additionalTrustedOrigins, + "displayMode": displayMode?.toMap(), + "screenOrientation": screenOrientation.toValue() }; } @@ -88,8 +122,20 @@ class AndroidChromeCustomTabsOptions options.instantAppsEnabled = map["instantAppsEnabled"]; options.packageName = map["packageName"]; options.keepAliveEnabled = map["keepAliveEnabled"]; - options.singleInstance = map["singleInstance"]; + options.isSingleInstance = map["isSingleInstance"]; options.noHistory = map["noHistory"]; + options.isTrustedWebActivity = map["isTrustedWebActivity"]; + options.additionalTrustedOrigins = map["additionalTrustedOrigins"]; + switch(map["displayMode"]["type"]) { + case "IMMERSIVE_MODE": + options.displayMode = TrustedWebActivityImmersiveDisplayMode.fromMap(map["displayMode"]); + break; + case "DEFAULT_MODE": + default: + options.displayMode = TrustedWebActivityDefaultDisplayMode(); + break; + } + options.screenOrientation = map["screenOrientation"]; return options; } diff --git a/lib/src/types.dart b/lib/src/types.dart index d8ccc7d7..fcd82656 100755 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -7012,6 +7012,264 @@ class CustomTabsShareState { bool operator ==(value) => value == _value; + @override + int get hashCode => _value.hashCode; +} + +///Android-class that represents display mode of a Trusted Web Activity. +abstract class TrustedWebActivityDisplayMode { + Map toMap() { + return {}; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///Android-class that represents the default display mode of a Trusted Web Activity. +///The system UI (status bar, navigation bar) is shown, and the browser toolbar is hidden while the user is on a verified origin. +class TrustedWebActivityDefaultDisplayMode implements TrustedWebActivityDisplayMode { + + String _type = "DEFAULT_MODE"; + + Map toMap() { + return { + "type": _type + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///Android-class that represents the default display mode of a Trusted Web Activity. +///The system UI (status bar, navigation bar) is shown, and the browser toolbar is hidden while the user is on a verified origin. +class TrustedWebActivityImmersiveDisplayMode implements TrustedWebActivityDisplayMode { + ///Whether the Trusted Web Activity should be in sticky immersive mode. + bool isSticky; + + ///The constant defining how to deal with display cutouts. + AndroidLayoutInDisplayCutoutMode layoutInDisplayCutoutMode; + + String _type = "IMMERSIVE_MODE"; + + TrustedWebActivityImmersiveDisplayMode( + {required this.isSticky, + required this.layoutInDisplayCutoutMode}); + + static TrustedWebActivityImmersiveDisplayMode? fromMap(Map? map) { + if (map == null) { + return null; + } + + return TrustedWebActivityImmersiveDisplayMode( + isSticky: map["isSticky"], + layoutInDisplayCutoutMode: map["layoutInDisplayCutoutMode"]); + } + + Map toMap() { + return { + "isSticky": isSticky, + "layoutInDisplayCutoutMode": layoutInDisplayCutoutMode.toValue(), + "type": _type + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///Android-specific class representing the share state that should be applied to the custom tab. +/// +///**NOTE**: available on Android 28+. +class AndroidLayoutInDisplayCutoutMode { + final int _value; + + const AndroidLayoutInDisplayCutoutMode._internal(this._value); + + static final Set values = [ + AndroidLayoutInDisplayCutoutMode.DEFAULT, + AndroidLayoutInDisplayCutoutMode.SHORT_EDGES, + AndroidLayoutInDisplayCutoutMode.NEVER, + AndroidLayoutInDisplayCutoutMode.ALWAYS + ].toSet(); + + static AndroidLayoutInDisplayCutoutMode? fromValue(int? value) { + if (value != null) { + try { + return AndroidLayoutInDisplayCutoutMode.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + int toValue() => _value; + + @override + String toString() { + switch (_value) { + case 1: + return "SHORT_EDGES"; + case 2: + return "NEVER"; + case 3: + return "ALWAYS"; + case 0: + default: + return "DEFAULT"; + } + } + + ///With this default setting, content renders into the cutout area when displayed in portrait mode, but content is letterboxed when displayed in landscape mode. + /// + ///**NOTE**: available on Android 28+. + static const DEFAULT = const AndroidLayoutInDisplayCutoutMode._internal(0); + + ///Content renders into the cutout area in both portrait and landscape modes. + /// + ///**NOTE**: available on Android 28+. + static const SHORT_EDGES = const AndroidLayoutInDisplayCutoutMode._internal(1); + + ///Content never renders into the cutout area. + /// + ///**NOTE**: available on Android 28+. + static const NEVER = const AndroidLayoutInDisplayCutoutMode._internal(2); + + ///The window is always allowed to extend into the DisplayCutout areas on the all edges of the screen. + /// + ///**NOTE**: available on Android 30+. + static const ALWAYS = const AndroidLayoutInDisplayCutoutMode._internal(3); + + bool operator ==(value) => value == _value; + + @override + int get hashCode => _value.hashCode; +} + +/// Android-specific class representing Screen Orientation Lock type value of a Trusted Web Activity: +/// https://www.w3.org/TR/screen-orientation/#screenorientation-interface +class TrustedWebActivityScreenOrientation { + final int _value; + + const TrustedWebActivityScreenOrientation._internal(this._value); + + static final Set values = [ + TrustedWebActivityScreenOrientation.DEFAULT, + TrustedWebActivityScreenOrientation.PORTRAIT_PRIMARY, + TrustedWebActivityScreenOrientation.PORTRAIT_SECONDARY, + TrustedWebActivityScreenOrientation.LANDSCAPE_PRIMARY, + TrustedWebActivityScreenOrientation.LANDSCAPE_SECONDARY, + TrustedWebActivityScreenOrientation.ANY, + TrustedWebActivityScreenOrientation.LANDSCAPE, + TrustedWebActivityScreenOrientation.PORTRAIT, + TrustedWebActivityScreenOrientation.NATURAL, + ].toSet(); + + static TrustedWebActivityScreenOrientation? fromValue(int? value) { + if (value != null) { + try { + return TrustedWebActivityScreenOrientation.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + int toValue() => _value; + + @override + String toString() { + switch (_value) { + case 1: + return "PORTRAIT_PRIMARY"; + case 2: + return "PORTRAIT_SECONDARY"; + case 3: + return "LANDSCAPE_PRIMARY"; + case 4: + return "LANDSCAPE_SECONDARY"; + case 5: + return "ANY"; + case 6: + return "LANDSCAPE"; + case 7: + return "PORTRAIT"; + case 8: + return "NATURAL"; + case 0: + default: + return "DEFAULT"; + } + } + + /// The default screen orientation is the set of orientations to which the screen is locked when + /// there is no current orientation lock. + static const DEFAULT = const TrustedWebActivityScreenOrientation._internal(0); + + /// Portrait-primary is an orientation where the screen width is less than or equal to the + /// screen height. If the device's natural orientation is portrait, then it is in + /// portrait-primary when held in that position. + static const PORTRAIT_PRIMARY = const TrustedWebActivityScreenOrientation._internal(1); + + /// Portrait-secondary is an orientation where the screen width is less than or equal to the + /// screen height. If the device's natural orientation is portrait, then it is in + /// portrait-secondary when rotated 180° from its natural position. + static const PORTRAIT_SECONDARY = const TrustedWebActivityScreenOrientation._internal(2); + + /// Landscape-primary is an orientation where the screen width is greater than the screen height. + /// If the device's natural orientation is landscape, then it is in landscape-primary when held + /// in that position. + static const LANDSCAPE_PRIMARY = const TrustedWebActivityScreenOrientation._internal(3); + + /// Landscape-secondary is an orientation where the screen width is greater than the + /// screen height. If the device's natural orientation is landscape, it is in + /// landscape-secondary when rotated 180° from its natural orientation. + static const LANDSCAPE_SECONDARY = const TrustedWebActivityScreenOrientation._internal(4); + + /// Any is an orientation that means the screen can be locked to any one of portrait-primary, + /// portrait-secondary, landscape-primary and landscape-secondary. + static const ANY = const TrustedWebActivityScreenOrientation._internal(5); + + /// Landscape is an orientation where the screen width is greater than the screen height and + /// depending on platform convention locking the screen to landscape can represent + /// landscape-primary, landscape-secondary or both. + static const LANDSCAPE = const TrustedWebActivityScreenOrientation._internal(6); + + /// Portrait is an orientation where the screen width is less than or equal to the screen height + /// and depending on platform convention locking the screen to portrait can represent + /// portrait-primary, portrait-secondary or both. + static const PORTRAIT = const TrustedWebActivityScreenOrientation._internal(7); + + /// Natural is an orientation that refers to either portrait-primary or landscape-primary + /// depending on the device's usual orientation. This orientation is usually provided by + /// the underlying operating system. + static const NATURAL = const TrustedWebActivityScreenOrientation._internal(8); + + bool operator ==(value) => value == _value; + @override int get hashCode => _value.hashCode; } \ No newline at end of file