// Copyright 2019 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 com.google.androidbrowserhelper.trusted; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.util.Log; import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabsClient; import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsService; import androidx.browser.customtabs.CustomTabsServiceConnection; import androidx.browser.customtabs.CustomTabsSession; import androidx.browser.customtabs.TrustedWebUtils; import androidx.browser.trusted.Token; import androidx.browser.trusted.TokenStore; import androidx.browser.trusted.TrustedWebActivityIntentBuilder; import androidx.core.content.ContextCompat; import com.google.androidbrowserhelper.trusted.splashscreens.SplashScreenStrategy; /** * Encapsulates the steps necessary to launch a Trusted Web Activity, such as establishing a * connection with {@link CustomTabsService}. */ public class TwaLauncher { private static final String TAG = "TwaLauncher"; private static final int DEFAULT_SESSION_ID = 96375; private static final String ARC_FEATURE = "org.chromium.arc"; public static final FallbackStrategy CCT_FALLBACK_STRATEGY = (context, twaBuilder, providerPackage, completionCallback) -> { // CustomTabsIntent will fall back to launching the Browser if there are no Custom Tabs // providers installed. CustomTabsIntent intent = twaBuilder.buildCustomTabsIntent(); if (providerPackage != null) { intent.intent.setPackage(providerPackage); } // Add the TWA flag to the intent if the app is running on ARC++ on Chrome OS. if (context.getPackageManager().hasSystemFeature(ARC_FEATURE)) { intent.intent.putExtra(TrustedWebUtils.EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY, true); } intent.launchUrl(context, twaBuilder.getUri()); if (completionCallback != null) { completionCallback.run(); } }; public static final FallbackStrategy WEBVIEW_FALLBACK_STRATEGY = (context, twaBuilder, providerPackage, completionCallback) -> { Intent intent = WebViewFallbackActivity.createLaunchIntent(context, twaBuilder.getUri(), LauncherActivityMetadata.parse(context)); context.startActivity(intent); if (completionCallback != null) { completionCallback.run(); } }; private final Context mContext; @Nullable private final String mProviderPackage; @TwaProviderPicker.LaunchMode private final int mLaunchMode; private final int mSessionId; @Nullable private TwaCustomTabsServiceConnection mServiceConnection; @Nullable private CustomTabsSession mSession; private TokenStore mTokenStore; private boolean mDestroyed; public interface FallbackStrategy { void launch(Context context, TrustedWebActivityIntentBuilder twaBuilder, @Nullable String providerPackage, @Nullable Runnable completionCallback); } /** * Creates an instance that will automatically choose the browser to launch a TWA in. * If no browser supports TWA, will launch a usual Custom Tab (see {@link TwaProviderPicker}. */ public TwaLauncher(Context context) { this(context, null); } /** * 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, new SharedPreferencesTokenStore(context)); } /** * 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, TokenStore tokenStore) { mContext = context; mSessionId = sessionId; mTokenStore = tokenStore; if (providerPackage == null) { TwaProviderPicker.Action action = TwaProviderPicker.pickProvider(context.getPackageManager()); mProviderPackage = action.provider; mLaunchMode = action.launchMode; } else { mProviderPackage = providerPackage; mLaunchMode = TwaProviderPicker.LaunchMode.TRUSTED_WEB_ACTIVITY; } } /** * Opens the specified url in a TWA. * When TWA is already running in the current task, the url will be opened in existing TWA, * if the same instance TwaLauncher is used. If another instance of TwaLauncher is used, * the TWA will be reused only if the session ids match (see constructors). * * @param url Url to open. */ public void launch(Uri url) { launch(new TrustedWebActivityIntentBuilder(url), null, null); } /** * Similar to {@link #launch(Uri)}, but allows more customization. * * @param twaBuilder {@link TrustedWebActivityIntentBuilder} containing the url to open, along with * optional parameters: status bar color, additional trusted origins, etc. * @param splashScreenStrategy {@link SplashScreenStrategy} to use for showing splash screens, * null if splash screen not needed. * @param completionCallback Callback triggered when the url has been opened. * @param fallbackStrategy Called when there is no TWA provider available or when launching * the Trusted Web Activity fails. */ public void launch(TrustedWebActivityIntentBuilder twaBuilder, @Nullable SplashScreenStrategy splashScreenStrategy, @Nullable Runnable completionCallback, FallbackStrategy fallbackStrategy) { if (mDestroyed) { throw new IllegalStateException("TwaLauncher already destroyed"); } if (mLaunchMode == TwaProviderPicker.LaunchMode.TRUSTED_WEB_ACTIVITY) { launchTwa(twaBuilder, splashScreenStrategy, completionCallback, fallbackStrategy); } else { fallbackStrategy.launch(mContext, twaBuilder, mProviderPackage, completionCallback); } } /** * Similar to {@link #launch(Uri)}, but allows more customization. Uses a Custom Tabs fallback * when a TWA provider is not available or when launching a TWA fails. * * @param twaBuilder {@link TrustedWebActivityIntentBuilder} containing the url to open, along with * optional parameters: status bar color, additional trusted origins, etc. * @param splashScreenStrategy {@link SplashScreenStrategy} to use for showing splash screens, * null if splash screen not needed. * @param completionCallback Callback triggered when the url has been opened. */ public void launch(TrustedWebActivityIntentBuilder twaBuilder, @Nullable SplashScreenStrategy splashScreenStrategy, @Nullable Runnable completionCallback) { launch(twaBuilder, splashScreenStrategy, completionCallback, CCT_FALLBACK_STRATEGY); } private void launchTwa(TrustedWebActivityIntentBuilder twaBuilder, @Nullable SplashScreenStrategy splashScreenStrategy, @Nullable Runnable completionCallback, FallbackStrategy fallbackStrategy) { if (splashScreenStrategy != null) { splashScreenStrategy.onTwaLaunchInitiated(mProviderPackage, twaBuilder); } Runnable onSessionCreatedRunnable = () -> launchWhenSessionEstablished(twaBuilder, splashScreenStrategy, completionCallback); if (mSession != null) { onSessionCreatedRunnable.run(); return; } Runnable onSessionCreationFailedRunnable = () -> { // The provider has been unable to create a session for us, we can't launch a // Trusted Web Activity. We launch a fallback specially designed to provide the // best user experience. fallbackStrategy.launch(mContext, twaBuilder, mProviderPackage, completionCallback); }; if (mServiceConnection == null) { mServiceConnection = new TwaCustomTabsServiceConnection(); } mServiceConnection.setSessionCreationRunnables( onSessionCreatedRunnable, onSessionCreationFailedRunnable); CustomTabsClient.bindCustomTabsService(mContext, mProviderPackage, mServiceConnection); } private void launchWhenSessionEstablished(TrustedWebActivityIntentBuilder twaBuilder, @Nullable SplashScreenStrategy splashScreenStrategy, @Nullable Runnable completionCallback) { if (mSession == null) { throw new IllegalStateException("mSession is null in launchWhenSessionEstablished"); } if (splashScreenStrategy != null) { splashScreenStrategy.configureTwaBuilder(twaBuilder, mSession, () -> launchWhenSplashScreenReady(twaBuilder, completionCallback)); } else { launchWhenSplashScreenReady(twaBuilder, completionCallback); } } private void launchWhenSplashScreenReady(TrustedWebActivityIntentBuilder builder, @Nullable Runnable completionCallback) { if (mDestroyed || mSession == null) { return; // Service was disconnected and / or TwaLauncher was destroyed while preparing // the splash screen (e.g. user closed the app). See https://crbug.com/1052367 // for further details. } Log.d(TAG, "Launching Trusted Web Activity."); Intent intent = builder.build(mSession).getIntent(); FocusActivity.addToIntent(intent, mContext); ContextCompat.startActivity(mContext, intent, null); // Remember who we connect to as the package that is allowed to delegate notifications // to us. mTokenStore.store(Token.create(mProviderPackage, mContext.getPackageManager())); if (completionCallback != null) { completionCallback.run(); } } /** * Performs clean-up. */ public void destroy() { if (mDestroyed) { return; } if (mServiceConnection != null) { mContext.unbindService(mServiceConnection); } mDestroyed = true; } /** * Returns package name of the browser this TwaLauncher is launching. */ @Nullable public String getProviderPackage() { return mProviderPackage; } private class TwaCustomTabsServiceConnection extends CustomTabsServiceConnection { private Runnable mOnSessionCreatedRunnable; private Runnable mOnSessionCreationFailedRunnable; private void setSessionCreationRunnables(@Nullable Runnable onSuccess, @Nullable Runnable onFailure) { mOnSessionCreatedRunnable = onSuccess; mOnSessionCreationFailedRunnable = onFailure; } @Override public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) { if (!ChromeLegacyUtils .supportsLaunchWithoutWarmup(mContext.getPackageManager(), mProviderPackage)) { client.warmup(0); } mSession = client.newSession(null, mSessionId); if (mSession != null && mOnSessionCreatedRunnable != null) { mOnSessionCreatedRunnable.run(); } else if (mSession == null && mOnSessionCreationFailedRunnable != null) { mOnSessionCreationFailedRunnable.run(); } mOnSessionCreatedRunnable = null; mOnSessionCreationFailedRunnable = null; } @Override public void onServiceDisconnected(ComponentName componentName) { mSession = null; } } }