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