diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index c0b95c7f..2932e50a 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -9,9 +9,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- - name: set up JDK 1.8
+ - name: set up JDK 17
uses: actions/setup-java@v1
with:
- java-version: 1.8
+ java-version: 17
- name: Build with Gradle
run: ./gradlew build
diff --git a/README.md b/README.md
index b580b7db..35668924 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Android Browser Helper
-
+
The Android Browser Helper library helps developers use Custom Tabs and Trusted
Web Activities on top of the AndroidX browser support library.
@@ -19,7 +19,7 @@ Android Browser helper is available on the Google Maven. To use it, modify your
```gradle
dependencies {
//...
- implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.3.0'
+ implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.4.0'
}
```
diff --git a/androidbrowserhelper/build.gradle b/androidbrowserhelper/build.gradle
index ccd86b87..502e5928 100644
--- a/androidbrowserhelper/build.gradle
+++ b/androidbrowserhelper/build.gradle
@@ -17,17 +17,24 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
-def VERSION = "2.4.0";
+// Before 2.6.1, the version code was unused and was kept at 1.
+// This is now being used to report metrics, and should be bumped with
+// every version bump.
+def VERSION = "2.6.1";
+def VERSION_CODE = 1;
android {
- compileSdkVersion 31
+ namespace "com.google.androidbrowserhelper"
defaultConfig {
- minSdkVersion 16
+ minSdkVersion 21
+ compileSdk 36
targetSdkVersion 31
- versionCode 1
+ versionCode VERSION_CODE
versionName VERSION
+ buildConfigField "int", "LIBRARY_VERSION", "${versionCode}"
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -38,9 +45,13 @@ android {
}
}
+ buildFeatures {
+ buildConfig true
+ }
+
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
testOptions {
@@ -51,16 +62,21 @@ android {
}
dependencies {
- api 'androidx.browser:browser:1.4.0'
+ // Force newest versions of kotlin and coroutines to avoid duplicate classes in kotlin-stdlib
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
+ implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
+
+ api 'androidx.browser:browser:1.9.0-alpha04'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.core:core:1.0.2'
+ implementation 'androidx.appcompat:appcompat:1.7.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:3.0.0'
- testImplementation 'org.robolectric:robolectric:4.4'
+ testImplementation 'org.robolectric:robolectric:4.12.2'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
diff --git a/androidbrowserhelper/src/androidTest/AndroidManifest.xml b/androidbrowserhelper/src/androidTest/AndroidManifest.xml
index 2cec8e7d..c58d1574 100644
--- a/androidbrowserhelper/src/androidTest/AndroidManifest.xml
+++ b/androidbrowserhelper/src/androidTest/AndroidManifest.xml
@@ -14,10 +14,22 @@
limitations under the License.
-->
+ xmlns:tools="http://schemas.android.com/tools">
+
+
+
+ android:enabled="false" android:exported="true">
@@ -43,14 +55,14 @@
+ android:enabled="false" android:exported="true">
+ android:enabled="false" android:exported="true">
@@ -60,7 +72,7 @@
+ android:enabled="false" android:exported="true">
@@ -82,9 +94,10 @@
+ android:label="Test Launcher Activity"
+ android:enabled="false"
+ android:exported="true">
diff --git a/androidbrowserhelper/src/androidTest/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategyTest.java b/androidbrowserhelper/src/androidTest/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategyTest.java
index 4f857161..c927412d 100644
--- a/androidbrowserhelper/src/androidTest/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategyTest.java
+++ b/androidbrowserhelper/src/androidTest/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategyTest.java
@@ -100,7 +100,7 @@ public void setUp() {
mActivity = mActivityTestRule.getActivity();
mSession = mConnectionRule.establishSessionBlocking(mActivity);
mStrategy = new PwaWrapperSplashScreenStrategy(mActivity, R.drawable.splash, 0,
- ImageView.ScaleType.FIT_XY, null, 0, FILE_PROVIDER_AUTHORITY);
+ ImageView.ScaleType.FIT_XY, null, 0, FILE_PROVIDER_AUTHORITY, false);
}
@After
@@ -145,7 +145,7 @@ public void setsParametersOnTwaBuilder() throws InterruptedException {
PwaWrapperSplashScreenStrategy strategy = new PwaWrapperSplashScreenStrategy(mActivity,
R.drawable.splash, bgColor, scaleType, matrix, fadeOutDuration,
- FILE_PROVIDER_AUTHORITY);
+ FILE_PROVIDER_AUTHORITY, false);
strategy.onActivityEnterAnimationComplete();
initiateLaunch(strategy);
diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivity.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivity.java
index 3536ba64..8aa9aba7 100644
--- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivity.java
+++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivity.java
@@ -16,9 +16,11 @@
import android.app.Activity;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Bundle;
+import android.os.SystemClock;
import android.util.Log;
import android.widget.ImageView;
@@ -27,6 +29,7 @@
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.trusted.FileHandlingData;
import androidx.browser.trusted.TrustedWebActivityDisplayMode;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import androidx.browser.trusted.TrustedWebActivityService;
@@ -38,6 +41,11 @@
import org.json.JSONException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
/**
* A convenience class to make using Trusted Web Activities easier. You can extend this class for
* basic modifications to the behaviour.
@@ -115,15 +123,16 @@ public class LauncherActivity extends Activity {
@Nullable
private PwaWrapperSplashScreenStrategy mSplashScreenStrategy;
- private CustomTabsCallback mCustomTabsCallback = new QualityEnforcer();
-
@Nullable
private TwaLauncher mTwaLauncher;
+ private long mStartupUptimeMillis;
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mStartupUptimeMillis = SystemClock.uptimeMillis();
sLauncherActivitiesAlive++;
boolean twaAlreadyRunning = sLauncherActivitiesAlive > 1;
boolean intentHasData = getIntent().getData() != null;
@@ -164,7 +173,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
getSplashImageScaleType(),
getSplashImageTransformationMatrix(),
mMetadata.splashScreenFadeOutDurationMillis,
- mMetadata.fileProviderAuthority);
+ mMetadata.fileProviderAuthority,
+ mMetadata.startChromeBeforeAnimationComplete);
}
if (shouldLaunchImmediately()) {
@@ -202,8 +212,9 @@ protected void launchTwa() {
getColorCompat(mMetadata.navigationBarDividerColorDarkId))
.build();
+ Uri launchUrl = getLaunchingUrl();
TrustedWebActivityIntentBuilder twaBuilder =
- new TrustedWebActivityIntentBuilder(getLaunchingUrl())
+ new TrustedWebActivityIntentBuilder(launchUrl)
.setToolbarColor(getColorCompat(mMetadata.statusBarColorId))
.setNavigationBarColor(getColorCompat(mMetadata.navigationBarColorId))
.setNavigationBarDividerColor(
@@ -212,17 +223,25 @@ protected void launchTwa() {
.setColorSchemeParams(
CustomTabsIntent.COLOR_SCHEME_DARK, darkModeColorScheme)
.setDisplayMode(getDisplayMode())
- .setScreenOrientation(mMetadata.screenOrientation);
+ .setScreenOrientation(mMetadata.screenOrientation)
+ .setLaunchHandlerClientMode(mMetadata.launchHandlerClientMode);
+
+ Uri intentUrl = getIntent().getData();
+ if (!launchUrl.equals(intentUrl)) {
+ twaBuilder.setOriginalLaunchUrl(intentUrl);
+ }
if (mMetadata.additionalTrustedOrigins != null) {
twaBuilder.setAdditionalTrustedOrigins(mMetadata.additionalTrustedOrigins);
}
addShareDataIfPresent(twaBuilder);
+ addFileDataIfPresent(twaBuilder);
mTwaLauncher = createTwaLauncher();
+ mTwaLauncher.setStartupUptimeMillis(mStartupUptimeMillis);
mTwaLauncher.launch(twaBuilder,
- mCustomTabsCallback,
+ getCustomTabsCallback(),
mSplashScreenStrategy,
() -> mBrowserWasLaunched = true,
getFallbackStrategy());
@@ -244,8 +263,12 @@ protected void launchTwa() {
mTwaLauncher.getProviderPackage());
}
+ protected CustomTabsCallback getCustomTabsCallback() {
+ return new QualityEnforcer();
+ }
+
protected TwaLauncher createTwaLauncher() {
- return new TwaLauncher(this);
+ return new TwaLauncher(this, getTaskId());
}
private boolean splashScreenNeeded() {
@@ -276,6 +299,31 @@ private void addShareDataIfPresent(TrustedWebActivityIntentBuilder twaBuilder) {
}
}
+ private void addFileDataIfPresent(TrustedWebActivityIntentBuilder twaBuilder) {
+ List uris;
+
+ if (getIntent().hasExtra(TrustedWebActivityIntentBuilder.EXTRA_FILE_HANDLING_DATA)) {
+ Bundle bundle = getIntent().getBundleExtra(TrustedWebActivityIntentBuilder.EXTRA_FILE_HANDLING_DATA);
+ if (bundle == null) return;
+ uris = FileHandlingData.fromBundle(bundle).uris;
+ } else {
+ uris = Arrays.asList(getIntent().getData());
+ }
+
+ for (Uri uri : uris) {
+ if (uri == null || !"content".equals(uri.getScheme())) return;
+
+ int granted = checkCallingOrSelfUriPermission(uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ if (granted != PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "Failed to open a file - no read / write permissions: " + uri);
+ return;
+ }
+ }
+
+ twaBuilder.setFileHandlingData(new FileHandlingData(uris));
+ }
+
/**
* Override to set a custom scale type for the image displayed on a splash screen.
* See {@link ImageView.ScaleType}.
@@ -328,6 +376,23 @@ protected void onSaveInstanceState(Bundle outState) {
outState.putBoolean(BROWSER_WAS_LAUNCHED_KEY, mBrowserWasLaunched);
}
+ /**
+ * Override this to enable Protocol Handler support.
+ * Keys of this map are data schemes, e.g. "bitcoin", "irc", "xmpp", "web+coffee", and values
+ * are templates that will be used to construct the full URL. The template must contain a "%s"
+ * token and be an absolute location with http/https scheme and the same origin as the TWA.
+ *
+ * An example valid entry in the map would be:
+ * ["web+coffee"] -> ["https://coffee.com/?type=%s"]
+ * This would result in a link "web+coffee://latte" being converted to
+ * "https://coffee.com/?type=web%2Bcoffee%3A%2F%2Flatte".
+ *
+ * {@see https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/protocol_handlers}
+ */
+ protected Map getProtocolHandlers() {
+ return Collections.emptyMap();
+ }
+
@Override
public void onEnterAnimationComplete() {
super.onEnterAnimationComplete();
@@ -345,18 +410,42 @@ public void onEnterAnimationComplete() {
* Override this for special handling (such as ignoring or sanitising data from the Intent).
*/
protected Uri getLaunchingUrl() {
- Uri uri = getIntent().getData();
- if (uri != null) {
- Log.d(TAG, "Using URL from Intent (" + uri + ").");
- return uri;
- }
-
- if (mMetadata.defaultUrl != null) {
- Log.d(TAG, "Using URL from Manifest (" + mMetadata.defaultUrl + ").");
- return Uri.parse(mMetadata.defaultUrl);
+ Uri defaultUrl = Uri.parse(mMetadata.defaultUrl);
+
+ Uri intentUrl = getIntent().getData();
+
+ if (intentUrl != null) {
+ Map protocolHandlers = getProtocolHandlers();
+ String scheme = intentUrl.getScheme();
+
+ if ("https".equals(scheme)) {
+ Log.d(TAG, "Using url from Intent: " + intentUrl);
+ return intentUrl;
+ }
+
+ if ("content".equals(scheme)) {
+ // The application was launched by opening a file - return the URL configured for
+ // this file type in the manifest
+ if (mMetadata.fileHandlingActionUrl == null) {
+ return defaultUrl;
+ }
+ return Uri.parse(mMetadata.fileHandlingActionUrl);
+ }
+
+ Uri format = protocolHandlers.get(scheme);
+ if (format != null) {
+ String target = Uri.encode(intentUrl.toString());
+ Uri targetUrl = Uri.parse(String.format(format.toString(), target));
+ Log.d(TAG, "Using protocol handler url: " + targetUrl);
+ return targetUrl;
+ }
+
+ Log.w(TAG, "Scheme " + scheme + " was registered in the manifest but not in " +
+ "getProtocolHandlers()! Ignoring it and falling back to the default url.");
}
- return Uri.parse("https://www.example.com/");
+ Log.d(TAG, "Using url from Manifest: " + defaultUrl);
+ return defaultUrl;
}
/**
diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivityMetadata.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivityMetadata.java
index 04f9ad1e..50a84d05 100644
--- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivityMetadata.java
+++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/LauncherActivityMetadata.java
@@ -14,19 +14,23 @@
package com.google.androidbrowserhelper.trusted;
+import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.browser.trusted.LaunchHandlerClientMode;
import androidx.browser.trusted.ScreenOrientation;
import androidx.browser.trusted.TrustedWebActivityDisplayMode;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
@@ -131,7 +135,7 @@ public class LauncherActivityMetadata {
/**
* The display mode to use when launching the Trusted Web Activity. Possible values are
- * "default", "immersive" and "sticky-immersive".
+ * "default", "immersive", "sticky-immersive", "minimal-ui", and "browser".
*/
private static final String METADATA_DISPLAY_MODE =
"android.support.customtabs.trusted.DISPLAY_MODE";
@@ -145,6 +149,30 @@ public class LauncherActivityMetadata {
private static final String METADATA_SCREEN_ORIENTATION =
"android.support.customtabs.trusted.SCREEN_ORIENTATION";
+ /**
+ * Url to launch in a Trusted Web Activity when handling a file
+ */
+ private static final String METADATA_FILE_HANDLING_ACTION_URL =
+ "android.support.customtabs.trusted.FILE_HANDLING_ACTION_URL";
+
+ /**
+ * Client mode of Launch Handler API. Describes how TWA will be launched. For example opening
+ * a new tasks or taking an action to an existing one.
+ */
+ private static final String LAUNCH_HANDLER_CLIENT_MODE_METADATA_NAME
+ = "android.support.customtabs.trusted.LAUNCH_HANDLER_CLIENT_MODE";
+ private static final Map LAUNCH_HANDLER_CLIENT_MODE_MAP =
+ Map.of(
+ "navigate-existing", LaunchHandlerClientMode.NAVIGATE_EXISTING,
+ "focus-existing", LaunchHandlerClientMode.FOCUS_EXISTING,
+ "navigate-new", LaunchHandlerClientMode.NAVIGATE_NEW,
+ "auto", LaunchHandlerClientMode.AUTO);
+ /**
+ * Whether to start Chrome before the enter animation is complete. Default is false.
+ */
+ private static final String METADATA_START_CHROME_BEFORE_ANIMATION_COMPLETE =
+ "android.support.customtabs.trusted.START_CHROME_BEFORE_ANIMATION_COMPLETE";
+
private final static int DEFAULT_COLOR_ID = android.R.color.white;
private final static int DEFAULT_DIVIDER_COLOR_ID = android.R.color.transparent;
@@ -164,6 +192,9 @@ public class LauncherActivityMetadata {
public final TrustedWebActivityDisplayMode displayMode;
@ScreenOrientation.LockType public final int screenOrientation;
@Nullable public final String shareTarget;
+ @Nullable public final String fileHandlingActionUrl;
+ @LaunchHandlerClientMode.ClientMode public final int launchHandlerClientMode;
+ public final boolean startChromeBeforeAnimationComplete;
private LauncherActivityMetadata(@NonNull Bundle metaData, @NonNull Resources resources) {
defaultUrl = metaData.getString(METADATA_DEFAULT_URL);
@@ -195,6 +226,11 @@ private LauncherActivityMetadata(@NonNull Bundle metaData, @NonNull Resources re
screenOrientation = getOrientation(metaData.getString(METADATA_SCREEN_ORIENTATION));
int shareTargetId = metaData.getInt(METADATA_SHARE_TARGET, 0);
shareTarget = shareTargetId == 0 ? null : resources.getString(shareTargetId);
+ fileHandlingActionUrl = metaData.getString(METADATA_FILE_HANDLING_ACTION_URL);
+ launchHandlerClientMode = getLaunchHandlerClientMode(
+ metaData.getString(LAUNCH_HANDLER_CLIENT_MODE_METADATA_NAME));
+ startChromeBeforeAnimationComplete =
+ metaData.getBoolean(METADATA_START_CHROME_BEFORE_ANIMATION_COMPLETE, false);
}
private @ScreenOrientation.LockType int getOrientation(String orientation) {
@@ -234,23 +270,54 @@ private static TrustedWebActivityDisplayMode getDisplayMode(@NonNull Bundle meta
return new TrustedWebActivityDisplayMode.ImmersiveMode(
true, LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT);
}
+ if ("minimal-ui".equals(displayMode)) {
+ return new TrustedWebActivityDisplayMode.MinimalUiMode();
+ }
+ if ("browser".equals(displayMode)) {
+ return new TrustedWebActivityDisplayMode.BrowserMode();
+ }
return new TrustedWebActivityDisplayMode.DefaultMode();
}
+ /**
+ * Returns a Launch Handler client mode in androidx format. In case it's absent or wrong in the
+ * metadata LaunchHandlerClientMode.AUTO is returned.
+ */
+ private @LaunchHandlerClientMode.ClientMode int getLaunchHandlerClientMode(
+ String clientModeName) {
+ Integer clientMode = LAUNCH_HANDLER_CLIENT_MODE_MAP.get(clientModeName);
+ return clientMode != null ? clientMode : LaunchHandlerClientMode.AUTO;
+ }
+
/**
* Creates LauncherActivityMetadata instance based on metadata of the passed Activity.
*/
public static LauncherActivityMetadata parse(Context context) {
Resources resources = context.getResources();
- Bundle metaData = null;
+ Bundle metaData = new Bundle();
try {
- metaData = context.getPackageManager().getActivityInfo(
- new ComponentName(context, context.getClass()),
- PackageManager.GET_META_DATA).metaData;
+ Bundle launchedComponentMetaData = context.getPackageManager().getActivityInfo(
+ new ComponentName(context, context.getClass()),
+ PackageManager.GET_META_DATA).metaData;
+ if (launchedComponentMetaData != null) {
+ metaData.putAll(launchedComponentMetaData);
+ }
+
+ if (context instanceof Activity) {
+ Activity activity = (Activity) context;
+ ActivityInfo activityInfo = activity.getPackageManager().getActivityInfo(
+ activity.getComponentName(),
+ PackageManager.GET_META_DATA);
+ if (activityInfo.targetActivity != null && activityInfo.metaData != null) {
+ // The app was launched through the activity alias -
+ // get all the metadata from the alias too
+ metaData.putAll(activityInfo.metaData);
+ }
+ }
} catch (PackageManager.NameNotFoundException e) {
// Will only happen if the package provided (the one we are running in) is not
// installed - so should never happen.
}
- return new LauncherActivityMetadata(metaData == null ? new Bundle() : metaData, resources);
+ return new LauncherActivityMetadata(metaData, resources);
}
}
diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java
index 95342faf..e5e97ab3 100644
--- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java
+++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/TwaLauncher.java
@@ -34,10 +34,12 @@
import androidx.browser.trusted.TokenStore;
import androidx.browser.trusted.TrustedWebActivityIntent;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
-import androidx.core.content.ContextCompat;
-import com.google.androidbrowserhelper.trusted.ChromeOsSupport;
-import com.google.androidbrowserhelper.trusted.splashscreens.SplashScreenStrategy;
+import com.google.androidbrowserhelper.BuildConfig;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
/**
* Encapsulates the steps necessary to launch a Trusted Web Activity, such as establishing a
@@ -46,7 +48,13 @@
public class TwaLauncher {
private static final String TAG = "TwaLauncher";
- private static final int DEFAULT_SESSION_ID = 96375;
+ private static final Map mTaskIdToSessionId = new HashMap<>();
+
+ private static final String EXTRA_STARTUP_UPTIME_MILLIS =
+ "org.chromium.chrome.browser.customtabs.trusted.STARTUP_UPTIME_MILLIS";
+
+ private static final String EXTRA_ANDROID_BROWSER_HELPER_VERSION =
+ "org.chromium.chrome.browser.ANDROID_BROWSER_HELPER_VERSION";
public static final FallbackStrategy CCT_FALLBACK_STRATEGY =
(context, twaBuilder, providerPackage, completionCallback) -> {
@@ -96,6 +104,8 @@ public class TwaLauncher {
private boolean mDestroyed;
+ private long mStartupUptimeMillis;
+
public interface FallbackStrategy {
void launch(Context context,
TrustedWebActivityIntentBuilder twaBuilder,
@@ -111,12 +121,20 @@ public TwaLauncher(Context context) {
this(context, null);
}
+ /**
+ * Same as above, but also allows to specify a task id to distinguish several sessions running
+ * for the same TWA app.
+ */
+ public TwaLauncher(Context context, @Nullable Integer taskId) {
+ this(context, null, taskId);
+ }
+
/**
* Same as above, but also allows to specify a browser to launch. If specified, it is assumed to
* support TWAs.
*/
- public TwaLauncher(Context context, @Nullable String providerPackage) {
- this(context, providerPackage, DEFAULT_SESSION_ID,
+ public TwaLauncher(Context context, @Nullable String providerPackage, @Nullable Integer taskId) {
+ this(context, providerPackage, taskId,
new SharedPreferencesTokenStore(context));
}
@@ -124,10 +142,10 @@ public TwaLauncher(Context context, @Nullable String providerPackage) {
* Same as above, but also accepts a session id. This allows to launch multiple TWAs in the same
* task.
*/
- public TwaLauncher(Context context, @Nullable String providerPackage, int sessionId,
+ public TwaLauncher(Context context, @Nullable String providerPackage, @Nullable Integer taskId,
TokenStore tokenStore) {
mContext = context;
- mSessionId = sessionId;
+ mSessionId = makeSessionId(taskId);
mTokenStore = tokenStore;
if (providerPackage == null) {
TwaProviderPicker.Action action =
@@ -140,6 +158,19 @@ public TwaLauncher(Context context, @Nullable String providerPackage, int sessio
}
}
+ private static Integer makeSessionId(@Nullable Integer taskId) {
+ if(taskId == null) return Integer.MAX_VALUE;
+
+ Integer sessionId = mTaskIdToSessionId.get(taskId);
+ if(sessionId == null) {
+ Random random = new Random();
+ sessionId = random.nextInt(Integer.MAX_VALUE);
+ mTaskIdToSessionId.put(taskId, sessionId);
+ }
+
+ return sessionId;
+ }
+
/**
* Opens the specified url in a TWA.
* When TWA is already running in the current task, the url will be opened in existing TWA,
@@ -269,6 +300,11 @@ private void launchWhenSplashScreenReady(TrustedWebActivityIntentBuilder builder
}
Log.d(TAG, "Launching Trusted Web Activity.");
TrustedWebActivityIntent intent = builder.build(mSession);
+ if (mStartupUptimeMillis != 0) {
+ intent.getIntent().putExtra(EXTRA_STARTUP_UPTIME_MILLIS, mStartupUptimeMillis);
+ }
+ intent.getIntent().putExtra(
+ EXTRA_ANDROID_BROWSER_HELPER_VERSION, BuildConfig.LIBRARY_VERSION);
FocusActivity.addToIntent(intent.getIntent(), mContext);
intent.launchTrustedWebActivity(mContext);
@@ -299,6 +335,15 @@ public String getProviderPackage() {
return mProviderPackage;
}
+ /**
+ * Sets the timestamp (in SystemClock.uptimeMillis()) when the TWA launcher
+ * activity was created. This timestamp is used to report the full startup
+ * duration to the browser.
+ */
+ public void setStartupUptimeMillis(long startupUptimeMillis) {
+ mStartupUptimeMillis = startupUptimeMillis;
+ }
+
private class TwaCustomTabsServiceConnection extends CustomTabsServiceConnection {
private Runnable mOnSessionCreatedRunnable;
private Runnable mOnSessionCreationFailedRunnable;
diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/WebViewFallbackActivity.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/WebViewFallbackActivity.java
index 34fdbb6b..a89c678e 100644
--- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/WebViewFallbackActivity.java
+++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/WebViewFallbackActivity.java
@@ -29,6 +29,7 @@
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowManager;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
@@ -296,10 +297,10 @@ public void onShowCustomView(View paramView, CustomViewCallback paramCustomViewC
// exiting the fullscreen.
this.originalOrientation = getRequestedOrientation();
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addContentView(this.fullScreenView,
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
- getWindow().addFlags(View.SYSTEM_UI_FLAG_FULLSCREEN);
}
@Override
@@ -308,9 +309,10 @@ public void onHideCustomView() {
if (fullScreenView == null) {
return;
}
+
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
((ViewGroup) fullScreenView.getParent()).removeView(fullScreenView);
this.fullScreenView = null;
- getWindow().clearFlags(View.SYSTEM_UI_FLAG_FULLSCREEN);
setRequestedOrientation(this.originalOrientation);
}
};
@@ -322,5 +324,8 @@ private static void setupWebSettings(WebSettings webSettings) {
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setDatabaseEnabled(true);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ webSettings.setMediaPlaybackRequiresUserGesture(false);
+ }
}
}
diff --git a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategy.java b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategy.java
index 4918a6e9..e9b3f9be 100644
--- a/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategy.java
+++ b/androidbrowserhelper/src/main/java/com/google/androidbrowserhelper/trusted/splashscreens/PwaWrapperSplashScreenStrategy.java
@@ -19,6 +19,7 @@
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Matrix;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -86,6 +87,8 @@ public class PwaWrapperSplashScreenStrategy implements SplashScreenStrategy {
@Nullable
private Runnable mOnEnterAnimationCompleteRunnable;
+ private boolean mStartChromeBeforeAnimationComplete;
+
/**
* @param activity {@link Activity} on top of which a TWA is going to be launched.
* @param drawableId Resource id of the Drawable of an image (e.g. logo) displayed in the
@@ -104,7 +107,8 @@ public PwaWrapperSplashScreenStrategy(
ImageView.ScaleType scaleType,
@Nullable Matrix transformationMatrix,
int fadeOutDurationMillis,
- String fileProviderAuthority) {
+ String fileProviderAuthority,
+ boolean startChromeBeforeAnimationComplete) {
mDrawableId = drawableId;
mBackgroundColor = backgroundColor;
mScaleType = scaleType;
@@ -112,6 +116,7 @@ public PwaWrapperSplashScreenStrategy(
mActivity = activity;
mFileProviderAuthority = fileProviderAuthority;
mFadeOutDurationMillis = fadeOutDurationMillis;
+ mStartChromeBeforeAnimationComplete = startChromeBeforeAnimationComplete;
}
@Override
@@ -192,11 +197,11 @@ public void configureTwaBuilder(TrustedWebActivityIntentBuilder builder,
mProviderPackage);
mSplashImageTransferTask.execute(
- success -> onSplashImageTransferred(builder, success, onReadyCallback));
+ success -> onSplashImageTransferred(builder, success, onReadyCallback, session));
}
private void onSplashImageTransferred(TrustedWebActivityIntentBuilder builder, boolean success,
- Runnable onReadyCallback) {
+ Runnable onReadyCallback, CustomTabsSession session) {
if (!success) {
Log.w(TAG, "Failed to transfer splash image.");
onReadyCallback.run();
@@ -204,17 +209,26 @@ private void onSplashImageTransferred(TrustedWebActivityIntentBuilder builder, b
}
builder.setSplashScreenParams(makeSplashScreenParamsBundle());
- runWhenEnterAnimationComplete(() -> {
- onReadyCallback.run();
- mActivity.overridePendingTransition(0, 0); // Avoid window animations during transition.
- });
+ Runnable taskToRun = () -> {
+ onReadyCallback.run();
+ mActivity.overridePendingTransition(0, 0); // Avoid window animations during transition.
+ };
+
+ if (mStartChromeBeforeAnimationComplete) {
+ taskToRun.run();
+ } else {
+ runWhenEnterAnimationComplete(taskToRun, session, builder.getUri());
+ }
}
- private void runWhenEnterAnimationComplete(Runnable runnable) {
+ private void runWhenEnterAnimationComplete(Runnable runnable, CustomTabsSession session,
+ Uri uri) {
if (mEnterAnimationComplete) {
runnable.run();
} else {
mOnEnterAnimationCompleteRunnable = runnable;
+ boolean preloadResult = session.mayLaunchUrl(uri, null, null);
+ Log.i(TAG, "Enter animation not complete, try preload url. Result: " + preloadResult);
}
}
diff --git a/build.gradle b/build.gradle
index c5faeab2..16a020c9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,7 +22,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.1'
+ classpath 'com.android.tools.build:gradle:8.9.1'
}
}
@@ -30,6 +30,14 @@ allprojects {
repositories {
google()
jcenter()
+
+ // Repository for DexMaker
+ maven {
+ url = "https://linkedin.jfrog.io/artifactory/open-source/"
+ content {
+ includeGroup 'com.linkedin.dexmaker'
+ }
+ }
}
}
diff --git a/demos/custom-tabs-auth-tab/.gitignore b/demos/custom-tabs-auth-tab/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/demos/custom-tabs-auth-tab/build.gradle b/demos/custom-tabs-auth-tab/build.gradle
new file mode 100644
index 00000000..2005de77
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ namespace 'com.google.androidbrowserhelper.demos.customtabsauthtab'
+ defaultConfig {
+ applicationId "com.google.androidbrowserhelper.demos.customtabsauthtab"
+ minSdkVersion 26
+ compileSdk 36
+ targetSdkVersion 35
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation project(path: ':androidbrowserhelper')
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'androidx.activity:activity:1.9.3'
+ implementation 'androidx.browser:browser:1.9.0-alpha03'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.annotation:annotation:1.9.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
+}
diff --git a/demos/custom-tabs-auth-tab/src/main/AndroidManifest.xml b/demos/custom-tabs-auth-tab/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..828ba710
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/AuthManager.java b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/AuthManager.java
new file mode 100644
index 00000000..2290a1fd
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/AuthManager.java
@@ -0,0 +1,104 @@
+package com.google.androidbrowserhelper.demos.customtabsauthtab;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+import androidx.browser.auth.AuthTabIntent;
+import androidx.annotation.OptIn;
+
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * This class helps managing an authentication flow. It was created with the goal of demonstrating
+ * how to use Custom Tabs Auth Tab to handle auth and is not meant as a complete implementation
+ * of the OAuth protocol. We recommend checking out https://github.com/openid/AppAuth-Android for
+ * a comprehensive implementation of the OAuth protocol.
+ */
+
+public class AuthManager {
+ private static final String TAG = "OAuthManager";
+
+ private final String mClientId;
+ private final String mClientSecret;
+ private final String mAuthorizationEndpoint;
+ private final String mRedirectScheme;
+
+ public interface OAuthCallback {
+ void auth(String accessToken, String scope, String tokenType);
+ }
+
+ public AuthManager(String clientId, String clientSecret, String authorizationEndpoint,
+ String redirectScheme) {
+ mClientId = clientId;
+ mClientSecret = clientSecret;
+ mAuthorizationEndpoint = authorizationEndpoint;
+ mRedirectScheme = redirectScheme;
+ }
+
+ public void authorize(Context context, ActivityResultLauncher launcher, String scope) {
+ // Generate a random state.
+ String state = UUID.randomUUID().toString();
+
+ // Save the state so we can verify later.
+ SharedPreferences preferences =
+ context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE);
+ preferences.edit()
+ .putString("OAUTH_STATE", state)
+ .apply();
+
+ // Create an authorization URI to the OAuth Endpoint.
+ Uri uri = Uri.parse(mAuthorizationEndpoint)
+ .buildUpon()
+ .appendQueryParameter("response_type", "code")
+ .appendQueryParameter("client_id", mClientId)
+ .appendQueryParameter("scope", scope)
+ .appendQueryParameter("state", state)
+ .build();
+
+ // Open the Authorization URI in a Chrome Custom Auth Tab.
+ AuthTabIntent authTabIntent = new AuthTabIntent.Builder().build();
+ authTabIntent.launch(launcher, uri, mRedirectScheme);
+ }
+
+ public void continueAuthFlow(@NonNull Context context, Uri uri, @NonNull OAuthCallback callback) {
+ String code = uri.getQueryParameter("code");
+ SharedPreferences preferences =
+ context.getSharedPreferences("OAUTH_STORAGE", Context.MODE_PRIVATE);
+ String state = preferences.getString("OAUTH_STATE", "");
+ Uri tokenUri = Uri.parse("https://github.com/login/oauth/access_token")
+ .buildUpon()
+ .appendQueryParameter("client_id", mClientId)
+ .appendQueryParameter("client_secret", mClientSecret)
+ .appendQueryParameter("code", code)
+ .appendQueryParameter("state", state)
+ .build();
+
+ // Run the network request off the UI thread.
+ new Thread(() -> {
+ try {
+ String response = Utils.fetch(tokenUri);
+ // The response is a query-string. We concatenate with a valid domain to be
+ // able to easily parse and extract values.
+ Uri responseUri = Uri.parse("http://example.com?" + response);
+ String accessToken = responseUri.getQueryParameter("access_token");
+ String tokenType = responseUri.getQueryParameter("token_type");
+ String scope = responseUri.getQueryParameter("scope");
+
+ // Invoke the callback in the main thread.
+ new Handler(Looper.getMainLooper()).post(
+ () -> callback.auth(accessToken, scope, tokenType));
+
+ } catch (IOException e) {
+ Log.e(TAG, "Error requesting access token: " + e.getMessage());
+ }
+ }).start();
+ }
+}
diff --git a/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/GithubApi.java b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/GithubApi.java
new file mode 100644
index 00000000..1d62cdca
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/GithubApi.java
@@ -0,0 +1,43 @@
+package com.google.androidbrowserhelper.demos.customtabsauthtab;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+public class GithubApi {
+ private static final String TAG = "GithubAPI";
+ private static final String API_ENDPOINT = "https://api.github.com/user";
+ private static final String AUTH_HEADER_KEY = "Authorization";
+
+ public interface UserCallback {
+ void onUserData(String username);
+ }
+
+ public static void requestGithubUsername(String token, UserCallback callback) {
+ new Thread(() -> {
+ try {
+ Uri uri = Uri.parse(API_ENDPOINT);
+ Map headers =
+ Collections.singletonMap(AUTH_HEADER_KEY, "token " + token);
+ String response = Utils.fetch(uri, headers);
+ JSONObject user = new JSONObject(response);
+ String username = user.getString("name");
+
+ // Invoke the callback in the main thread.
+ new Handler(Looper.getMainLooper()).post(() -> {
+ callback.onUserData(username);
+ });
+ } catch (IOException | JSONException ex) {
+ Log.e(TAG, "Error fetching GitHub user: " + ex.getMessage());
+ }
+ }).start();
+ }
+}
diff --git a/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/MainActivity.java b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/MainActivity.java
new file mode 100644
index 00000000..190bc79c
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/MainActivity.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.androidbrowserhelper.demos.customtabsauthtab;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.browser.auth.AuthTabIntent;
+
+public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "MainActivity";
+
+ private static final String AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
+ private static final String CLIENT_ID = "";
+ private static final String CLIENT_SECRET = "";
+ private static final String REDIRECT_SCHEME = "auth";
+
+ private static final AuthManager O_AUTH_MANAGER =
+ new AuthManager(CLIENT_ID, CLIENT_SECRET, AUTHORIZATION_ENDPOINT, REDIRECT_SCHEME);
+
+ private final ActivityResultLauncher mLauncher =
+ AuthTabIntent.registerActivityResultLauncher(this, this::handleAuthResult);
+
+ private Button mLoginButton;
+ private TextView mUserText;
+ private ProgressBar mProgressBar;
+ private boolean mLoggedIn;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mLoginButton = findViewById(R.id.login_button);
+ mUserText = findViewById(R.id.user_text);
+ mProgressBar = findViewById(R.id.progress_bar);
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ Uri data = intent.getData();
+ if (data != null && data.getHost() != null
+ && data.getHost().startsWith("callback")) {
+ mProgressBar.setVisibility(View.VISIBLE);
+ mLoginButton.setEnabled(false);
+ completeAuth(data);
+ }
+ }
+ }
+
+ public void login(View v) {
+ if (mLoggedIn) {
+ mLoginButton.setText(R.string.login);
+ mUserText.setText(R.string.logged_out);
+ mLoggedIn = false;
+ } else {
+ O_AUTH_MANAGER.authorize(this, mLauncher, "user");
+ }
+ }
+
+ private void handleAuthResult(AuthTabIntent.AuthResult result) {
+ if (result.resultCode == AuthTabIntent.RESULT_OK) {
+ completeAuth(result.resultUri);
+ }
+ }
+
+ private void completeAuth(Uri uri) {
+ O_AUTH_MANAGER.continueAuthFlow(this, uri, (accessToken, scope, tokenType) -> {
+ GithubApi.requestGithubUsername(accessToken, (username -> {
+ mLoginButton.setText(R.string.logout);
+ mLoginButton.setEnabled(true);
+ mProgressBar.setVisibility(View.INVISIBLE);
+ mUserText.setText(getString(R.string.logged_in, username));
+ mLoggedIn = true;
+ }));
+ });
+ }
+}
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/Utils.java b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/Utils.java
new file mode 100644
index 00000000..ebd29121
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/java/com/google/androidbrowserhelper/demos/customtabsauthtab/Utils.java
@@ -0,0 +1,49 @@
+package com.google.androidbrowserhelper.demos.customtabsauthtab;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Map;
+
+public class Utils {
+ public static String fetch(@NonNull Uri uri) throws IOException {
+ return fetch(uri, Collections.emptyMap());
+ }
+
+ public static String fetch(@NonNull Uri uri, Map headers) throws IOException {
+ HttpURLConnection connection = null;
+ try {
+ URL url = new URL(uri.toString());
+ connection = (HttpURLConnection)url.openConnection();
+ for(Map.Entry entry: headers.entrySet()) {
+ connection.setRequestProperty(entry.getKey(), entry.getValue());
+ }
+ connection.setDoOutput(true);
+ return inputStreamToString(connection.getInputStream());
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ @NonNull
+ public static String inputStreamToString(@NonNull InputStream inputStream) throws IOException {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ StringBuilder builder = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ builder.append(line).append('\n');
+ }
+ return builder.toString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/res/drawable/ic_launcher_background.xml b/demos/custom-tabs-auth-tab/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..9e983cb3
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-auth-tab/src/main/res/drawable/ic_launcher_foreground.xml b/demos/custom-tabs-auth-tab/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..9be1fda8
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/res/layout/activity_main.xml b/demos/custom-tabs-auth-tab/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..ad1f1ec3
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/layout/activity_main.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-anydpi/ic_launcher.xml b/demos/custom-tabs-auth-tab/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/demos/custom-tabs-auth-tab/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 00000000..6f3b755b
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-hdpi/ic_launcher.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 00000000..c209e78e
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..b2dfe3d1
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-mdpi/ic_launcher.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 00000000..4f0f1d64
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..62b611da
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-xhdpi/ic_launcher.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 00000000..948a3070
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..1b9a6956
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..28d4b77f
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9287f508
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 00000000..aa7d6427
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 00000000..9126ae37
Binary files /dev/null and b/demos/custom-tabs-auth-tab/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/demos/custom-tabs-auth-tab/src/main/res/values-night/themes.xml b/demos/custom-tabs-auth-tab/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..bbcbc94f
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/res/values/colors.xml b/demos/custom-tabs-auth-tab/src/main/res/values/colors.xml
new file mode 100644
index 00000000..03bda374
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/values/colors.xml
@@ -0,0 +1,18 @@
+
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
diff --git a/demos/custom-tabs-auth-tab/src/main/res/values/strings.xml b/demos/custom-tabs-auth-tab/src/main/res/values/strings.xml
new file mode 100644
index 00000000..520cf405
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Custom Auth Tab OAuth
+ You are not logged in.
+ Hello, %s.
+ login
+ logout
+
diff --git a/demos/custom-tabs-auth-tab/src/main/res/values/themes.xml b/demos/custom-tabs-auth-tab/src/main/res/values/themes.xml
new file mode 100644
index 00000000..18744301
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/demos/custom-tabs-auth-tab/src/main/res/xml/backup_rules.xml b/demos/custom-tabs-auth-tab/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..75dd5113
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-auth-tab/src/main/res/xml/data_extraction_rules.xml b/demos/custom-tabs-auth-tab/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000..a73ffe12
--- /dev/null
+++ b/demos/custom-tabs-auth-tab/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-ephemeral-with-fallback/.gitignore b/demos/custom-tabs-ephemeral-with-fallback/.gitignore
new file mode 100644
index 00000000..a7747886
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/.gitignore
@@ -0,0 +1,2 @@
+/build
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/README.md b/demos/custom-tabs-ephemeral-with-fallback/README.md
new file mode 100644
index 00000000..5382a9c7
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/README.md
@@ -0,0 +1,6 @@
+# Custom Tabs Ephemeral Demo With Fallback
+
+This demo shows how to launch a Custom Tab with ephemeral browsing enabled.
+For devices where ephemeral browsing is not yet available, this demo also
+implements a fallback method to open a web view that clears cookies, cache,
+history and stored file data.
\ No newline at end of file
diff --git a/demos/custom-tabs-ephemeral-with-fallback/build.gradle b/demos/custom-tabs-ephemeral-with-fallback/build.gradle
new file mode 100644
index 00000000..e643657e
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+apply plugin: 'com.android.application'
+
+android {
+ namespace 'com.google.androidbrowserhelper.demos.customtabsephemeralwithfallback'
+ defaultConfig {
+ applicationId "com.google.androidbrowserhelper.demos.customtabsephemeralwithfallback"
+ minSdkVersion 26
+ compileSdk 36
+ targetSdkVersion 35
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation project(path: ':androidbrowserhelper')
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'androidx.activity:activity:1.9.3'
+ implementation 'androidx.browser:browser:1.9.0-alpha01'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.annotation:annotation:1.9.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
+}
diff --git a/demos/custom-tabs-ephemeral-with-fallback/proguard-rules.pro b/demos/custom-tabs-ephemeral-with-fallback/proguard-rules.pro
new file mode 100644
index 00000000..cf504086
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/AndroidManifest.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..94b6703a
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/MainActivity.java b/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/MainActivity.java
new file mode 100644
index 00000000..d99e834e
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/MainActivity.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.androidbrowserhelper.demos.customtabsephemeralwithfallback;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.widget.Button;
+import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
+import androidx.browser.customtabs.CustomTabColorSchemeParams;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.CustomTabsServiceConnection;
+import androidx.browser.customtabs.CustomTabsSession;
+import androidx.browser.customtabs.ExperimentalEphemeralBrowsing;
+
+public class MainActivity extends Activity {
+ private static final String URL = "https://xchrdw.github.io/browsing-data/siteDataTester.html";
+ private CustomTabsSession mSession;
+ private CustomTabsServiceConnection mConnection;
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ mConnection = new CustomTabsServiceConnection() {
+ @Override
+ public void onCustomTabsServiceConnected(@NonNull ComponentName name,
+ @NonNull CustomTabsClient client) {
+ mSession = client.newSession(null);
+ client.warmup(0);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) { }
+ };
+
+ String packageName = CustomTabsClient.getPackageName(this, null);
+ if (packageName != null) {
+ CustomTabsClient.bindCustomTabsService(this, packageName, mConnection);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Button launchButton = findViewById(R.id.launch);
+ launchButton.setOnClickListener(view -> {
+ try {
+ if (isEphemeralTabSupported()) {
+ launchEphemeralTab();
+ } else {
+ launchFallbackWebView();
+ }
+ } catch (RemoteException e) {
+ launchFallbackWebView();
+ }
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mConnection == null) return;
+ unbindService(mConnection);
+ mConnection = null;
+ }
+
+ @OptIn(markerClass = ExperimentalEphemeralBrowsing.class)
+ private void launchEphemeralTab() {
+ CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
+ .setEphemeralBrowsingEnabled(true)
+ .setUrlBarHidingEnabled(false)
+ .setShareState(CustomTabsIntent.SHARE_STATE_OFF)
+ .setCloseButtonIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_back_arrow))
+ .setDefaultColorSchemeParams(new CustomTabColorSchemeParams.Builder()
+ .setToolbarColor(getColor(R.color.colorPrimary)).build())
+ .build();
+ customTabsIntent.launchUrl(this, Uri.parse(URL));
+ }
+
+ private void launchFallbackWebView() {
+ Intent webIntent = new Intent(this, WebViewActivity.class);
+ webIntent.putExtra(WebViewActivity.EXTRA_URL, URL);
+ startActivity(webIntent);
+ }
+
+ @OptIn(markerClass = ExperimentalEphemeralBrowsing.class)
+ private boolean isEphemeralTabSupported() throws RemoteException {
+ String provider = CustomTabsClient.getPackageName(this, null);
+ if (provider == null) {
+ return false;
+ } else {
+ return CustomTabsClient.isEphemeralBrowsingSupported(this, provider);
+ }
+ }
+}
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/Utils.java b/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/Utils.java
new file mode 100644
index 00000000..a74e0dd3
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/Utils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.androidbrowserhelper.demos.customtabsephemeralwithfallback;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+
+public class Utils {
+ private static final String TAG = "Utils";
+ private static final String APP_WEB_VIEW_DIRECTORY = "app_webview";
+
+ public static void clearAppDirectory(Context context) {
+ File appDirectory = new File(context.getCacheDir().getParent(), APP_WEB_VIEW_DIRECTORY);
+ if (appDirectory.exists() && appDirectory.isDirectory()) {
+ delete(appDirectory);
+ }
+ }
+
+ private static void delete(File file) {
+ if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ if (files != null) {
+ for (File innerFile : files) {
+ delete(innerFile);
+ }
+ }
+ }
+
+ if (!file.delete()) {
+ Log.d(TAG, "Could not remove file: " + file.getName());
+ }
+ }
+}
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/WebViewActivity.java b/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/WebViewActivity.java
new file mode 100644
index 00000000..9b5a4756
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeralwithfallback/WebViewActivity.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.androidbrowserhelper.demos.customtabsephemeralwithfallback;
+
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.webkit.CookieManager;
+import android.webkit.ServiceWorkerController;
+import android.webkit.WebStorage;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import androidx.annotation.Nullable;
+
+public class WebViewActivity extends Activity {
+
+ public static final String EXTRA_URL = "extra_url";
+
+ private WebView mWebView;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_webview);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ mWebView = findViewById(R.id.webview);
+ setupWebView();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private void setupWebView() {
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.getSettings().setDomStorageEnabled(true);
+
+ String url = getIntent().getStringExtra(EXTRA_URL);
+ if (url == null || url.isEmpty()) {
+ finish();
+ return;
+ }
+
+ clearAllWebData();
+
+ // Disable service workers
+ ServiceWorkerController swController = ServiceWorkerController.getInstance();
+ swController.getServiceWorkerWebSettings().setBlockNetworkLoads(true);
+
+ mWebView.setWebViewClient(new WebViewClient() {
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+
+ // Block JavaScript Web Locks API
+ view.evaluateJavascript(
+ "Object.defineProperty(navigator, 'locks', { value: null });",
+ null
+ );
+ }
+ });
+
+ mWebView.loadUrl(url);
+ }
+
+ private void clearAllWebData() {
+
+ // Clear cookies, cache, history and stored file data
+ CookieManager.getInstance().removeAllCookies(null);
+ CookieManager.getInstance().flush();
+
+ mWebView.clearCache(true);
+ mWebView.clearHistory();
+ mWebView.clearFormData();
+ mWebView.clearSslPreferences();
+ mWebView.clearMatches();
+
+ WebStorage.getInstance().deleteAllData();
+ Utils.clearAppDirectory(this);
+ }
+}
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-anydpi/ic_notification_icon.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-anydpi/ic_notification_icon.xml
new file mode 100644
index 00000000..4c5c8a73
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-anydpi/ic_notification_icon.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-hdpi/ic_back_arrow.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-hdpi/ic_back_arrow.png
new file mode 100644
index 00000000..af495f3d
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-hdpi/ic_back_arrow.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-mdpi/ic_back_arrow.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-mdpi/ic_back_arrow.png
new file mode 100644
index 00000000..8f04754d
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-mdpi/ic_back_arrow.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..5a8f0381
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xhdpi/ic_back_arrow.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xhdpi/ic_back_arrow.png
new file mode 100644
index 00000000..568a0ed4
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xhdpi/ic_back_arrow.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xxhdpi/ic_back_arrow.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xxhdpi/ic_back_arrow.png
new file mode 100644
index 00000000..2cac4131
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xxhdpi/ic_back_arrow.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xxxhdpi/ic_back_arrow.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xxxhdpi/ic_back_arrow.png
new file mode 100644
index 00000000..05ea471b
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable-xxxhdpi/ic_back_arrow.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable/ic_launcher_background.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..7806d1dc
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/layout/activity_main.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..6876cae7
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/layout/activity_webview.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/layout/activity_webview.xml
new file mode 100644
index 00000000..893cb863
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/layout/activity_webview.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..4a757b27
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..4a757b27
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a571e600
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..61da551c
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c41dd285
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..db5080a7
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..6dba46da
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..da31a871
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..15ac6817
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b216f2d3
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f25a4197
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e96783cc
Binary files /dev/null and b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/colors.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/colors.xml
new file mode 100644
index 00000000..ac2bf97a
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/colors.xml
@@ -0,0 +1,19 @@
+
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/strings.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/strings.xml
new file mode 100644
index 00000000..e18538f3
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+
+
+ Custom Tab Ephemeral With Fallback
+ Launch
+
+
diff --git a/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/styles.xml b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/styles.xml
new file mode 100644
index 00000000..643c7ae8
--- /dev/null
+++ b/demos/custom-tabs-ephemeral-with-fallback/src/main/res/values/styles.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/.gitignore b/demos/custom-tabs-ephemeral/.gitignore
new file mode 100644
index 00000000..a7747886
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/.gitignore
@@ -0,0 +1,2 @@
+/build
+
diff --git a/demos/custom-tabs-ephemeral/README.md b/demos/custom-tabs-ephemeral/README.md
new file mode 100644
index 00000000..edc6c902
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/README.md
@@ -0,0 +1,3 @@
+# Custom Tabs Ephemeral Demo
+
+This demo shows how to launch a Custom Tab with ephemeral browsing enabled.
\ No newline at end of file
diff --git a/demos/custom-tabs-ephemeral/build.gradle b/demos/custom-tabs-ephemeral/build.gradle
new file mode 100644
index 00000000..43a9d980
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+apply plugin: 'com.android.application'
+
+android {
+ namespace 'com.google.androidbrowserhelper.demos.customtabsephemeral'
+ defaultConfig {
+ applicationId "com.google.androidbrowserhelper.demos.customtabsephemeral"
+ minSdkVersion 26
+ compileSdk 36
+ targetSdkVersion 35
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation project(path: ':androidbrowserhelper')
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'androidx.activity:activity:1.9.3'
+ implementation 'androidx.browser:browser:1.9.0-alpha01'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.annotation:annotation:1.9.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
+}
diff --git a/demos/custom-tabs-ephemeral/proguard-rules.pro b/demos/custom-tabs-ephemeral/proguard-rules.pro
new file mode 100644
index 00000000..cf504086
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
diff --git a/demos/custom-tabs-ephemeral/src/main/AndroidManifest.xml b/demos/custom-tabs-ephemeral/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..94e67844
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeral/MainActivity.java b/demos/custom-tabs-ephemeral/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeral/MainActivity.java
new file mode 100644
index 00000000..8308bd50
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/java/com/google/androidbrowserhelper/demos/customtabsephemeral/MainActivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.androidbrowserhelper.demos.customtabsephemeral;
+
+import androidx.annotation.OptIn;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.ExperimentalEphemeralBrowsing;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.Button;
+
+public class MainActivity extends Activity {
+ private static final Uri URL = Uri.parse("https://xchrdw.github.io/browsing-data/siteDataTester.html");
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ Button mLaunchButton = findViewById(R.id.launch);
+ mLaunchButton.setOnClickListener(view -> {
+ launchTab();
+ });
+ }
+
+ @OptIn(markerClass = ExperimentalEphemeralBrowsing.class)
+ private void launchTab() {
+ Intent customTabsIntent = new CustomTabsIntent.Builder()
+ .setEphemeralBrowsingEnabled(true)
+ .build()
+ .intent;
+ customTabsIntent.setData(URL);
+ startActivity(customTabsIntent);
+ }
+}
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/drawable-anydpi/ic_notification_icon.xml b/demos/custom-tabs-ephemeral/src/main/res/drawable-anydpi/ic_notification_icon.xml
new file mode 100644
index 00000000..4c5c8a73
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/drawable-anydpi/ic_notification_icon.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demos/custom-tabs-ephemeral/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..5a8f0381
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/drawable/ic_launcher_background.xml b/demos/custom-tabs-ephemeral/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..7806d1dc
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/layout/activity_main.xml b/demos/custom-tabs-ephemeral/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..6876cae7
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demos/custom-tabs-ephemeral/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..4a757b27
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demos/custom-tabs-ephemeral/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..4a757b27
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a571e600
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..61da551c
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c41dd285
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..db5080a7
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..6dba46da
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..da31a871
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..15ac6817
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b216f2d3
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f25a4197
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e96783cc
Binary files /dev/null and b/demos/custom-tabs-ephemeral/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/demos/custom-tabs-ephemeral/src/main/res/values/colors.xml b/demos/custom-tabs-ephemeral/src/main/res/values/colors.xml
new file mode 100644
index 00000000..ac2bf97a
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/values/colors.xml
@@ -0,0 +1,19 @@
+
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/values/strings.xml b/demos/custom-tabs-ephemeral/src/main/res/values/strings.xml
new file mode 100644
index 00000000..aa04e69d
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+
+
+ Custom Tab Ephemeral Demo
+ Launch
+
+
diff --git a/demos/custom-tabs-ephemeral/src/main/res/values/styles.xml b/demos/custom-tabs-ephemeral/src/main/res/values/styles.xml
new file mode 100644
index 00000000..643c7ae8
--- /dev/null
+++ b/demos/custom-tabs-ephemeral/src/main/res/values/styles.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-example-app/build.gradle b/demos/custom-tabs-example-app/build.gradle
index 9a38b094..56a29cb6 100644
--- a/demos/custom-tabs-example-app/build.gradle
+++ b/demos/custom-tabs-example-app/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "org.chromium.customtabsdemos"
defaultConfig {
applicationId "org.chromium.customtabsdemos"
minSdkVersion 26
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
@@ -35,8 +35,8 @@ android {
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
}
@@ -45,7 +45,8 @@ dependencies {
implementation project(path: ':androidbrowserhelper')
implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.browser:browser:1.4.0'
+ implementation 'androidx.browser:browser:1.6.0-alpha01'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
}
diff --git a/demos/custom-tabs-example-app/src/main/AndroidManifest.xml b/demos/custom-tabs-example-app/src/main/AndroidManifest.xml
index 369699cb..9ed50cd0 100644
--- a/demos/custom-tabs-example-app/src/main/AndroidManifest.xml
+++ b/demos/custom-tabs-example-app/src/main/AndroidManifest.xml
@@ -1,3 +1,4 @@
+
-
+
+
+
+
+
+
-
+
+
-
+
+
+
+ android:parentActivityName=".DemoListActivity">
@@ -41,7 +56,7 @@
+ android:parentActivityName=".DemoListActivity">
@@ -49,7 +64,15 @@
+ android:parentActivityName=".DemoListActivity">
+
+
+
@@ -57,7 +80,7 @@
+ android:parentActivityName=".DemoListActivity">
@@ -65,7 +88,7 @@
+ android:parentActivityName=".DemoListActivity">
@@ -74,10 +97,4 @@
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomTabsHelper.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomTabsHelper.java
index 0aa45d4c..b3ee8f71 100644
--- a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomTabsHelper.java
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomTabsHelper.java
@@ -85,23 +85,16 @@ public static String getPackageNameToUse(Context context) {
}
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
- // and service calls.
+ // and service calls. Prefer the default browser if it supports Custom Tabs.
if (packagesSupportingCustomTabs.isEmpty()) {
sPackageNameToUse = null;
- } else if (packagesSupportingCustomTabs.size() == 1) {
- sPackageNameToUse = packagesSupportingCustomTabs.get(0);
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
&& !hasSpecializedHandlerIntents(context, activityIntent)
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
sPackageNameToUse = defaultViewHandlerPackageName;
- } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
- sPackageNameToUse = STABLE_PACKAGE;
- } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
- sPackageNameToUse = BETA_PACKAGE;
- } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
- sPackageNameToUse = DEV_PACKAGE;
- } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
- sPackageNameToUse = LOCAL_PACKAGE;
+ } else {
+ // Otherwise, pick the next favorite Custom Tabs provider.
+ sPackageNameToUse = packagesSupportingCustomTabs.get(0);
}
return sPackageNameToUse;
}
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomUIActivity.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomUIActivity.java
index 761d9e62..ec734ba0 100644
--- a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomUIActivity.java
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomUIActivity.java
@@ -86,12 +86,8 @@ protected void onStop() {
@Override
public void onClick(View v) {
int viewId = v.getId();
- switch (viewId) {
- case R.id.start_custom_tab:
- openCustomTab();
- break;
- default:
- //Unknown View Clicked
+ if (viewId == R.id.start_custom_tab) {
+ openCustomTab();
}
}
@@ -171,7 +167,7 @@ private PendingIntent createPendingIntent(int actionSourceId) {
this.getApplicationContext(), ActionBroadcastReceiver.class);
actionIntent.putExtra(ActionBroadcastReceiver.KEY_ACTION_SOURCE, actionSourceId);
return PendingIntent.getBroadcast(
- getApplicationContext(), actionSourceId, actionIntent, 0);
+ getApplicationContext(), actionSourceId, actionIntent, PendingIntent.FLAG_MUTABLE);
}
/**
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/DemoListActivity.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/DemoListActivity.java
index 08f5342e..4c83b37a 100644
--- a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/DemoListActivity.java
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/DemoListActivity.java
@@ -46,6 +46,11 @@ protected void onCreate(Bundle savedInstanceState) {
SimpleCustomTabActivity.class);
activityDescList.add(activityDesc);
+ activityDesc = createActivityDesc(R.string.title_activity_partial_custom_tab,
+ R.string.description_activity_partial_custom_tab,
+ PartialCustomTabActivity.class);
+ activityDescList.add(activityDesc);
+
activityDesc = createActivityDesc(R.string.title_activity_service_connection,
R.string.description_activity_service_connection,
ServiceConnectionActivity.class);
@@ -61,6 +66,11 @@ protected void onCreate(Bundle savedInstanceState) {
NotificationParentActivity.class);
activityDescList.add(activityDesc);
+ activityDesc = createActivityDesc(R.string.title_activity_engagement_signals,
+ R.string.description_activity_engagement_signals,
+ EngagementSignalsActivity.class);
+ activityDescList.add(activityDesc);
+
RecyclerView recyclerView = findViewById(android.R.id.list);
recyclerView.setAdapter(listAdapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
@@ -125,6 +135,7 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position)
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
+ position = viewHolder.getAdapterPosition();
final ActivityDesc activityDesc = mActivityDescs.get(position);
String title = activityDesc.mTitle;
String description = activityDesc.mDescription;
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/EngagementSignalsActivity.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/EngagementSignalsActivity.java
new file mode 100644
index 00000000..bdedf6c2
--- /dev/null
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/EngagementSignalsActivity.java
@@ -0,0 +1,199 @@
+// Copyright 2023 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.chromium.customtabsdemos;
+
+import static androidx.browser.customtabs.CustomTabsIntent.ACTIVITY_HEIGHT_ADJUSTABLE;
+
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.browser.customtabs.CustomTabsCallback;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.CustomTabsSession;
+import androidx.browser.customtabs.EngagementSignalsCallback;
+
+public class EngagementSignalsActivity extends AppCompatActivity implements View.OnClickListener {
+ private static final String TAG = "EngagementSignals";
+ private static final int INITIAL_HEIGHT_DEFAULT_PX = 600;
+
+ private EditText mUrlEditText;
+ private TextView mTextVerticalScroll;
+ private TextView mTextGreatestPercentage;
+ private TextView mTextSessionEnd;
+ private TextView mTextNavigation;
+
+ @Nullable
+ private ServiceConnection mConnection;
+ @Nullable
+ private CustomTabsClient mClient;
+ @Nullable
+ private CustomTabsSession mCustomTabsSession;
+
+ private ServiceConnectionCallback mServiceConnectionCallback = new ServiceConnectionCallback() {
+ @Override
+ public void onServiceConnected(CustomTabsClient client) {
+ mClient = client;
+ mCustomTabsSession = mClient.newSession(mCustomTabsCallback);
+ try {
+ boolean engagementSignalsApiAvailable = mCustomTabsSession.isEngagementSignalsApiAvailable(Bundle.EMPTY);
+ if (!engagementSignalsApiAvailable) {
+ Log.d(TAG, "CustomTab Engagement signals not available, make sure to use the " +
+ "latest Chrome version");
+ return;
+ }
+ boolean signalsCallback = mCustomTabsSession.setEngagementSignalsCallback(mEngagementSignalsCallback, Bundle.EMPTY);
+ if (!signalsCallback) {
+ Log.w(TAG, "Could not set EngagementSignalsCallback");
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "The Service died while responding to the request.", e);
+ } catch (UnsupportedOperationException e) {
+ Log.w(TAG, "Engagement Signals API isn't supported by the browser.", e);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected() {
+ mClient = null;
+ mConnection = null;
+ mCustomTabsSession = null;
+ }
+ };
+
+ private EngagementSignalsCallback mEngagementSignalsCallback = new EngagementSignalsCallback() {
+ @Override
+ public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) {
+ Log.d(TAG, "onVerticalScrollEvent (isDirectionUp=" + isDirectionUp + ')');
+ mTextVerticalScroll.setText("vertical scroll " + (isDirectionUp ? "UP️" : "DOWN"));
+ }
+
+ @Override
+ public void onGreatestScrollPercentageIncreased(int scrollPercentage, @NonNull Bundle extras) {
+ Log.d(TAG, "scroll percentage: " + scrollPercentage + "%");
+ mTextGreatestPercentage.setText("scroll percentage: " + scrollPercentage + "%");
+ }
+
+ @Override
+ public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras) {
+ Log.d(TAG, "onSessionEnded (didUserInteract=" + didUserInteract + ')');
+ mTextSessionEnd.setText(didUserInteract ? "session ended with user interaction" : "session ended without user interaction");
+ }
+ };
+
+ private CustomTabsCallback mCustomTabsCallback = new CustomTabsCallback() {
+ @Override
+ public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
+ String event;
+ switch (navigationEvent) {
+ case CustomTabsCallback.NAVIGATION_ABORTED:
+ event = "NAVIGATION_ABORTED";
+ break;
+ case CustomTabsCallback.NAVIGATION_FAILED:
+ event = "NAVIGATION_FAILED";
+ break;
+ case CustomTabsCallback.NAVIGATION_FINISHED:
+ event = "NAVIGATION_FINISHED";
+ break;
+ case CustomTabsCallback.NAVIGATION_STARTED:
+ event = "NAVIGATION_STARTED";
+ // Scroll percentage and direction should be reset
+ mTextVerticalScroll.setText("vertical scroll: n/a");
+ mTextGreatestPercentage.setText("scroll percentage: n/a");
+ break;
+ case CustomTabsCallback.TAB_SHOWN:
+ event = "TAB_SHOWN";
+ break;
+ case CustomTabsCallback.TAB_HIDDEN:
+ event = "TAB_HIDDEN";
+ break;
+ default:
+ event = String.valueOf(navigationEvent);
+ }
+ Log.d(TAG, "onNavigationEvent (navigationEvent=" + event + ')');
+ mTextNavigation.setText("onNavigationEvent " + event);
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_engagement_signals);
+ findViewById(R.id.start_custom_tab).setOnClickListener(this);
+
+ mUrlEditText = findViewById(R.id.url);
+ mTextGreatestPercentage = findViewById(R.id.label_event_greatest_percentage);
+ mTextNavigation = findViewById(R.id.label_event_navigation);
+ mTextSessionEnd = findViewById(R.id.label_event_session_ended);
+ mTextVerticalScroll = findViewById(R.id.label_event_vertical_scroll);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ bindCustomTabsService();
+ }
+
+ private void bindCustomTabsService() {
+ String packageName = CustomTabsHelper.getPackageNameToUse(this);
+ if (packageName == null) {
+ Log.w(TAG, packageName + " does not support a Custom Tab Service connection");
+ return;
+ }
+ mConnection = new ServiceConnection(mServiceConnectionCallback);
+ CustomTabsClient.bindCustomTabsService(this, packageName, mConnection);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ unbindCustomTabsService();
+ }
+
+ private void unbindCustomTabsService() {
+ if (mConnection == null) return;
+ unbindService(mConnection);
+ mClient = null;
+ mCustomTabsSession = null;
+ mConnection = null;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int viewId = v.getId();
+ if (viewId == R.id.start_custom_tab) {
+ openCustomTab();
+ }
+ }
+
+ private void openCustomTab() {
+ String url = mUrlEditText.getText().toString();
+ CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(mCustomTabsSession);
+ intentBuilder.setInitialActivityHeightPx(INITIAL_HEIGHT_DEFAULT_PX);
+ CustomTabsIntent customTabsIntent = intentBuilder.build();
+ customTabsIntent.launchUrl(this, Uri.parse(url));
+ }
+
+}
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/NotificationParentActivity.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/NotificationParentActivity.java
index 52b785b1..cceda853 100644
--- a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/NotificationParentActivity.java
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/NotificationParentActivity.java
@@ -14,6 +14,9 @@
package org.chromium.customtabsdemos;
+import static org.chromium.customtabsdemos.R.*;
+
+import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -35,6 +38,7 @@
public class NotificationParentActivity extends AppCompatActivity implements View.OnClickListener {
private static final int NOTIFICATION_ID = 1;
public static final String EXTRA_URL = "extra.url";
+ private static final String CT_NOTIFICATION_CHANNEL_ID = "999";
private View mMessageTextView;
private View mCreateNotificationButton;
@@ -58,20 +62,24 @@ protected void onNewIntent(Intent intent) {
@Override
public void onClick(View v) {
int viewId = v.getId();
- switch (viewId) {
- case R.id.create_notification:
- createAndShowNotification();
- finish();
- break;
- default:
- //Unknown view clicked
+ if (viewId == R.id.create_notification) {
+ createAndShowNotification();
+ finish();
}
}
private void createAndShowNotification() {
+ NotificationManager notificationManager =
+ (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationChannel mChannel = notificationManager.getNotificationChannel(CT_NOTIFICATION_CHANNEL_ID);
+ if (mChannel == null) {
+ mChannel = new NotificationChannel(CT_NOTIFICATION_CHANNEL_ID, "Custom Tab Demo app", NotificationManager.IMPORTANCE_DEFAULT);
+ mChannel.enableVibration(true);
+ notificationManager.createNotificationChannel(mChannel);
+ }
NotificationCompat.Builder mBuilder =
- new NotificationCompat.Builder(this)
- .setSmallIcon(R.drawable.abc_popup_background_mtrl_mult)
+ new NotificationCompat.Builder(this, CT_NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(androidx.appcompat.R.drawable.abc_popup_background_mtrl_mult)
.setContentTitle(getString(R.string.notification_title))
.setContentText(getString(R.string.notification_text));
@@ -84,14 +92,12 @@ private void createAndShowNotification() {
resultIntent.setAction(Intent.ACTION_VIEW);
PendingIntent pendingIntent = PendingIntent.getActivity(
- this.getApplicationContext(), 0, resultIntent, 0);
+ this.getApplicationContext(), 0, resultIntent, PendingIntent.FLAG_IMMUTABLE);
mBuilder.setContentIntent(pendingIntent);
mBuilder.setAutoCancel(true);
- NotificationManager mNotificationManager =
- (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// mId allows you to update the notification later on.
- mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
+ notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
}
private void startChromeCustomTab(Intent intent) {
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/PartialCustomTabActivity.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/PartialCustomTabActivity.java
new file mode 100644
index 00000000..22ace067
--- /dev/null
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/PartialCustomTabActivity.java
@@ -0,0 +1,181 @@
+// Copyright 2022 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.chromium.customtabsdemos;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.browser.customtabs.CustomTabsSession;
+
+/**
+ * Opens Partial-height Chrome Custom Tab.
+ *
+ *
As of androidx.browser:browser:1.5.0-alpha01, following APIs for UI customization
+ * are supported:
+ *
+ *
Initial activity height
+ *
+ *
Prior to Chrome M107, this can be customized with a legacy intent extra flag
+ * "androidx.browser.customtabs.extra.INITIAL_ACTIVITY_HEIGHT_IN_PIXEL".
+ *
From M107, {@link CustomTabIntent.Builder#setInitialActivityHeightPx()} is supported.
+ *
+ *
+ *
Fixed-height tab
+ *
+ *
The tab height is not fixed i.e. resizable by default.
+ *
From M107, this can be set to fixed with an intent extra flag
+ * CustomTabsIntent.EXTRA_ACTIVITY_RESIZE_BEHAVIOR set to
+ * CustomTabsIntent.ACTIVITY_HEIGHT_FIXED.
+ *
From M107, {@link CustomTabIntent.Builder#setInitialActivityHeightPx(int, int)}
+ * is also supported to specify the height and resize behavior.
+ *
+ *
+ *
Start/End animation
+ *
+ *
Start animation is always slide-up from bottom. This will be enforced from M109.
+ *
End animation is always slide-down. This is not customizable.
+ *
+ *
+ *
Toolbar corner radius
+ *
+ *
The maximum radius is 16dp, also the default value.
+ *
Prior to M107, this can be customized with a legacy intent extra flag
+ * "androidx.browser.customtabs.extra.TOOLBAR_CORNER_RADIUS_IN_PIXEL"
+ *
From M107, {@link CustomTabIntent.Builder#setToolbarCornerRadiusDp()} is supported.
+ *
+ *
+ *
Background app interaction
+ *
+ *
Background app is interactable by default.
+ *
Interaction can be disabled from M109 with an intent extra flag
+ * "androix.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION"
+ *
Builder API will be provided in the future.
+ *
+ */
+public class PartialCustomTabActivity extends AppCompatActivity implements View.OnClickListener {
+ private static final String TAG = "PartialCustomTabActivity";
+ private static final int INITIAL_HEIGHT_DEFAULT_PX = 600;
+ private static final int CORNER_RADIUS_MAX_DP = 16;
+ private static final int CORNER_RADIUS_DEFAULT_DP = CORNER_RADIUS_MAX_DP;
+ private static final int BACKGROUND_INTERACT_OFF_VALUE = 2;
+
+ private EditText mUrlEditText;
+ private CheckBox mFixedHeightCheckbox;
+ private CheckBox mBackgroundAppCheckbox;
+ private SeekBar mToolbarCornerRadiusSlider;
+ private TextView mToolbarCornerRadiusLabel;
+ private CustomTabActivityHelper mCustomTabActivityHelper;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_partial_custom);
+
+ mCustomTabActivityHelper = new CustomTabActivityHelper();
+ findViewById(R.id.start_custom_tab).setOnClickListener(this);
+
+ mUrlEditText = findViewById(R.id.url);
+ mFixedHeightCheckbox = findViewById(R.id.fixed_height);
+ mBackgroundAppCheckbox = findViewById(R.id.background_app);
+
+ mToolbarCornerRadiusLabel = findViewById(R.id.radius_dp_label);
+ mToolbarCornerRadiusSlider = findViewById(R.id.corner_radius_slider);
+ mToolbarCornerRadiusSlider.setMax(CORNER_RADIUS_MAX_DP);
+ mToolbarCornerRadiusSlider.setProgress(CORNER_RADIUS_DEFAULT_DP);
+
+ int dp = mToolbarCornerRadiusSlider.getProgress();
+ mToolbarCornerRadiusLabel.setText(getString(R.string.dp_template, dp));
+ mToolbarCornerRadiusSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ String strDp = getString(R.string.dp_template, progress);
+ mToolbarCornerRadiusLabel.setText(strDp);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mCustomTabActivityHelper.bindCustomTabsService(this);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mCustomTabActivityHelper.unbindCustomTabsService(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int viewId = v.getId();
+ if (viewId == R.id.start_custom_tab) {
+ openCustomTab();
+ }
+ }
+
+ private void openCustomTab() {
+ String url = mUrlEditText.getText().toString();
+
+ // Uses the established session to build a PCCT intent.
+ CustomTabsSession session = mCustomTabActivityHelper.getSession();
+ CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(session);
+ int resizeBehavior = mFixedHeightCheckbox.isChecked()
+ ? CustomTabsIntent.ACTIVITY_HEIGHT_FIXED
+ : CustomTabsIntent.ACTIVITY_HEIGHT_DEFAULT;
+
+ intentBuilder.setInitialActivityHeightPx(INITIAL_HEIGHT_DEFAULT_PX, resizeBehavior);
+ int toolbarCornerRadiusDp = mToolbarCornerRadiusSlider.getProgress();
+ intentBuilder.setToolbarCornerRadiusDp(toolbarCornerRadiusDp);
+
+ CustomTabsIntent customTabsIntent = intentBuilder.build();
+
+ customTabsIntent.intent.putExtra(
+ "androidx.browser.customtabs.extra.INITIAL_ACTIVITY_HEIGHT_IN_PIXEL",
+ INITIAL_HEIGHT_DEFAULT_PX);
+ int toolbarCornerRadiusPx =
+ Math.round(toolbarCornerRadiusDp * getResources().getDisplayMetrics().density);
+ customTabsIntent.intent.putExtra(
+ "androidx.browser.customtabs.extra.TOOLBAR_CORNER_RADIUS_IN_PIXEL",
+ toolbarCornerRadiusPx);
+ if (resizeBehavior != CustomTabsIntent.ACTIVITY_HEIGHT_DEFAULT) {
+ customTabsIntent.intent.putExtra(
+ CustomTabsIntent.EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR, resizeBehavior);
+ }
+ if (!mBackgroundAppCheckbox.isChecked()) {
+ customTabsIntent.intent.putExtra(
+ "androix.browser.customtabs.extra.ENABLE_BACKGROUND_INTERACTION",
+ BACKGROUND_INTERACT_OFF_VALUE);
+ }
+
+ CustomTabActivityHelper.openCustomTab(
+ this, customTabsIntent, Uri.parse(url), new WebviewFallback());
+ }
+}
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/ServiceConnectionActivity.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/ServiceConnectionActivity.java
index 140685e9..44dec19a 100644
--- a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/ServiceConnectionActivity.java
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/ServiceConnectionActivity.java
@@ -36,21 +36,23 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_serviceconnection);
- customTabActivityHelper = new CustomTabActivityHelper();
- customTabActivityHelper.setConnectionCallback(this);
-
mUrlEditText = findViewById(R.id.url);
mMayLaunchUrlButton = findViewById(R.id.button_may_launch_url);
mMayLaunchUrlButton.setEnabled(false);
mMayLaunchUrlButton.setOnClickListener(this);
findViewById(R.id.start_custom_tab).setOnClickListener(this);
+
+ customTabActivityHelper = new CustomTabActivityHelper();
+ customTabActivityHelper.setConnectionCallback(this);
+ customTabActivityHelper.bindCustomTabsService(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
customTabActivityHelper.setConnectionCallback(null);
+ customTabActivityHelper.unbindCustomTabsService(this);
}
@Override
@@ -63,36 +65,18 @@ public void onCustomTabsDisconnected() {
mMayLaunchUrlButton.setEnabled(false);
}
- @Override
- protected void onStart() {
- super.onStart();
- customTabActivityHelper.bindCustomTabsService(this);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- customTabActivityHelper.unbindCustomTabsService(this);
- mMayLaunchUrlButton.setEnabled(false);
- }
-
@Override
public void onClick(View view) {
int viewId = view.getId();
Uri uri = Uri.parse(mUrlEditText.getText().toString());
- switch (viewId) {
- case R.id.button_may_launch_url:
- customTabActivityHelper.mayLaunchUrl(uri, null, null);
- break;
- case R.id.start_custom_tab:
- CustomTabsIntent customTabsIntent =
- new CustomTabsIntent.Builder(customTabActivityHelper.getSession())
- .build();
- CustomTabActivityHelper.openCustomTab(
- this, customTabsIntent, uri, new WebviewFallback());
- break;
- default:
- //Unkown View Clicked
+ if (viewId == R.id.button_may_launch_url) {
+ customTabActivityHelper.mayLaunchUrl(uri, null, null);
+ } else if (viewId == R.id.start_custom_tab) {
+ CustomTabsIntent customTabsIntent =
+ new CustomTabsIntent.Builder(customTabActivityHelper.getSession())
+ .build();
+ CustomTabActivityHelper.openCustomTab(
+ this, customTabsIntent, uri, new WebviewFallback());
}
}
}
diff --git a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/SimpleCustomTabActivity.java b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/SimpleCustomTabActivity.java
index 605a10c8..0c75521c 100644
--- a/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/SimpleCustomTabActivity.java
+++ b/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/SimpleCustomTabActivity.java
@@ -40,16 +40,11 @@ protected void onCreate(Bundle savedInstanceState) {
@Override
public void onClick(View v) {
int viewId = v.getId();
-
- switch (viewId) {
- case R.id.start_custom_tab:
- String url = mUrlEditText.getText().toString();
- CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
- CustomTabActivityHelper.openCustomTab(
- this, customTabsIntent, Uri.parse(url), new WebviewFallback());
- break;
- default:
- //Unknown View Clicked
+ if (viewId == R.id.start_custom_tab) {
+ String url = mUrlEditText.getText().toString();
+ CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
+ CustomTabActivityHelper.openCustomTab(
+ this, customTabsIntent, Uri.parse(url), new WebviewFallback());
}
}
}
diff --git a/demos/custom-tabs-example-app/src/main/res/layout/activity_engagement_signals.xml b/demos/custom-tabs-example-app/src/main/res/layout/activity_engagement_signals.xml
new file mode 100644
index 00000000..91ec1c7e
--- /dev/null
+++ b/demos/custom-tabs-example-app/src/main/res/layout/activity_engagement_signals.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-example-app/src/main/res/layout/activity_partial_custom.xml b/demos/custom-tabs-example-app/src/main/res/layout/activity_partial_custom.xml
new file mode 100644
index 00000000..74acf9a2
--- /dev/null
+++ b/demos/custom-tabs-example-app/src/main/res/layout/activity_partial_custom.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/custom-tabs-example-app/src/main/res/values/strings.xml b/demos/custom-tabs-example-app/src/main/res/values/strings.xml
index 043a4597..fe3caee8 100644
--- a/demos/custom-tabs-example-app/src/main/res/values/strings.xml
+++ b/demos/custom-tabs-example-app/src/main/res/values/strings.xml
@@ -21,20 +21,25 @@
Open your web content on Chrome Custom Tabshttps://developer.chrome.com/multidevice/android/customtabsURL:
- http://developer.android.com
+ http://developer.chrome.comBack to your application!Open Custom TabAdd MenusAdd Action ButtonPerform Action
+ Fixed height
+ Interactable background app
+ Corner radiusToolbar Color:#980e03Sample Menu ItemMay Launch URL
+ %ddpSimple Chrome Custom Tab
+ Partial Custom TabCustomized UI Chrome Custom TabService Connection ActivityBrowser Actions Demo
@@ -45,6 +50,11 @@
customizations, warm-up or pre-fetch. Allows the user to choose which URL to open.
+
+ It is a Chrome Custom Tab that works like a bottom sheet. Users can resize the tab,
+ interact with the parent activity. Also, additional UI customization is possible.
+
+
Allows the user to choose which URL to open, the Toolbar color, add an Action button and
custom menus to the Toolbar.
@@ -79,4 +89,9 @@
Toolbar Clicked with URL: %sUnknown Action Clicked with URL: %sTap the button to open custom tab. Long press the button to see the actions menu.
+ Engagement Signals Demo
+ Demonstrates which user engagement events are fired by a Custom Tab.
+ Open
+ Next
+ Previous
diff --git a/demos/custom-tabs-headers/build.gradle b/demos/custom-tabs-headers/build.gradle
index f4992fbb..f1755c2d 100644
--- a/demos/custom-tabs-headers/build.gradle
+++ b/demos/custom-tabs-headers/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.demos.customtabsheaders"
defaultConfig {
applicationId "com.google.androidbrowserhelper.demos.customtabsheaders"
minSdkVersion 25
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
diff --git a/demos/custom-tabs-navigation-callbacks/build.gradle b/demos/custom-tabs-navigation-callbacks/build.gradle
index 18b659e2..73840d0e 100644
--- a/demos/custom-tabs-navigation-callbacks/build.gradle
+++ b/demos/custom-tabs-navigation-callbacks/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.demos.customtabssession"
defaultConfig {
applicationId "com.google.androidbrowserhelper.demos.customtabsnavigationcallbacks"
minSdkVersion 25
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
diff --git a/demos/custom-tabs-navigation-callbacks/src/main/AndroidManifest.xml b/demos/custom-tabs-navigation-callbacks/src/main/AndroidManifest.xml
index df543531..096f8852 100644
--- a/demos/custom-tabs-navigation-callbacks/src/main/AndroidManifest.xml
+++ b/demos/custom-tabs-navigation-callbacks/src/main/AndroidManifest.xml
@@ -12,8 +12,7 @@
limitations under the License.
-->
-
+
diff --git a/demos/custom-tabs-oauth/build.gradle b/demos/custom-tabs-oauth/build.gradle
index 207a18cf..828ba20f 100644
--- a/demos/custom-tabs-oauth/build.gradle
+++ b/demos/custom-tabs-oauth/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.demos.customtabsoauth"
defaultConfig {
applicationId "com.google.androidbrowserhelper.demos.customtabsoauth"
minSdkVersion 23
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
diff --git a/demos/custom-tabs-oauth/readme.md b/demos/custom-tabs-oauth/readme.md
new file mode 100644
index 00000000..7395be8c
--- /dev/null
+++ b/demos/custom-tabs-oauth/readme.md
@@ -0,0 +1,2 @@
+This demo is deprecated but is retained for historical purposes.
+Please use the Auth Tab demo found in demo/custom-tabs-auth-tab.
\ No newline at end of file
diff --git a/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/MainActivity.java b/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/MainActivity.java
index 97dfe906..96e76557 100644
--- a/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/MainActivity.java
+++ b/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/MainActivity.java
@@ -25,6 +25,11 @@
import android.widget.ProgressBar;
import android.widget.TextView;
+@Deprecated
+/*
+ * This sample is deprecated and retained for historical purposes only.
+ * Instead use Auth Tab demo found in demos/custom-tabs-auth-tab.
+ */
public class MainActivity extends Activity {
private static final String AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
private static final String CLIENT_ID = "";
diff --git a/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/OAuthManager.java b/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/OAuthManager.java
index decc129a..dcc7658b 100644
--- a/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/OAuthManager.java
+++ b/demos/custom-tabs-oauth/src/main/java/com/google/androidbrowserhelper/demos/customtabsoauth/OAuthManager.java
@@ -15,11 +15,10 @@
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-/**
- * This class helps managing an OAuth flow. It was created with the goal of demonstrating how to
- * use Custom Tabs to handle the authorization flow and is not meant as a complete implementation
- * of the OAuth protocol. We recommend checking out https://github.com/openid/AppAuth-Android for
- * a comprehensive implementation of the OAuth protocol.
+@Deprecated
+/*
+ * This sample is deprecated and retained for historical purposes only.
+ * Instead use Auth Tab demo found in demos/custom-tabs-auth-tab.
*/
public class OAuthManager {
private static final String TAG = "OAuthManager";
diff --git a/demos/custom-tabs-session/build.gradle b/demos/custom-tabs-session/build.gradle
index aca58531..ec44fe72 100644
--- a/demos/custom-tabs-session/build.gradle
+++ b/demos/custom-tabs-session/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.demos.customtabssession"
defaultConfig {
applicationId "com.google.androidbrowserhelper.demos.customtabssession"
minSdkVersion 25
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
diff --git a/demos/twa-basic/build.gradle b/demos/twa-basic/build.gradle
index ae44ac57..0cdc23b2 100644
--- a/demos/twa-basic/build.gradle
+++ b/demos/twa-basic/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.browser.examples.twa_basic"
defaultConfig {
applicationId "com.google.browser.examples.twa_basic"
minSdkVersion 26
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
@@ -35,8 +35,8 @@ android {
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
}
diff --git a/demos/twa-custom-launcher/build.gradle b/demos/twa-custom-launcher/build.gradle
index 3859b409..a09dc435 100644
--- a/demos/twa-custom-launcher/build.gradle
+++ b/demos/twa-custom-launcher/build.gradle
@@ -17,11 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.demo"
+
defaultConfig {
applicationId "com.google.androidbrowserhelper"
minSdkVersion 23
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
@@ -34,8 +35,8 @@ android {
}
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
}
diff --git a/demos/twa-custom-launcher/src/main/AndroidManifest.xml b/demos/twa-custom-launcher/src/main/AndroidManifest.xml
index 25ebdb5f..eaf2bb38 100644
--- a/demos/twa-custom-launcher/src/main/AndroidManifest.xml
+++ b/demos/twa-custom-launcher/src/main/AndroidManifest.xml
@@ -12,8 +12,7 @@
limitations under the License.
-->
+ xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/MainActivity.java b/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/MainActivity.java
new file mode 100644
index 00000000..51809e93
--- /dev/null
+++ b/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/MainActivity.java
@@ -0,0 +1,171 @@
+package com.google.androidbrowserhelper.demos.twapostmessage;
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.browser.customtabs.CustomTabsCallback;
+import androidx.browser.customtabs.CustomTabsClient;
+import androidx.browser.customtabs.CustomTabsServiceConnection;
+import androidx.browser.customtabs.CustomTabsSession;
+import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
+import androidx.core.content.ContextCompat;
+
+/**
+ * A demo to showcase how to use the postMessage in CCT/TWA. Please note that the initialization has
+ * to be from the app, the steps will be as following:
+ *
+ * 1. Bind CustomTabsService and wait for the CustomTabsClient to be ready. 2. Use this Client to
+ * warmup and to create a new CustomTabsSession. 3. Listen for navigation events from
+ * CustomTabsCallback#onNavigationEvent. 4. Upon receiving NAVIGATION_FINISHED, we request a new
+ * PostMessageChannel from CustomTabsSession#requestPostMessageChannel. 5. When the channel is ready
+ * we can initialize the communication by posting the first message using
+ * CustomTabsSession#postMessage.
+ *
+ * Please note that requesting post message channel validates the relationship between your origin
+ * and the application using CustomTabsSession#validateRelationship, with the relation as
+ * Relation#RELATION_USE_AS_ORIGIN, please read the methods doc for more details.
+ *
+ * Validation with the origin doesn't necessarily mean that communication is exclusive to this
+ * origin, but the DAL validation is required to provide MessageEvent.origin field.
+ */
+public class MainActivity extends AppCompatActivity {
+
+ private CustomTabsClient mClient;
+ private CustomTabsSession mSession;
+ private Uri URL = Uri.parse("https://peconn.github.io/starters");
+
+ // This origin is going to be validated via DAL, please see
+ // (https://developer.chrome.com/docs/android/post-message-twa#add_the_app_to_web_validation),
+ // it has to either start with http or https.
+ private Uri SOURCE_ORIGIN = Uri.parse("https://sayedelabady.github.io/");
+ private Uri TARGET_ORIGIN = Uri.parse("https://peconn.github.io");
+ private boolean mValidated = false;
+
+ private final String TAG = "PostMessageDemo";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ // No need to ask for permission as the compileSDK is 31.
+ MessageNotificationHandler.createNotificationChannelIfNeeded(this);
+
+ bindCustomTabsService();
+
+ }
+
+ private final CustomTabsCallback customTabsCallback =
+ new CustomTabsCallback() {
+ @Override
+ public void onPostMessage(@NonNull String message, @Nullable Bundle extras) {
+ super.onPostMessage(message, extras);
+ if (message.contains("ACK")) {
+ return;
+ }
+ MessageNotificationHandler.showNotificationWithMessage(MainActivity.this, message);
+ Log.d(TAG, "Got message: " + message);
+ }
+
+ @Override
+ public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin,
+ boolean result, @Nullable Bundle extras) {
+ // If this fails:
+ // - Have you called warmup?
+ // - Have you set up Digital Asset Links correctly?
+ // - Double check what browser you're using.
+ Log.d(TAG, "Relationship result: " + result);
+ mValidated = result;
+ }
+
+ // Listens for navigation, requests the postMessage channel when one completes.
+ @Override
+ public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
+ if (navigationEvent != NAVIGATION_FINISHED) {
+ return;
+ }
+
+ if (!mValidated) {
+ Log.d(TAG, "Not starting PostMessage as validation didn't succeed.");
+ }
+
+ // If this fails:
+ // - Have you included PostMessageService in your AndroidManifest.xml?
+ boolean result = mSession.requestPostMessageChannel(SOURCE_ORIGIN, TARGET_ORIGIN,
+ new Bundle());
+ Log.d(TAG, "Requested Post Message Channel: " + result);
+ }
+
+ @Override
+ public void onMessageChannelReady(@Nullable Bundle extras) {
+ Log.d(TAG, "Message channel ready.");
+
+ int result = mSession.postMessage("First message", null);
+ Log.d(TAG, "postMessage returned: " + result);
+ }
+ };
+
+
+ private void bindCustomTabsService() {
+ String packageName = CustomTabsClient.getPackageName(this, null);
+ Toast.makeText(this, "Binding to " + packageName, Toast.LENGTH_SHORT).show();
+ CustomTabsClient.bindCustomTabsService(this, packageName,
+ new CustomTabsServiceConnection() {
+ @Override
+ public void onCustomTabsServiceConnected(@NonNull ComponentName name,
+ @NonNull CustomTabsClient client) {
+ mClient = client;
+
+ // Note: validateRelationship requires warmup to have been called.
+ client.warmup(0L);
+
+ mSession = mClient.newSession(customTabsCallback);
+
+ launch();
+ registerBroadcastReceiver();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ mClient = null;
+
+ }
+ });
+ }
+
+
+ // The demo should work for both CCT and TWA but here we are using TWA.
+ private void launch() {
+ new TrustedWebActivityIntentBuilder(URL).build(mSession)
+ .launchTrustedWebActivity(MainActivity.this);
+ }
+
+ @SuppressLint("WrongConstant")
+ private void registerBroadcastReceiver() {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(PostMessageBroadcastReceiver.POST_MESSAGE_ACTION);
+ ContextCompat.registerReceiver(this, new PostMessageBroadcastReceiver(mSession),
+ intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
+ }
+}
\ No newline at end of file
diff --git a/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/MessageNotificationHandler.java b/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/MessageNotificationHandler.java
new file mode 100644
index 00000000..35b72254
--- /dev/null
+++ b/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/MessageNotificationHandler.java
@@ -0,0 +1,69 @@
+package com.google.androidbrowserhelper.demos.twapostmessage;
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.annotation.SuppressLint;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+public class MessageNotificationHandler {
+
+ private final static String CHANNEL_ID = "channel_id";
+
+ private MessageNotificationHandler() {
+ }
+
+ @SuppressLint("MissingPermission")
+ public static void showNotificationWithMessage(Context context, String message) {
+ Intent intent = new Intent();
+ intent.setAction(PostMessageBroadcastReceiver.POST_MESSAGE_ACTION);
+
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_launcher_background)
+ .setContentTitle("Received a message")
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .addAction(R.drawable.ic_launcher_background, "Reply back", pendingIntent)
+ .setAutoCancel(true);
+
+ NotificationManagerCompat.from(context).notify(1, builder.build());
+
+ }
+
+ public static void createNotificationChannelIfNeeded(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ String name = "PostMessage Demo";
+ String descriptionText = "A channel to send post message demo notification";
+ int importance = NotificationManager.IMPORTANCE_HIGH;
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
+ channel.setDescription(descriptionText);
+
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+}
diff --git a/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/PostMessageBroadcastReceiver.java b/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/PostMessageBroadcastReceiver.java
new file mode 100644
index 00000000..60fa884c
--- /dev/null
+++ b/demos/twa-post-message/src/main/java/com/google/androidbrowserhelper/demos/twapostmessage/PostMessageBroadcastReceiver.java
@@ -0,0 +1,37 @@
+package com.google.androidbrowserhelper.demos.twapostmessage;
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import androidx.browser.customtabs.CustomTabsSession;
+
+public class PostMessageBroadcastReceiver extends BroadcastReceiver {
+
+ private CustomTabsSession customTabsSession;
+
+ public final static String POST_MESSAGE_ACTION = "com.example.postmessage.POST_MESSAGE_ACTION";
+
+ public PostMessageBroadcastReceiver(CustomTabsSession session) {
+ customTabsSession = session;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ customTabsSession.postMessage("Got it!", null);
+ }
+}
diff --git a/demos/twa-post-message/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demos/twa-post-message/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..966abaff
--- /dev/null
+++ b/demos/twa-post-message/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/twa-post-message/src/main/res/drawable/ic_launcher_background.xml b/demos/twa-post-message/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..61bb79ed
--- /dev/null
+++ b/demos/twa-post-message/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/twa-post-message/src/main/res/layout/activity_main.xml b/demos/twa-post-message/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..a82980fc
--- /dev/null
+++ b/demos/twa-post-message/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
\ No newline at end of file
diff --git a/demos/twa-post-message/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demos/twa-post-message/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..1323c6e8
--- /dev/null
+++ b/demos/twa-post-message/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/demos/twa-post-message/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demos/twa-post-message/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..1323c6e8
--- /dev/null
+++ b/demos/twa-post-message/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/demos/twa-post-message/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/twa-post-message/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a571e600
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/twa-post-message/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..61da551c
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/twa-post-message/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c41dd285
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/twa-post-message/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..db5080a7
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/twa-post-message/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..6dba46da
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/twa-post-message/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..da31a871
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/twa-post-message/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..15ac6817
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/twa-post-message/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b216f2d3
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/twa-post-message/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f25a4197
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/twa-post-message/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/twa-post-message/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e96783cc
Binary files /dev/null and b/demos/twa-post-message/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/demos/twa-post-message/src/main/res/values/strings.xml b/demos/twa-post-message/src/main/res/values/strings.xml
new file mode 100644
index 00000000..20488e38
--- /dev/null
+++ b/demos/twa-post-message/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ PostMessage Demo
+
\ No newline at end of file
diff --git a/demos/twa-web-share-target/build.gradle b/demos/twa-web-share-target/build.gradle
index 5e3952b4..a00a02dd 100644
--- a/demos/twa-web-share-target/build.gradle
+++ b/demos/twa-web-share-target/build.gradle
@@ -17,12 +17,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.webshare"
defaultConfig {
applicationId "com.google.androidbrowserhelper.webshare"
minSdkVersion 26
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
@@ -35,8 +35,8 @@ android {
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
}
diff --git a/demos/twa-webview-fallback/build.gradle b/demos/twa-webview-fallback/build.gradle
index dfa94625..0bd1f97b 100644
--- a/demos/twa-webview-fallback/build.gradle
+++ b/demos/twa-webview-fallback/build.gradle
@@ -1,13 +1,12 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
-
+ namespace "com.google.browser.examples.twawebviewfallback"
defaultConfig {
applicationId "com.google.browser.examples.twawebviewfallback"
minSdkVersion 26
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName "1.0"
@@ -16,8 +15,8 @@ android {
}
compileOptions {
- sourceCompatibility = 1.8
- targetCompatibility = 1.8
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0703c031..6c1b7b36 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Jan 06 14:01:45 GMT 2021
+#Tue Dec 03 18:37:48 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/lint.xml b/lint.xml
new file mode 100644
index 00000000..ab6fd29f
--- /dev/null
+++ b/lint.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/locationdelegation/build.gradle b/locationdelegation/build.gradle
index cf9a5009..34760ecb 100644
--- a/locationdelegation/build.gradle
+++ b/locationdelegation/build.gradle
@@ -15,14 +15,14 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
-def VERSION = "1.1.0";
+def VERSION = "1.1.2";
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.locationdelegation"
defaultConfig {
- minSdkVersion 19
+ minSdkVersion 21
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName VERSION
@@ -52,15 +52,16 @@ dependencies {
api 'androidx.browser:browser:1.4.0'
implementation fileTree(dir: "libs", include: ["*.jar"])
- implementation 'com.google.android.gms:play-services-location:17.0.0'
+ implementation 'com.google.android.gms:play-services-location:21.0.1'
testImplementation 'junit:junit:4.12'
- testImplementation 'org.mockito:mockito-core:3.0.0'
- testImplementation 'org.robolectric:robolectric:4.4'
+ testImplementation 'org.mockito:mockito-core:5.15.2'
+ testImplementation 'org.mockito:mockito-inline:5.2.0'
+ testImplementation 'org.robolectric:robolectric:4.12.2'
testImplementation 'androidx.test:core:1.4.0'
debugImplementation project(path: ':androidbrowserhelper')
- releaseImplementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.2.2'
+ releaseImplementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0'
}
publishing {
diff --git a/locationdelegation/src/test/java/com/google/androidbrowserhelper/locationdelegation/LocationProviderGmsCoreTest.java b/locationdelegation/src/test/java/com/google/androidbrowserhelper/locationdelegation/LocationProviderGmsCoreTest.java
index fde9c705..b538e87d 100644
--- a/locationdelegation/src/test/java/com/google/androidbrowserhelper/locationdelegation/LocationProviderGmsCoreTest.java
+++ b/locationdelegation/src/test/java/com/google/androidbrowserhelper/locationdelegation/LocationProviderGmsCoreTest.java
@@ -181,7 +181,7 @@ public void testStopAndReturnWhenError() throws Exception {
callbackTriggered.countDown();
};
doThrow(new IllegalStateException())
- .when(mMockLocationClient).requestLocationUpdates(any(), any(),any());
+ .when(mMockLocationClient).requestLocationUpdates(any(), any(LocationCallback.class),any());
mLocationProvider.start(locationCallback, false);
diff --git a/playbilling/README.md b/playbilling/README.md
new file mode 100644
index 00000000..721a901f
--- /dev/null
+++ b/playbilling/README.md
@@ -0,0 +1,63 @@
+# Play Billing
+
+The Play Billing module provides capabilities for your TWA app to connect with [Google Play Billing library](https://developer.android.com/google/play/billing), for example you can:
+
+* Query purchase history
+* Initialize a payment
+* Query SKU details
+
+The module uses [Version 5](https://developer.android.com/google/play/billing/release-notes#5-2-1) of Play Billing library.
+
+
+## How to use it? (Website)
+To use this API from your website you can do it as follows:
+```js
+const PAYMENT_METHOD = 'https://play.google.com/billing';
+const SKUS = [
+ 'android.test.purchased',
+ 'android.test.canceled',
+]
+
+const service = await window.getDigitalGoodsService(PAYMENT_METHOD);
+const details = await service.getDetails(SKUS);
+console.log(details);
+```
+
+## How to use it? (Android Project)
+To use it from your Android project you will need to do two steps:
+Add this to your `AndroidManifest.xml`
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Create a new Service that extends `DelegationService` with this line in `onCreate()` method:
+```java
+registerExtraCommandHandler(new DigitalGoodsRequestHandler(getApplicationContext()));
+```
+Then add it to your AndroidManifest and make it exported (`android:exported="true"`).
+
+
+
+You can find a working demo [here](https://github.com/GoogleChrome/android-browser-helper/tree/main/demos/twa-play-billing)
diff --git a/playbilling/build.gradle b/playbilling/build.gradle
index 2a9b5c7d..ad00e42a 100644
--- a/playbilling/build.gradle
+++ b/playbilling/build.gradle
@@ -17,14 +17,14 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
-def VERSION = "1.0.0-alpha08";
+def VERSION = "1.0.1";
android {
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
+ namespace "com.google.androidbrowserhelper.playbilling"
defaultConfig {
- minSdkVersion 19
+ minSdkVersion 21
+ compileSdk 36
targetSdkVersion 31
versionCode 1
versionName VERSION
@@ -33,6 +33,10 @@ android {
consumerProguardFiles 'consumer-rules.pro'
}
+ buildFeatures {
+ aidl = true
+ }
+
buildTypes {
release {
minifyEnabled false
@@ -56,10 +60,10 @@ dependencies {
api 'androidx.browser:browser:1.4.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation 'com.android.billingclient:billing:4.0.0'
+ implementation 'com.android.billingclient:billing:6.2.1'
testImplementation 'junit:junit:4.12'
- testImplementation 'org.robolectric:robolectric:4.4'
+ testImplementation 'org.robolectric:robolectric:4.12.2'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
@@ -89,7 +93,7 @@ publishing {
Production(MavenPublication) {
pom {
name = 'android-browser-helper-billing'
- url = 'https://github.com/GoogleChrome/android-browser-helper/tree/master/playbilling'
+ url = 'https://github.com/GoogleChrome/android-browser-helper/tree/main/playbilling'
licenses {
license {
name = 'The Apache License, Version 2.0'
diff --git a/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java b/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java
index 9fac75cb..5a38aafe 100644
--- a/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java
+++ b/playbilling/src/androidTest/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivityTest.java
@@ -90,23 +90,6 @@ public void successfulFlow() throws InterruptedException, JSONException {
assertActivityResult(Activity.RESULT_OK);
}
- @Test
- public void priceChangeConfirmationFlow() throws InterruptedException, JSONException {
- mContext.startActivity(getIntent(SKU, true));
- assertTrue(WrapperActivity.waitForLaunch());
-
- assertTrue(mWrapper.waitForConnect());
- mWrapper.triggerConnected();
-
- assertTrue(mWrapper.waitForQuerySkuDetails());
- mWrapper.triggerOnGotSkuDetails(getSkuDetailsList());
-
- assertTrue(mWrapper.waitForLaunchPriceChangeConfirmationFlow());
- mWrapper.triggerOnPriceChangeConfirmationResult();
-
- assertActivityResult(Activity.RESULT_OK);
- }
-
@Test
public void setsProxy() throws InterruptedException, JSONException {
mWrapper.setPaymentFlowWillBeSuccessful(true);
@@ -197,7 +180,7 @@ public void skuPassedToPlayBilling() throws InterruptedException {
assertTrue(mWrapper.waitForQuerySkuDetails());
List queriedSkuDetails = mWrapper.getQueriedSkuDetails();
- assertEquals(1, queriedSkuDetails.size());
+ assertEquals(2, queriedSkuDetails.size());
assertTrue(queriedSkuDetails.contains(SKU));
}
diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java
index 84081872..c567a550 100644
--- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java
+++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/digitalgoods/ConnectedBillingWrapper.java
@@ -21,7 +21,6 @@
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeResponseListener;
-import com.android.billingclient.api.PriceChangeConfirmationListener;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.SkuDetails;
@@ -120,11 +119,4 @@ public boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData d
throw new IllegalStateException(
"EnsuredConnectionBillingWrapper doesn't handle launch Payment flow");
}
-
- @Override
- public void launchPriceChangeConfirmationFlow(Activity activity, SkuDetails sku,
- PriceChangeConfirmationListener listener) {
- throw new IllegalStateException("EnsuredConnectionBillingWrapper doesn't handle the " +
- "price change confirmation flow");
- }
}
diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java
index 6d71630d..ecda3d84 100644
--- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java
+++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/BillingWrapper.java
@@ -21,7 +21,6 @@
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeResponseListener;
-import com.android.billingclient.api.PriceChangeConfirmationListener;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.SkuDetails;
@@ -79,10 +78,4 @@ void queryPurchaseHistory(@BillingClient.SkuType String skuType,
*/
boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData methodData);
- /**
- * Launches the price change confirmation flow.
- */
- void launchPriceChangeConfirmationFlow(Activity activity, SkuDetails sku,
- PriceChangeConfirmationListener listener);
-
}
diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java
index 7dd2c3a5..7069120f 100644
--- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java
+++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/MockBillingWrapper.java
@@ -22,7 +22,6 @@
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeResponseListener;
-import com.android.billingclient.api.PriceChangeConfirmationListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryRecord;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
@@ -58,9 +57,6 @@ public class MockBillingWrapper implements BillingWrapper {
private MultiSkuTypeInvocationTracker
mQueryPurchaseHistoryInvocation = new MultiSkuTypeInvocationTracker<>();
- private InvocationTracker
- mPriceChangeConfirmationFlow = new InvocationTracker<>();
-
private Intent mPlayBillingFlowLaunchIntent;
private final CountDownLatch mConnectLatch = new CountDownLatch(1);
@@ -105,12 +101,6 @@ public boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData d
return mPaymentFlowSuccessful;
}
- @Override
- public void launchPriceChangeConfirmationFlow(Activity activity, SkuDetails sku,
- PriceChangeConfirmationListener listener) {
- mPriceChangeConfirmationFlow.call(sku, listener);
- }
-
public void triggerConnected() {
mConnectionStateListener.onBillingSetupFinished(
toResult(BillingClient.BillingResponseCode.OK));
@@ -171,11 +161,6 @@ public void triggerOnPurchasesUpdated() {
mListener.onPurchaseFlowComplete(toResult(BillingClient.BillingResponseCode.OK), "");
}
- public void triggerOnPriceChangeConfirmationResult() {
- mPriceChangeConfirmationFlow.getCallback().onPriceChangeConfirmationResult(
- toResult(BillingClient.BillingResponseCode.OK));
- }
-
public boolean waitForConnect() throws InterruptedException {
return wait(mConnectLatch);
}
@@ -188,10 +173,6 @@ public boolean waitForLaunchPaymentFlow() throws InterruptedException {
return wait(mLaunchPaymentFlowLatch);
}
- public boolean waitForLaunchPriceChangeConfirmationFlow() throws InterruptedException {
- return mPriceChangeConfirmationFlow.waitUntilCalled();
- }
-
public boolean waitForQueryPurchases() throws InterruptedException {
return mQueryPurchasesInvocation.waitUntilCalled();
}
diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java
index aaea2ce8..60baf582 100644
--- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java
+++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PaymentActivity.java
@@ -63,6 +63,9 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
fail("Could not parse SKU.");
return;
}
+ if (mMethodData.isPriceChangeConfirmation) {
+ fail("Price change confirmation flow is not supported");
+ }
/**
* Note that we have temporarily disabled the IMMEDIATE_WITHOUT_PRORATION mode
@@ -114,21 +117,7 @@ private void onSkusQueried(BillingResult result, List skus) {
SkuDetails sku = skus.get(0);
- if (mMethodData.isPriceChangeConfirmation) {
- launchPriceChangeConfirmationFlow(sku);
- } else {
- launchPaymentFlow(sku);
- }
- }
-
- private void launchPriceChangeConfirmationFlow(SkuDetails sku) {
- mWrapper.launchPriceChangeConfirmationFlow(this, sku, result -> {
- if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
- setResultAndFinish(PaymentResult.priceChangeSuccess());
- } else {
- fail("Price change confirmation flow ended with result: " + result);
- }
- });
+ launchPaymentFlow(sku);
}
private void launchPaymentFlow(SkuDetails sku) {
diff --git a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java
index 69e5e499..d197be2b 100644
--- a/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java
+++ b/playbilling/src/main/java/com/google/androidbrowserhelper/playbilling/provider/PlayBillingWrapper.java
@@ -27,8 +27,6 @@
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
-import com.android.billingclient.api.PriceChangeConfirmationListener;
-import com.android.billingclient.api.PriceChangeFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesResponseListener;
@@ -124,15 +122,12 @@ public boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData m
BillingFlowParams.Builder builder = BillingFlowParams.newBuilder();
builder.setSkuDetails(sku);
- if (methodData.purchaseToken != null) {
- subUpdateParamsBuilder.setOldSkuPurchaseToken(methodData.purchaseToken);
- }
-
if (methodData.prorationMode != null) {
subUpdateParamsBuilder.setReplaceSkusProrationMode(methodData.prorationMode);
}
- if (methodData.purchaseToken != null || methodData.prorationMode != null) {
+ if (methodData.purchaseToken != null) {
+ subUpdateParamsBuilder.setOldSkuPurchaseToken(methodData.purchaseToken);
builder.setSubscriptionUpdateParams(subUpdateParamsBuilder.build());
}
@@ -142,14 +137,4 @@ public boolean launchPaymentFlow(Activity activity, SkuDetails sku, MethodData m
return result.getResponseCode() == BillingClient.BillingResponseCode.OK;
}
-
- @Override
- public void launchPriceChangeConfirmationFlow(Activity activity, SkuDetails sku,
- PriceChangeConfirmationListener listener) {
- PriceChangeFlowParams params = PriceChangeFlowParams
- .newBuilder()
- .setSkuDetails(sku)
- .build();
- mClient.launchPriceChangeConfirmationFlow(activity, params, listener);
- }
}
diff --git a/settings.gradle b/settings.gradle
index 6564ef5f..a0c64ef5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,6 +6,9 @@ include ':demos:custom-tabs-example-app'
include ':demos:custom-tabs-headers'
include ':demos:custom-tabs-navigation-callbacks'
include ':demos:custom-tabs-oauth'
+include ':demos:custom-tabs-auth-tab'
+include ':demos:custom-tabs-ephemeral-with-fallback'
+include ':demos:custom-tabs-ephemeral'
include ':demos:custom-tabs-session'
include ':demos:twa-basic'
include ':demos:twa-custom-launcher'
@@ -16,5 +19,6 @@ include ':demos:twa-notification-delegation'
include ':demos:twa-offline-first'
include ':demos:twa-orientation'
include ':demos:twa-play-billing'
+include ':demos:twa-post-message'
include ':demos:twa-web-share-target'
include ':demos:twa-webview-fallback'