// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs;
import static org.chromium.components.content_settings.PrefNames.COOKIE_CONTROLS_MODE;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.os.SystemClock;
import android.text.TextUtils;
import android.widget.RemoteViews;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.browser.customtabs.EngagementSignalsCallback;
import androidx.browser.customtabs.ExperimentalMinimizationCallback;
import androidx.browser.customtabs.PostMessageServiceConnection;
import androidx.browser.customtabs.PrefetchOptions;
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;
import org.json.JSONException;
import org.json.JSONObject;
import org.chromium.base.Callback;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.SysUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.ChainedTasks;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.build.annotations.MockedInTests;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.ChromeApplicationImpl;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.browserservices.PostMessageHandler;
import org.chromium.chrome.browser.browserservices.SessionDataHolder;
import org.chromium.chrome.browser.browserservices.SessionHandler;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.content.WebContentsFactory;
import org.chromium.chrome.browser.customtabs.ClientManager.CalledWarmup;
import org.chromium.chrome.browser.customtabs.content.EngagementSignalsHandler;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.init.ProcessInitializationHandler;
import org.chromium.chrome.browser.metrics.UmaSessionStats;
import org.chromium.chrome.browser.page_load_metrics.PageLoadMetrics;
import org.chromium.chrome.browser.prefetch.settings.PreloadPagesSettingsBridge;
import org.chromium.chrome.browser.prefetch.settings.PreloadPagesState;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.google_bottom_bar.proto.IntentParams.GoogleBottomBarIntentParams;
import org.chromium.components.content_settings.CookieControlsMode;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.components.variations.SyntheticTrialAnnotationMode;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.ChildProcessLauncherHelper;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.Referrer;
import org.chromium.network.mojom.ReferrerPolicy;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* Implementation of the ICustomTabsService interface.
*
* <p>Note: This class is meant to be package private, and is public to be accessible from {@link
* ChromeApplicationImpl}.
*/
@JNINamespace("customtabs")
@MockedInTests
public class CustomTabsConnection {
private static final String TAG = "ChromeConnection";
private static final String LOG_SERVICE_REQUESTS = "custom-tabs-log-service-requests";
// Callback names for |extraCallback()|.
@VisibleForTesting static final String PAGE_LOAD_METRICS_CALLBACK = "NavigationMetrics";
static final String BOTTOM_BAR_SCROLL_STATE_CALLBACK = "onBottomBarScrollStateChanged";
@VisibleForTesting static final String OPEN_IN_BROWSER_CALLBACK = "onOpenInBrowser";
@VisibleForTesting static final String ON_WARMUP_COMPLETED = "onWarmupCompleted";
@VisibleForTesting
static final String ON_DETACHED_REQUEST_REQUESTED = "onDetachedRequestRequested";
@VisibleForTesting
static final String ON_DETACHED_REQUEST_COMPLETED = "onDetachedRequestCompleted";
// Constants for sending connection characteristics.
public static final String DATA_REDUCTION_ENABLED = "dataReductionEnabled";
// "/bg_non_interactive" is from L MR1, "/apps/bg_non_interactive" before,
// and "background" from O.
@VisibleForTesting
static final Set<String> BACKGROUND_GROUPS =
new HashSet<>(
Arrays.asList(
"/bg_non_interactive", "/apps/bg_non_interactive", "/background"));
// TODO(lizeb): Move to the support library.
@VisibleForTesting
static final String REDIRECT_ENDPOINT_KEY = "androidx.browser.REDIRECT_ENDPOINT";
@VisibleForTesting
static final String PARALLEL_REQUEST_REFERRER_KEY =
"android.support.customtabs.PARALLEL_REQUEST_REFERRER";
static final String PARALLEL_REQUEST_REFERRER_POLICY_KEY =
"android.support.customtabs.PARALLEL_REQUEST_REFERRER_POLICY";
@VisibleForTesting
static final String PARALLEL_REQUEST_URL_KEY =
"android.support.customtabs.PARALLEL_REQUEST_URL";
static final String RESOURCE_PREFETCH_URL_LIST_KEY =
"androidx.browser.RESOURCE_PREFETCH_URL_LIST";
private static final String ON_RESIZED_CALLBACK = "onResized";
private static final String ON_RESIZED_SIZE_EXTRA = "size";
@VisibleForTesting
static final String IS_EPHEMERAL_BROWSING_SUPPORTED = "isEphemeralBrowsingSupported";
@VisibleForTesting
static final String EPHEMERAL_BROWSING_SUPPORTED_KEY = "ephemeralBrowsingSupported";
@VisibleForTesting static final String ON_ACTIVITY_LAYOUT_CALLBACK = "onActivityLayout";
@VisibleForTesting static final String ON_ACTIVITY_LAYOUT_LEFT_EXTRA = "left";
@VisibleForTesting static final String ON_ACTIVITY_LAYOUT_TOP_EXTRA = "top";
@VisibleForTesting static final String ON_ACTIVITY_LAYOUT_RIGHT_EXTRA = "right";
@VisibleForTesting static final String ON_ACTIVITY_LAYOUT_BOTTOM_EXTRA = "bottom";
@VisibleForTesting static final String ON_ACTIVITY_LAYOUT_STATE_EXTRA = "state";
@IntDef({
ParallelRequestStatus.NO_REQUEST,
ParallelRequestStatus.SUCCESS,
ParallelRequestStatus.FAILURE_NOT_INITIALIZED,
ParallelRequestStatus.FAILURE_NOT_AUTHORIZED,
ParallelRequestStatus.FAILURE_INVALID_URL,
ParallelRequestStatus.FAILURE_INVALID_REFERRER,
ParallelRequestStatus.FAILURE_INVALID_REFERRER_FOR_SESSION
})
@Retention(RetentionPolicy.SOURCE)
@interface ParallelRequestStatus {
// Values should start from 0 and can't have gaps (they're used for indexing
// PARALLEL_REQUEST_MESSAGES).
@VisibleForTesting int NO_REQUEST = 0;
@VisibleForTesting int SUCCESS = 1;
@VisibleForTesting int FAILURE_NOT_INITIALIZED = 2;
@VisibleForTesting int FAILURE_NOT_AUTHORIZED = 3;
@VisibleForTesting int FAILURE_INVALID_URL = 4;
@VisibleForTesting int FAILURE_INVALID_REFERRER = 5;
@VisibleForTesting int FAILURE_INVALID_REFERRER_FOR_SESSION = 6;
int NUM_ENTRIES = 7;
}
private static final String[] PARALLEL_REQUEST_MESSAGES = {
"No request",
"Success",
"Chrome not initialized",
"Not authorized",
"Invalid URL",
"Invalid referrer",
"Invalid referrer for session"
};
private static final String SYNTHETIC_FIELDTRIAL_CCT_EXPERIMENT_OVERRIDE =
"CCT_EXPERIMENT_OVERRIDE";
private static CustomTabsConnection sInstance;
private @Nullable String mTrustedPublisherUrlPackage;
private final HiddenTabHolder mHiddenTabHolder = new HiddenTabHolder();
protected final SessionDataHolder mSessionDataHolder;
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
final ClientManager mClientManager;
protected final boolean mLogRequests;
private final AtomicBoolean mWarmupHasBeenCalled = new AtomicBoolean();
private final AtomicBoolean mWarmupHasBeenFinished = new AtomicBoolean();
@Nullable private Callback<CustomTabsSessionToken> mDisconnectCallback;
private volatile ChainedTasks mWarmupTasks;
// Caches the previous height reported via |onResized|. Used for extraCallback
// |ON_RESIZED_CALLLBACK| which cares about height only.
private int mPrevHeight;
/** Whether Dynamic Features are enabled. CCT Intents can override the feature set. */
private boolean mIsDynamicIntentFeatureOverridesEnabled =
ChromeFeatureList.sCctIntentFeatureOverrides.isEnabled();
@Nullable private List<String> mDynamicEnabledFeatures;
@Nullable private List<String> mDynamicDisabledFeatures;
// Async tab prewarming can cause flakiness in tests when it runs after test shutdown and
// triggers LifetimeAsserts.
@VisibleForTesting public static boolean sSkipTabPrewarmingForTesting;
/**
* <strong>DO NOT CALL</strong> Public to be instanciable from {@link ChromeApplicationImpl}.
* This is however intended to be private.
*/
public CustomTabsConnection() {
super();
mClientManager = new ClientManager();
mLogRequests = CommandLine.getInstance().hasSwitch(LOG_SERVICE_REQUESTS);
mSessionDataHolder = ChromeApplicationImpl.getComponent().resolveSessionDataHolder();
}
/**
* @return The unique instance of ChromeCustomTabsConnection.
*/
public static CustomTabsConnection getInstance() {
if (sInstance == null) {
sInstance = AppHooks.get().createCustomTabsConnection();
}
return sInstance;
}
private static boolean hasInstance() {
return sInstance != null;
}
/**
* If service requests logging is enabled, logs that a call was made.
*
* No rate-limiting, can be spammy if the app is misbehaved.
*
* @param name Call name to log.
* @param result The return value for the logged call.
*/
void logCall(String name, Object result) {
if (!mLogRequests) return;
Log.w(TAG, "%s = %b, Calling UID = %d", name, result, Binder.getCallingUid());
}
/**
* If service requests logging is enabled, logs a callback.
*
* No rate-limiting, can be spammy if the app is misbehaved.
*
* @param name Callback name to log.
* @param args arguments of the callback.
*/
void logCallback(String name, Object args) {
if (!mLogRequests) return;
Log.w(TAG, "%s args = %s", name, args);
}
/**
* Converts a Bundle to JSON.
*
* The conversion is limited to Bundles not containing any array, and some elements are
* converted into strings.
*
* @param bundle a Bundle to convert.
* @return A JSON object, empty object if the parameter is null.
*/
protected static JSONObject bundleToJson(Bundle bundle) {
JSONObject json = new JSONObject();
if (bundle == null) return json;
for (String key : bundle.keySet()) {
Object o = bundle.get(key);
try {
if (o instanceof Bundle b) {
json.put(key, bundleToJson(b));
} else if (o instanceof Integer || o instanceof Long || o instanceof Boolean) {
json.put(key, o);
} else if (o == null) {
json.put(key, JSONObject.NULL);
} else {
json.put(key, o.toString());
}
} catch (JSONException e) {
// Ok, only used for logging.
}
}
return json;
}
/*
* Logging for page load metrics callback, if service has enabled logging.
*
* No rate-limiting, can be spammy if the app is misbehaved.
*
* @param args arguments of the callback.
*/
void logPageLoadMetricsCallback(Bundle args) {
if (!mLogRequests) return; // Don't build args if not necessary.
logCallback(
"extraCallback(" + PAGE_LOAD_METRICS_CALLBACK + ")", bundleToJson(args).toString());
}
/** Sets a callback to be triggered when a service connection is terminated. */
public void setDisconnectCallback(@Nullable Callback<CustomTabsSessionToken> callback) {
mDisconnectCallback = callback;
}
public boolean newSession(CustomTabsSessionToken session) {
boolean success = newSessionInternal(session);
logCall("newSession()", success);
return success;
}
private boolean newSessionInternal(CustomTabsSessionToken session) {
if (session == null) return false;
ClientManager.DisconnectCallback onDisconnect =
new ClientManager.DisconnectCallback() {
@Override
public void run(CustomTabsSessionToken session) {
cancelSpeculation(session);
if (mDisconnectCallback != null) {
mDisconnectCallback.onResult(session);
}
// TODO(pshmakov): invert this dependency by moving event dispatching to a
// separate class.
ChromeApplicationImpl.getComponent()
.resolveCustomTabsFileProcessor()
.onSessionDisconnected(session);
}
};
// TODO(peconn): Make this not an anonymous class once PostMessageServiceConnection is made
// non-abstract in AndroidX.
PostMessageServiceConnection serviceConnection =
new PostMessageServiceConnection(session) {};
PostMessageHandler handler = new PostMessageHandler(serviceConnection);
var engagementSignalsHandler = new EngagementSignalsHandler(this, session);
return mClientManager.newSession(
session,
Binder.getCallingUid(),
onDisconnect,
handler,
serviceConnection,
engagementSignalsHandler);
}
/**
* Overrides the given session's packageName if it is generated by Chrome. To be used for
* testing only. To be called before the session given is associated with a tab.
* @param session The session for which the package name should be overridden.
* @param packageName The new package name to set.
*/
public void overridePackageNameForSessionForTesting(
CustomTabsSessionToken session, String packageName) {
String originalPackage = getClientPackageNameForSession(session);
String selfPackage = ContextUtils.getApplicationContext().getPackageName();
if (TextUtils.isEmpty(originalPackage) || !selfPackage.equals(originalPackage)) return;
mClientManager.overridePackageNameForSessionForTesting(session, packageName); // IN-TEST
}
/** Warmup activities that should only happen once. */
private static void initializeBrowser(final Context context) {
ThreadUtils.assertOnUiThread();
ChromeBrowserInitializer.getInstance().handleSynchronousStartupWithGpuWarmUp();
ChildProcessLauncherHelper.warmUpOnAnyThread(context, true);
}
public boolean warmup(long flags) {
try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.warmup")) {
boolean success = warmupInternal(true);
logCall("warmup()", success);
return success;
}
}
/**
* @return Whether {@link CustomTabsConnection#warmup(long)} has been called.
*/
public boolean hasWarmUpBeenFinished() {
return mWarmupHasBeenFinished.get();
}
/**
* Starts as much as possible in anticipation of a future navigation.
*
* @param mayCreateSpareWebContents true if warmup() can create a spare renderer.
* @return true for success.
*/
private boolean warmupInternal(final boolean mayCreateSpareWebContents) {
// Here and in mayLaunchUrl(), don't do expensive work for background applications.
if (!isCallerForegroundOrSelf()) return false;
int uid = Binder.getCallingUid();
mClientManager.recordUidHasCalledWarmup(uid);
final boolean initialized = !mWarmupHasBeenCalled.compareAndSet(false, true);
// The call is non-blocking and this must execute on the UI thread, post chained tasks.
ChainedTasks tasks = new ChainedTasks();
// Ordering of actions here:
// 1. Initializing the browser needs to be done once, and first.
// 2. Creating a spare renderer takes time, in other threads and processes, so start it
// sooner rather than later. Can be done several times.
// 3. UI inflation has to be done for any new activity.
// 4. Initializing the LoadingPredictor is done once, and triggers work on other threads,
// start it early.
// 5. RequestThrottler first access has to be done only once.
// (1)
if (!initialized) {
tasks.add(
TaskTraits.UI_DEFAULT,
() -> {
try (TraceEvent e =
TraceEvent.scoped("CustomTabsConnection.initializeBrowser()")) {
initializeBrowser(ContextUtils.getApplicationContext());
ProcessInitializationHandler.getInstance().initNetworkChangeNotifier();
mWarmupHasBeenFinished.set(true);
}
});
}
// (2)
if (mayCreateSpareWebContents && !mHiddenTabHolder.hasHiddenTab()) {
tasks.add(
TaskTraits.UI_DEFAULT,
() -> {
// Temporary fix for https://crbug.com/797832.
// TODO(lizeb): Properly fix instead of papering over the bug, this code
// should not be scheduled unless startup is done. See
// https://crbug.com/797832.
if (!BrowserStartupController.getInstance().isFullBrowserStarted()) return;
try (TraceEvent e = TraceEvent.scoped("CreateSpareWebContents")) {
createSpareWebContents(ProfileManager.getLastUsedRegularProfile());
}
});
}
// (3)
tasks.add(
TaskTraits.UI_DEFAULT,
() -> {
try (TraceEvent e = TraceEvent.scoped("InitializeViewHierarchy")) {
WarmupManager.getInstance()
.initializeViewHierarchy(
ContextUtils.getApplicationContext(),
R.layout.custom_tabs_control_container,
R.layout.custom_tabs_toolbar);
}
});
if (!initialized) {
tasks.add(
TaskTraits.UI_DEFAULT,
() -> {
try (TraceEvent e =
TraceEvent.scoped("WarmupInternalFinishInitialization")) {
// (4)
Profile profile = ProfileManager.getLastUsedRegularProfile();
WarmupManager.startPreconnectPredictorInitialization(profile);
// (5) The throttling database uses shared preferences, that can cause
// a StrictMode violation on the first access. Make sure that this
// access is not in mayLauchUrl.
RequestThrottler.loadInBackground();
}
});
}
tasks.add(TaskTraits.UI_DEFAULT, () -> notifyWarmupIsDone(uid));
tasks.start(false);
mWarmupTasks = tasks;
return true;
}
/** @return the URL or null if it's invalid. */
private static boolean isValid(Uri uri) {
if (uri == null) return false;
// Don't do anything for unknown schemes. Not having a scheme is allowed, as we allow
// "www.example.com".
String scheme = uri.normalizeScheme().getScheme();
boolean allowedScheme =
scheme == null
|| scheme.equals(UrlConstants.HTTP_SCHEME)
|| scheme.equals(UrlConstants.HTTPS_SCHEME);
return allowedScheme;
}
/**
* High confidence mayLaunchUrl() call, that is:
* - Tries to speculate if possible.
* - An empty URL cancels the current prerender if any.
* - Start a spare renderer if necessary.
*/
private void highConfidenceMayLaunchUrl(
CustomTabsSessionToken session,
int uid,
String url,
Bundle extras,
List<Bundle> otherLikelyBundles) {
ThreadUtils.assertOnUiThread();
if (TextUtils.isEmpty(url)) {
cancelSpeculation(session);
return;
}
if (maySpeculate(session)) {
// `IntentHandler.hasAnyIncognitoExtra` check:
// Hidden tabs are created always with regular profile, so we need to block hidden tab
// creation in incognito mode not to have inconsistent modes between tab model and
// hidden tab. (crbug.com/1190971)
// The incognito check is already performed in the entrypoint
// `mayLaunchUrlInternal`,
// but also performed here to be safe against future callers.
// Read the discussion at
// https://chromium-review.googlesource.com/c/chromium/src/+/5004377/comment/02cf16f4_82578ace/
boolean canUseHiddenTab =
mClientManager.getCanUseHiddenTab(session)
&& !IntentHandler.hasAnyIncognitoExtra(extras);
boolean useSeparateStoragePartitionForExperiment =
ChromeFeatureList.isEnabled(
ChromeFeatureList.MAYLAUNCHURL_USES_SEPARATE_STORAGE_PARTITION);
startSpeculation(
session,
url,
canUseHiddenTab,
extras,
uid,
useSeparateStoragePartitionForExperiment);
}
preconnectUrls(otherLikelyBundles);
}
/**
* Low confidence mayLaunchUrl() call, that is:
* - Preconnects to the ordered list of URLs.
* - Makes sure that there is a spare renderer.
*/
@VisibleForTesting
boolean lowConfidenceMayLaunchUrl(List<Bundle> likelyBundles) {
ThreadUtils.assertOnUiThread();
if (!preconnectUrls(likelyBundles)) return false;
createSpareWebContents(ProfileManager.getLastUsedRegularProfile());
return true;
}
public Tab getHiddenTabForTesting() {
return mHiddenTabHolder != null ? mHiddenTabHolder.getHiddenTabForTesting() : null;
}
private boolean preconnectUrls(List<Bundle> likelyBundles) {
boolean atLeastOneUrl = false;
if (likelyBundles == null) return false;
WarmupManager warmupManager = WarmupManager.getInstance();
Profile profile = ProfileManager.getLastUsedRegularProfile();
for (Bundle bundle : likelyBundles) {
Uri uri;
try {
uri = IntentUtils.safeGetParcelable(bundle, CustomTabsService.KEY_URL);
} catch (ClassCastException e) {
continue;
}
if (isValid(uri)) {
warmupManager.maybePreconnectUrlAndSubResources(profile, uri.toString());
atLeastOneUrl = true;
}
}
return atLeastOneUrl;
}
public boolean mayLaunchUrl(
CustomTabsSessionToken session,
Uri url,
Bundle extras,
List<Bundle> otherLikelyBundles) {
try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.mayLaunchUrl")) {
boolean success = mayLaunchUrlInternal(session, url, extras, otherLikelyBundles);
logCall("mayLaunchUrl(" + url + ")", success);
return success;
}
}
private boolean mayLaunchUrlInternal(
final CustomTabsSessionToken session,
final Uri url,
final Bundle extras,
final List<Bundle> otherLikelyBundles) {
// mayLaunchUrl should not be executed for Incognito CCT since all setup is created with
// regular profile. If we need to enable mayLaunchUrl for off-the-record profiles, we need
// to update the profile used. Please see crbug.com/1106757.
if (IntentHandler.hasAnyIncognitoExtra(extras)) return false;
final boolean lowConfidence =
(url == null || TextUtils.isEmpty(url.toString())) && otherLikelyBundles != null;
final String urlString = isValid(url) ? url.toString() : null;
if (url != null && urlString == null && !lowConfidence) return false;
final int uid = Binder.getCallingUid();
// Things below need the browser process to be initialized.
// Forbids warmup() from creating a spare renderer, as prerendering wouldn't reuse
// it. Checking whether prerendering is enabled requires the native library to be loaded,
// which is not necessarily the case yet.
if (!warmupInternal(false)) return false; // Also does the foreground check.
if (!mClientManager.updateStatsAndReturnWhetherAllowed(
session, uid, urlString, otherLikelyBundles != null)) {
return false;
}
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
doMayLaunchUrlOnUiThread(
lowConfidence,
session,
uid,
urlString,
extras,
otherLikelyBundles,
true);
});
return true;
}
@androidx.browser.customtabs.ExperimentalPrefetch
public boolean prefetch(
CustomTabsSessionToken session, Uri uri, @Nullable PrefetchOptions options) {
try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.prefetch")) {
if (!ChromeFeatureList.sPrefetchBrowserInitiatedTriggers.isEnabled()
|| !ChromeFeatureList.sCctNavigationalPrefetch.isEnabled()) {
return false;
}
return prefetchInternal(session, uri, options);
}
}
@androidx.browser.customtabs.ExperimentalPrefetch
private boolean prefetchInternal(
CustomTabsSessionToken session, Uri uri, PrefetchOptions options) {
String uriString = isValid(uri) ? uri.toString() : null;
if (uriString == null) return false;
boolean usePrefetchProxy = options.requiresAnonymousIpWhenCrossOrigin;
String verifiedSourceOrigin =
verifySourceOriginOfPrefetch(session, options.sourceOrigin)
? options.sourceOrigin.toString()
: null;
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
WarmupManager.getInstance()
.startPrefetchFromCCT(
uriString, usePrefetchProxy, verifiedSourceOrigin);
});
return true;
}
@VisibleForTesting
@androidx.browser.customtabs.ExperimentalPrefetch
boolean verifySourceOriginOfPrefetch(
CustomTabsSessionToken session, @Nullable Uri rawSourceOrigin) {
if (rawSourceOrigin == null) return false;
String sourceOriginString = rawSourceOrigin.toString();
Origin sourceOrigin = Origin.create(sourceOriginString);
return sourceOrigin != null
&& mClientManager.isFirstPartyOriginForSession(session, sourceOrigin);
}
private void enableExperimentIdsIfNecessary(Bundle extras) {
ThreadUtils.assertOnUiThread();
if (extras == null) return;
int[] experimentIds =
IntentUtils.safeGetIntArray(extras, CustomTabIntentDataProvider.EXPERIMENT_IDS);
if (experimentIds == null) return;
// When ids are set through cct, they should not override existing ids.
boolean override = false;
UmaSessionStats.registerExternalExperiment(experimentIds, override);
}
private void doMayLaunchUrlOnUiThread(
final boolean lowConfidence,
final CustomTabsSessionToken session,
final int uid,
final String urlString,
final Bundle extras,
final List<Bundle> otherLikelyBundles,
boolean retryIfNotLoaded) {
ThreadUtils.assertOnUiThread();
try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.mayLaunchUrlOnUiThread")) {
// doMayLaunchUrlInternal() is always called once the native level initialization is
// done, at least the initial profile load. However, at that stage the startup callback
// may not have run, which causes ProfileManager.getLastUsedRegularProfile() to throw an
// exception. But the tasks have been posted by then, so reschedule ourselves, only
// once.
if (!BrowserStartupController.getInstance().isFullBrowserStarted()) {
if (retryIfNotLoaded) {
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
doMayLaunchUrlOnUiThread(
lowConfidence,
session,
uid,
urlString,
extras,
otherLikelyBundles,
false);
});
}
return;
}
enableExperimentIdsIfNecessary(extras);
if (lowConfidence) {
lowConfidenceMayLaunchUrl(otherLikelyBundles);
} else {
highConfidenceMayLaunchUrl(session, uid, urlString, extras, otherLikelyBundles);
}
}
}
/**
* Sends a command that isn't part of the API yet.
*
* @param commandName Name of the extra command to execute.
* @param args Arguments for the command.
* @return The result {@link Bundle}, or null.
*/
public @Nullable Bundle extraCommand(String commandName, Bundle args) {
if (commandName.equals(IS_EPHEMERAL_BROWSING_SUPPORTED)) {
var bundle = new Bundle();
bundle.putBoolean(
EPHEMERAL_BROWSING_SUPPORTED_KEY,
ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_EPHEMERAL_MODE));
return bundle;
}
return null;
}
public boolean updateVisuals(final CustomTabsSessionToken session, Bundle bundle) {
if (mLogRequests) Log.w(TAG, "updateVisuals: %s", bundleToJson(bundle));
SessionHandler handler = mSessionDataHolder.getActiveHandler(session);
if (handler == null) return false;
final Bundle actionButtonBundle =
IntentUtils.safeGetBundle(bundle, CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE);
boolean result = true;
List<Integer> ids = new ArrayList<>();
List<String> descriptions = new ArrayList<>();
List<Bitmap> icons = new ArrayList<>();
if (actionButtonBundle != null) {
int id =
IntentUtils.safeGetInt(
actionButtonBundle,
CustomTabsIntent.KEY_ID,
CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID);
Bitmap bitmap = CustomButtonParamsImpl.parseBitmapFromBundle(actionButtonBundle);
String description =
CustomButtonParamsImpl.parseDescriptionFromBundle(actionButtonBundle);
if (bitmap != null && description != null) {
ids.add(id);
descriptions.add(description);
icons.add(bitmap);
}
}
List<Bundle> bundleList =
IntentUtils.safeGetParcelableArrayList(
bundle, CustomTabsIntent.EXTRA_TOOLBAR_ITEMS);
if (bundleList != null) {
for (Bundle toolbarItemBundle : bundleList) {
int id =
IntentUtils.safeGetInt(
toolbarItemBundle,
CustomTabsIntent.KEY_ID,
CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID);
if (ids.contains(id)) continue;
Bitmap bitmap = CustomButtonParamsImpl.parseBitmapFromBundle(toolbarItemBundle);
if (bitmap == null) continue;
String description =
CustomButtonParamsImpl.parseDescriptionFromBundle(toolbarItemBundle);
if (description == null) continue;
ids.add(id);
descriptions.add(description);
icons.add(bitmap);
}
}
if (!ids.isEmpty()) {
result &=
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() -> {
boolean res = true;
for (int i = 0; i < ids.size(); i++) {
res &=
handler.updateCustomButton(
ids.get(i), icons.get(i), descriptions.get(i));
}
return res;
});
}
if (bundle.containsKey(CustomTabsIntent.EXTRA_REMOTEVIEWS)) {
final RemoteViews remoteViews =
IntentUtils.safeGetParcelable(bundle, CustomTabsIntent.EXTRA_REMOTEVIEWS);
final int[] clickableIDs =
IntentUtils.safeGetIntArray(
bundle, CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS);
final PendingIntent pendingIntent =
IntentUtils.safeGetParcelable(
bundle, CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT);
result &=
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() ->
handler.updateRemoteViews(
remoteViews, clickableIDs, pendingIntent));
}
PendingIntent pendingIntent = getSecondarySwipeToolbarSwipeUpGesture(bundle);
if (pendingIntent != null) {
result &=
PostTask.runSynchronously(
TaskTraits.UI_DEFAULT,
() ->
handler.updateSecondaryToolbarSwipeUpPendingIntent(
pendingIntent));
}
logCall("updateVisuals()", result);
return result;
}
private static PendingIntent getSecondarySwipeToolbarSwipeUpGesture(Bundle bundle) {
PendingIntent pendingIntent =
IntentUtils.safeGetParcelable(
bundle, CustomTabsIntent.EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_GESTURE);
if (pendingIntent == null) {
pendingIntent =
IntentUtils.safeGetParcelable(
bundle,
CustomTabIntentDataProvider.EXTRA_SECONDARY_TOOLBAR_SWIPE_UP_ACTION);
}
return pendingIntent;
}
public boolean requestPostMessageChannel(
CustomTabsSessionToken session,
Origin postMessageSourceOrigin,
@Nullable Origin postMessageTargetOrigin) {
boolean success =
requestPostMessageChannelInternal(
session, postMessageSourceOrigin, postMessageTargetOrigin);
logCall(
"requestPostMessageChannel() with origin "
+ (postMessageSourceOrigin != null
? postMessageSourceOrigin.toString()
: ""),
success);
RecordHistogram.recordBooleanHistogram(
"CustomTabs.PostMessage.RequestPostMessageChannel", success);
return success;
}
private boolean requestPostMessageChannelInternal(
final CustomTabsSessionToken session,
final Origin postMessageOrigin,
@Nullable Origin postMessageTargetOrigin) {
if (!mWarmupHasBeenCalled.get()) return false;
if (!isCallerForegroundOrSelf() && !mSessionDataHolder.isActiveSession(session)) {
return false;
}
if (!mClientManager.bindToPostMessageServiceForSession(session)) return false;
final int uid = Binder.getCallingUid();
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
// Attempt to verify origin synchronously. If successful directly initialize
// postMessage channel for session.
Uri verifiedOrigin = verifyOriginForSession(session, uid, postMessageOrigin);
if (verifiedOrigin == null) {
mClientManager.verifyAndInitializeWithPostMessageOriginForSession(
session,
postMessageOrigin,
postMessageTargetOrigin,
CustomTabsService.RELATION_USE_AS_ORIGIN);
} else {
mClientManager.initializeWithPostMessageOriginForSession(
session,
verifiedOrigin,
postMessageTargetOrigin != null
? postMessageTargetOrigin.uri()
: null);
}
});
return true;
}
/**
* Acquire the origin for the client that owns the given session.
* @param session The session to use for getting client information.
* @param clientUid The UID for the client controlling the session.
* @param origin The origin that is suggested by the client. The validated origin may be this or
* a derivative of this.
* @return The validated origin {@link Uri} for the given session's client.
*/
protected Uri verifyOriginForSession(
CustomTabsSessionToken session, int clientUid, Origin origin) {
if (clientUid == Process.myUid()) return Uri.EMPTY;
return null;
}
/**
* Returns whether an intent is first-party with respect to its session, that is if the
* application linked to the session has a relation with the provided origin.
*
* @param intent The intent to verify.
*/
public boolean isFirstPartyOriginForIntent(Intent intent) {
CustomTabsSessionToken session = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
if (session == null) return false;
Origin origin = Origin.create(intent.getData());
if (origin == null) return false;
return mClientManager.isFirstPartyOriginForSession(session, origin);
}
public int postMessage(CustomTabsSessionToken session, String message, Bundle extras) {
int result;
if (!mWarmupHasBeenCalled.get()) result = CustomTabsService.RESULT_FAILURE_DISALLOWED;
if (!isCallerForegroundOrSelf() && !mSessionDataHolder.isActiveSession(session)) {
result = CustomTabsService.RESULT_FAILURE_DISALLOWED;
}
// If called before a validatePostMessageOrigin, the post message origin will be invalid and
// will return a failure result here.
result = mClientManager.postMessage(session, message);
logCall("postMessage", result);
return result;
}
public boolean validateRelationship(
CustomTabsSessionToken sessionToken, int relation, Origin origin, Bundle extras) {
// Essential parts of the verification will depend on native code and will be run sync on UI
// thread. Make sure the client has called warmup() beforehand.
if (!mWarmupHasBeenCalled.get()) {
Log.d(TAG, "Verification failed due to warmup not having been previously called.");
mClientManager
.getCallbackForSession(sessionToken)
.onRelationshipValidationResult(
relation, Uri.parse(origin.toString()), false, null);
return false;
}
return mClientManager.validateRelationship(sessionToken, relation, origin, extras);
}
/**
* See
* {@link ClientManager#resetPostMessageHandlerForSession(CustomTabsSessionToken, WebContents)}.
*/
public void resetPostMessageHandlerForSession(
CustomTabsSessionToken session, WebContents webContents) {
mClientManager.resetPostMessageHandlerForSession(session, webContents);
}
/**
* Registers a launch of a |url| for a given |session|.
*
* This is used for accounting.
*/
void registerLaunch(CustomTabsSessionToken session, String url) {
mClientManager.registerLaunch(session, url);
}
public @Nullable String getSpeculatedUrl(CustomTabsSessionToken session) {
return mHiddenTabHolder.getSpeculatedUrl(session);
}
/**
* Returns the preloaded {@link Tab} if it matches the given |url| and |referrer|. Null if no
* such {@link Tab}. If a {@link Tab} is preloaded but it does not match, it is discarded.
*
* @param session The Binder object identifying a session.
* @param url The URL the tab is for.
* @param referrer The referrer to use for |url|.
* @return The hidden tab, or null.
*/
public @Nullable Tab takeHiddenTab(
@Nullable CustomTabsSessionToken session, String url, @Nullable String referrer) {
return mHiddenTabHolder.takeHiddenTab(
session, mClientManager.getIgnoreFragmentsForSession(session), url, referrer);
}
/**
* Called when an intent is handled by either an existing or a new CustomTabActivity.
*
* @param session Session extracted from the intent.
* @param intent incoming intent.
*/
public void onHandledIntent(CustomTabsSessionToken session, Intent intent) {
String url = IntentHandler.getUrlFromIntent(intent);
if (TextUtils.isEmpty(url)) {
return;
}
if (mLogRequests) {
Log.w(
TAG,
"onHandledIntent, URL: %s, extras: %s",
url,
bundleToJson(intent.getExtras()));
}
// If we still have pending warmup tasks, don't continue as they would only delay intent
// processing from now on.
if (mWarmupTasks != null) mWarmupTasks.cancel();
maybePreconnectToRedirectEndpoint(session, url, intent);
ChromeBrowserInitializer.getInstance()
.runNowOrAfterFullBrowserStarted(() -> handleParallelRequest(session, intent));
maybePrefetchResources(session, intent);
}
/**
* Called each time a CCT tab is created to check if a client data header was set and if so
* forward it along to the native side.
* @param session Session identifier.
* @param webContents the WebContents of the new tab.
*/
public void setClientDataHeaderForNewTab(
CustomTabsSessionToken session, WebContents webContents) {}
protected void setClientDataHeader(WebContents webContents, String header) {
if (TextUtils.isEmpty(header)) return;
CustomTabsConnectionJni.get().setClientDataHeader(webContents, header);
}
private void maybePreconnectToRedirectEndpoint(
CustomTabsSessionToken session, String url, Intent intent) {
// For the preconnection to not be a no-op, we need more than just the native library.
if (!ChromeBrowserInitializer.getInstance().isFullBrowserInitialized()) {
return;
}
// Conditions:
// - There is a valid redirect endpoint.
// - The URL's origin is first party with respect to the app.
Uri redirectEndpoint = intent.getParcelableExtra(REDIRECT_ENDPOINT_KEY);
if (redirectEndpoint == null || !isValid(redirectEndpoint)) return;
Origin origin = Origin.create(url);
if (origin == null) return;
if (!mClientManager.isFirstPartyOriginForSession(session, origin)) return;
WarmupManager.getInstance()
.maybePreconnectUrlAndSubResources(
ProfileManager.getLastUsedRegularProfile(), redirectEndpoint.toString());
}
@VisibleForTesting
@ParallelRequestStatus
int handleParallelRequest(CustomTabsSessionToken session, Intent intent) {
int status = maybeStartParallelRequest(session, intent);
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.ParallelRequestStatusOnStart",
status,
ParallelRequestStatus.NUM_ENTRIES);
if (mLogRequests) {
Log.w(TAG, "handleParallelRequest() = " + PARALLEL_REQUEST_MESSAGES[status]);
}
if ((status != ParallelRequestStatus.NO_REQUEST)
&& (status != ParallelRequestStatus.FAILURE_NOT_INITIALIZED)
&& (status != ParallelRequestStatus.FAILURE_NOT_AUTHORIZED)
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.CCT_REPORT_PARALLEL_REQUEST_STATUS)) {
Bundle args = new Bundle();
Uri url = intent.getParcelableExtra(PARALLEL_REQUEST_URL_KEY);
args.putParcelable("url", url);
args.putInt("status", status);
safeExtraCallback(session, ON_DETACHED_REQUEST_REQUESTED, args);
if (mLogRequests) {
logCallback(ON_DETACHED_REQUEST_REQUESTED, bundleToJson(args).toString());
}
}
return status;
}
/**
* Maybe starts a parallel request.
*
* @param session Calling context session.
* @param intent Incoming intent with the extras.
* @return Whether the request was started, with reason in case of failure.
*/
private @ParallelRequestStatus int maybeStartParallelRequest(
CustomTabsSessionToken session, Intent intent) {
ThreadUtils.assertOnUiThread();
if (!intent.hasExtra(PARALLEL_REQUEST_URL_KEY)) return ParallelRequestStatus.NO_REQUEST;
if (!ChromeBrowserInitializer.getInstance().isFullBrowserInitialized()) {
return ParallelRequestStatus.FAILURE_NOT_INITIALIZED;
}
if (!mClientManager.getAllowParallelRequestForSession(session)) {
return ParallelRequestStatus.FAILURE_NOT_AUTHORIZED;
}
Uri referrer = intent.getParcelableExtra(PARALLEL_REQUEST_REFERRER_KEY);
Uri url = intent.getParcelableExtra(PARALLEL_REQUEST_URL_KEY);
int policy =
intent.getIntExtra(PARALLEL_REQUEST_REFERRER_POLICY_KEY, ReferrerPolicy.DEFAULT);
if (url == null) return ParallelRequestStatus.FAILURE_INVALID_URL;
if (referrer == null) return ParallelRequestStatus.FAILURE_INVALID_REFERRER;
if (policy < ReferrerPolicy.MIN_VALUE || policy > ReferrerPolicy.MAX_VALUE) {
policy = ReferrerPolicy.DEFAULT;
}
if (url.toString().equals("") || !isValid(url)) {
return ParallelRequestStatus.FAILURE_INVALID_URL;
}
if (!canDoParallelRequest(session, referrer)) {
return ParallelRequestStatus.FAILURE_INVALID_REFERRER_FOR_SESSION;
}
String urlString = url.toString();
String referrerString = referrer.toString();
String packageName = mClientManager.getClientPackageNameForSession(session);
CustomTabsConnectionJni.get()
.createAndStartDetachedResourceRequest(
ProfileManager.getLastUsedRegularProfile(),
session,
packageName,
urlString,
referrerString,
policy,
DetachedResourceRequestMotivation.PARALLEL_REQUEST);
if (mLogRequests) {
Log.w(TAG, "startParallelRequest(%s, %s, %d)", urlString, referrerString, policy);
}
return ParallelRequestStatus.SUCCESS;
}
/**
* Maybe starts a resource prefetch.
*
* @param session Calling context session.
* @param intent Incoming intent with the extras.
* @return Number of prefetch requests that have been sent.
*/
@VisibleForTesting
int maybePrefetchResources(CustomTabsSessionToken session, Intent intent) {
ThreadUtils.assertOnUiThread();
if (!mClientManager.getAllowResourcePrefetchForSession(session)) return 0;
List<Uri> resourceList = intent.getParcelableArrayListExtra(RESOURCE_PREFETCH_URL_LIST_KEY);
Uri referrer = intent.getParcelableExtra(PARALLEL_REQUEST_REFERRER_KEY);
int policy =
intent.getIntExtra(PARALLEL_REQUEST_REFERRER_POLICY_KEY, ReferrerPolicy.DEFAULT);
if (resourceList == null || referrer == null) return 0;
if (policy < 0 || policy > ReferrerPolicy.MAX_VALUE) policy = ReferrerPolicy.DEFAULT;
Origin origin = Origin.create(referrer);
if (origin == null) return 0;
if (!mClientManager.isFirstPartyOriginForSession(session, origin)) return 0;
String referrerString = referrer.toString();
int requestsSent = 0;
for (Uri url : resourceList) {
String urlString = url.toString();
if (urlString.isEmpty() || !isValid(url)) continue;
// Session is null because we don't need completion notifications.
CustomTabsConnectionJni.get()
.createAndStartDetachedResourceRequest(
ProfileManager.getLastUsedRegularProfile(),
null,
null,
urlString,
referrerString,
policy,
DetachedResourceRequestMotivation.RESOURCE_PREFETCH);
++requestsSent;
if (mLogRequests) {
Log.w(TAG, "startResourcePrefetch(%s, %s, %d)", urlString, referrerString, policy);
}
}
return requestsSent;
}
/**
* @return Whether {@code session} can create a parallel request for a given
* {@code referrer}.
*/
@VisibleForTesting
boolean canDoParallelRequest(CustomTabsSessionToken session, Uri referrer) {
ThreadUtils.assertOnUiThread();
Origin origin = Origin.create(referrer);
if (origin == null) return false;
return mClientManager.isFirstPartyOriginForSession(session, origin);
}
/** @see ClientManager#shouldHideDomainForSession(CustomTabsSessionToken) */
public boolean shouldHideDomainForSession(CustomTabsSessionToken session) {
return mClientManager.shouldHideDomainForSession(session);
}
/** @see ClientManager#shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken) */
public boolean shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session) {
return mClientManager.shouldSpeculateLoadOnCellularForSession(session);
}
/** @see ClientManager#getCanUseHiddenTab(CustomTabsSessionToken) */
public boolean canUseHiddenTabForSession(CustomTabsSessionToken session) {
return mClientManager.getCanUseHiddenTab(session);
}
/** @see ClientManager#shouldSendNavigationInfoForSession(CustomTabsSessionToken) */
public boolean shouldSendNavigationInfoForSession(CustomTabsSessionToken session) {
return mClientManager.shouldSendNavigationInfoForSession(session);
}
/** @see ClientManager#shouldSendBottomBarScrollStateForSession(CustomTabsSessionToken) */
public boolean shouldSendBottomBarScrollStateForSession(CustomTabsSessionToken session) {
return mClientManager.shouldSendBottomBarScrollStateForSession(session);
}
/** See {@link ClientManager#getClientPackageNameForSession(CustomTabsSessionToken)} */
public String getClientPackageNameForSession(CustomTabsSessionToken session) {
return mClientManager.getClientPackageNameForSession(session);
}
/**
* @return Whether the given package name is that of a first-party application.
*/
public boolean isFirstParty(String packageName) {
if (packageName == null) return false;
return ChromeApplicationImpl.getComponent()
.resolveExternalAuthUtils()
.isGoogleSigned(packageName);
}
void setIgnoreUrlFragmentsForSession(CustomTabsSessionToken session, boolean value) {
mClientManager.setIgnoreFragmentsForSession(session, value);
}
@VisibleForTesting
boolean getIgnoreUrlFragmentsForSession(CustomTabsSessionToken session) {
return mClientManager.getIgnoreFragmentsForSession(session);
}
@VisibleForTesting
void setShouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session, boolean value) {
mClientManager.setSpeculateLoadOnCellularForSession(session, value);
}
@VisibleForTesting
public void setCanUseHiddenTabForSession(CustomTabsSessionToken session, boolean value) {
mClientManager.setCanUseHiddenTab(session, value);
}
/**
* See {@link ClientManager#setSendNavigationInfoForSession(CustomTabsSessionToken, boolean)}.
*/
void setSendNavigationInfoForSession(CustomTabsSessionToken session, boolean send) {
mClientManager.setSendNavigationInfoForSession(session, send);
}
/**
* Shows a toast about any possible sign in issues encountered during custom tab startup.
* @param session The session that corresponding custom tab is assigned.
* @param intent The intent that launched the custom tab.
*/
void showSignInToastIfNecessary(CustomTabsSessionToken session, Intent intent) {}
/**
* Sends a callback using {@link CustomTabsCallback} with the first run result if necessary.
* @param intentExtras The extras for the initial VIEW intent that initiated first run.
* @param resultOK Whether first run was successful.
*/
public void sendFirstRunCallbackIfNecessary(Bundle intentExtras, boolean resultOK) {}
/**
* Sends the navigation info that was captured to the client callback.
* @param session The session to use for getting client callback.
* @param url The current url for the tab.
* @param title The current title for the tab.
* @param snapshotPath Uri location for screenshot of the tab contents which is publicly
* available for sharing.
*/
public void sendNavigationInfo(
CustomTabsSessionToken session, String url, String title, Uri snapshotPath) {}
/**
* Called when the bottom bar for the custom tab has been hidden or shown completely by user
* scroll.
*
* @param session The session that is linked with the custom tab.
* @param hidden Whether the bottom bar is hidden or shown.
*/
public void onBottomBarScrollStateChanged(CustomTabsSessionToken session, boolean hidden) {
Bundle args = new Bundle();
args.putBoolean("hidden", hidden);
if (safeExtraCallback(session, BOTTOM_BAR_SCROLL_STATE_CALLBACK, args) && mLogRequests) {
logCallback("extraCallback(" + BOTTOM_BAR_SCROLL_STATE_CALLBACK + ")", hidden);
}
}
/** Called when a resizable Custom Tab is resized. */
public void onResized(@Nullable CustomTabsSessionToken session, int height, int width) {
Bundle args = new Bundle();
if (height != mPrevHeight) {
args.putInt(ON_RESIZED_SIZE_EXTRA, height);
// TODO(crbug.com/40867201): Deprecate the extra callback.
if (safeExtraCallback(session, ON_RESIZED_CALLBACK, args) && mLogRequests) {
logCallback("extraCallback(" + ON_RESIZED_CALLBACK + ")", args);
}
mPrevHeight = height;
}
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return;
try {
callback.onActivityResized(height, width, args);
} catch (Exception e) {
// Catching all exceptions is really bad, but we need it here,
// because Android exposes us to client bugs by throwing a variety
// of exceptions. See crbug.com/517023.
return;
}
logCallback("onActivityResized()", "(" + height + "x" + width + ")");
}
/** Called when a Custom Tab is unminimized. */
@OptIn(markerClass = ExperimentalMinimizationCallback.class)
public void onUnminimized(@Nullable CustomTabsSessionToken session) {
Bundle args = new Bundle();
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return;
try {
callback.onUnminimized(args);
} catch (Exception e) {
// Catching all exceptions is really bad, but we need it here,
// because Android exposes us to client bugs by throwing a variety
// of exceptions. See crbug.com/517023.
return;
}
logCallback("onUnminimized()", args);
}
/** Called when a Custom Tab is minimized. */
@OptIn(markerClass = ExperimentalMinimizationCallback.class)
public void onMinimized(@Nullable CustomTabsSessionToken session) {
Bundle args = new Bundle();
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return;
try {
callback.onMinimized(args);
} catch (Exception e) {
// Catching all exceptions is really bad, but we need it here,
// because Android exposes us to client bugs by throwing a variety
// of exceptions. See crbug.com/517023.
return;
}
logCallback("onMinimized()", args);
}
/**
* Called when the Custom Tab's layout has changed.
*
* @param left The left coordinate of the custom tab window in pixels
* @param top The top coordinate of the custom tab window in pixels
* @param right The right coordinate of the custom tab window in pixels
* @param bottom The bottom coordinate of the custom tab window in pixels
* @param state The current layout state in which the Custom Tab is displayed.
*/
public void onActivityLayout(
@Nullable CustomTabsSessionToken session,
int left,
int top,
int right,
int bottom,
@CustomTabsCallback.ActivityLayoutState int state) {
Bundle args = new Bundle();
args.putInt(ON_ACTIVITY_LAYOUT_LEFT_EXTRA, left);
args.putInt(ON_ACTIVITY_LAYOUT_TOP_EXTRA, top);
args.putInt(ON_ACTIVITY_LAYOUT_RIGHT_EXTRA, right);
args.putInt(ON_ACTIVITY_LAYOUT_BOTTOM_EXTRA, bottom);
args.putInt(ON_ACTIVITY_LAYOUT_STATE_EXTRA, state);
if (safeExtraCallback(session, ON_ACTIVITY_LAYOUT_CALLBACK, args) && mLogRequests) {
logCallback("extraCallback(" + ON_ACTIVITY_LAYOUT_CALLBACK + ")", args);
}
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return;
try {
callback.onActivityLayout(left, top, right, bottom, state, Bundle.EMPTY);
} catch (Exception e) {
// Catching all exceptions is really bad, but we need it here,
// because Android exposes us to client bugs by throwing a variety
// of exceptions. See crbug.com/517023.
return;
}
}
/**
* @see {@link notifyNavigationEvent(CustomTabsSessionToken, int, Optional<int>)}
*/
public boolean notifyNavigationEvent(CustomTabsSessionToken session, int navigationEvent) {
return notifyNavigationEvent(session, navigationEvent, Optional.empty());
}
/**
* Notifies the application of a navigation event.
*
* <p>Delivers the {@link CustomTabsCallback#onNavigationEvent} callback to the application.
*
* @param session The Binder object identifying the session.
* @param navigationEvent The navigation event code, defined in {@link CustomTabsCallback}
* @param errorCode Network error code. Empty if there was no error or the error code is not in
* the list of error codes that should be passed to the embedder.
* @return true for success.
*/
public boolean notifyNavigationEvent(
CustomTabsSessionToken session, int navigationEvent, Optional<Integer> errorCode) {
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return false;
try {
Bundle extra = getExtrasBundleForNavigationEventForSession(session);
if (errorCode.isPresent()) extra.putInt("navigationEventErrorCode", errorCode.get());
callback.onNavigationEvent(navigationEvent, extra);
} catch (Exception e) {
// Catching all exceptions is really bad, but we need it here,
// because Android exposes us to client bugs by throwing a variety
// of exceptions. See crbug.com/517023.
return false;
}
logCallback("onNavigationEvent()", navigationEvent);
return true;
}
/** Resets dynamic experiment features that can be enabled/disabled via an Intent. */
@VisibleForTesting
void resetDynamicFeatures() {
mDynamicEnabledFeatures = null;
mDynamicDisabledFeatures = null;
}
/**
* Does setup of dynamic experiment features that can be enabled/disabled via an Intent.
*
* @param intent The {@link Intent} that is active, to be scanned for enable/disable Extras.
* @return Whether the setup will actually change the active feature set.
*/
boolean setupDynamicFeatures(Intent intent) {
CustomTabsSessionToken session = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
if (!mIsDynamicIntentFeatureOverridesEnabled
|| (!CustomTabIntentDataProvider.isTrustedCustomTab(intent, session)
&& !CommandLine.getInstance()
.hasSwitch("cct-client-firstparty-override"))) {
return false;
}
return setupDynamicFeaturesInternal(intent);
}
@VisibleForTesting
boolean setupDynamicFeaturesInternal(Intent intent) {
// TODO(crbug.com/40884078) Add support for separate dynamic experiments per session!
// Early exits if any CCT client app has already set or cleared dynamic experiments.
if (mDynamicEnabledFeatures != null || mDynamicDisabledFeatures != null) return false;
ArrayList<String> enabledExperiments =
IntentUtils.safeGetStringArrayListExtra(
intent, CustomTabIntentDataProvider.EXPERIMENTS_ENABLE);
ArrayList<String> disabledExperiments =
IntentUtils.safeGetStringArrayListExtra(
intent, CustomTabIntentDataProvider.EXPERIMENTS_DISABLE);
if (!areExperimentsSupported(enabledExperiments, disabledExperiments)) return false;
mDynamicEnabledFeatures = enabledExperiments;
mDynamicDisabledFeatures = disabledExperiments;
if (UmaSessionStats.isMetricsServiceAvailable()) {
boolean isEnabling = enabledExperiments != null;
String groupPrefix = isEnabling ? "Enable_" : "Disable_";
List<String> featuresUsed = isEnabling ? enabledExperiments : disabledExperiments;
String groupName = groupPrefix + String.join("_", featuresUsed);
UmaSessionStats.registerSyntheticFieldTrial(
SYNTHETIC_FIELDTRIAL_CCT_EXPERIMENT_OVERRIDE,
groupName,
SyntheticTrialAnnotationMode.CURRENT_LOG);
} else {
Log.w(TAG, "The Metrics Service is not available, so no synthetic field trial");
}
return true;
}
/**
* Determines whether the given enable and disable features are currently supported.
* @param enabledExperiments A list of Features to enable.
* @param disabledExperiments A list of Features to disable.
* @return Whether this set of Features is allowed to be overridden by an Intent.
*/
@VisibleForTesting
boolean areExperimentsSupported(
List<String> enabledExperiments, List<String> disabledExperiments) {
return false;
}
// TODO(crbug.com/40274032): Remove this and other dynamic feature related methods.
/**
* Determines if the given Feature is enabled after factoring in active Intent overrides.
*
* @see #setupDynamicFeatures
* @param featureName The Feature to check if it's enabled.
* @return Whether the given Feature is effectively enabled given active overrides.
*/
public boolean isDynamicFeatureEnabled(String featureName) {
if (mIsDynamicIntentFeatureOverridesEnabled) {
if (mDynamicEnabledFeatures != null && mDynamicEnabledFeatures.contains(featureName)) {
return true;
}
if (mDynamicDisabledFeatures != null
&& mDynamicDisabledFeatures.contains(featureName)) {
return false;
}
}
Log.e(TAG, "Unsupported Feature!");
return false;
}
@VisibleForTesting
void setIsDynamicFeaturesEnabled(boolean isDynamicFeaturesEnabled) {
mIsDynamicIntentFeatureOverridesEnabled = isDynamicFeaturesEnabled;
}
/**
* Returns whether the given feature is enabled with Intent overrides.
* @param featureName The feature to check.
* @return Whether the feature is enabled with Intent overrides.
*/
public boolean isDynamicFeatureEnabledWithOverrides(String featureName) {
return mDynamicEnabledFeatures != null && mDynamicEnabledFeatures.contains(featureName);
}
/**
* @return The {@link Bundle} to use as extra to
* {@link CustomTabsCallback#onNavigationEvent(int, Bundle)}
*/
protected Bundle getExtrasBundleForNavigationEventForSession(CustomTabsSessionToken session) {
// SystemClock.uptimeMillis() is used here as it (as of June 2017) uses the same system call
// as all the native side of Chrome, and this is the same clock used for page load metrics.
Bundle extras = new Bundle();
extras.putLong("timestampUptimeMillis", SystemClock.uptimeMillis());
return extras;
}
private void notifyWarmupIsDone(int uid) {
ThreadUtils.assertOnUiThread();
final Bundle args = new Bundle(); // Empty one - safe to reuse for all the callbacks.
// Notifies all the sessions, as warmup() is tied to a UID, not a session.
for (CustomTabsSessionToken session : mClientManager.uidToSessions(uid)) {
// TODO(crbug.com/40932858): Remove extra callback after its usage dwindles down.
safeExtraCallback(session, ON_WARMUP_COMPLETED, null);
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) continue;
try {
callback.onWarmupCompleted(args);
} catch (Exception e) {
// Catching all exceptions is really bad, but we need it here,
// because Android exposes us to client bugs by throwing a variety
// of exceptions. See crbug.com/517023.
continue;
}
}
logCallback("onWarmupCompleted()", bundleToJson(args).toString());
}
/**
* Creates a Bundle with a value for navigation start and the specified page load metric.
*
* @param metricName Name of the page load metric.
* @param navigationStartMicros Absolute navigation start time, in microseconds, in
* {@link SystemClock#uptimeMillis()} timebase.
* @param offsetMs Offset in ms from navigationStart for the page load metric.
*
* @return A Bundle containing navigation start and the page load metric.
*/
Bundle createBundleWithNavigationStartAndPageLoadMetric(
String metricName, long navigationStartMicros, long offsetMs) {
Bundle args = new Bundle();
args.putLong(metricName, offsetMs);
args.putLong(PageLoadMetrics.NAVIGATION_START, navigationStartMicros / 1000);
return args;
}
/**
* Notifies the application of a page load metric for a single metric.
*
* @param session Session identifier.
* @param metricName Name of the page load metric.
* @param navigationStartMicros Absolute navigation start time, in microseconds, in
* {@link SystemClock#uptimeMillis()} timebase.
* @param offsetMs Offset in ms from navigationStart for the page load metric.
*
* @return Whether the metric has been dispatched to the client.
*/
boolean notifySinglePageLoadMetric(
CustomTabsSessionToken session,
String metricName,
long navigationStartMicros,
long offsetMs) {
return notifyPageLoadMetrics(
session,
createBundleWithNavigationStartAndPageLoadMetric(
metricName, navigationStartMicros, offsetMs));
}
/**
* Notifies the application of a general page load metrics.
*
* TODD(lizeb): Move this to a proper method in {@link CustomTabsCallback} once one is
* available.
*
* @param session Session identifier.
* @param args Bundle containing metric information to update. Each item in the bundle
* should be a key specifying the metric name and the metric value as the value.
*/
boolean notifyPageLoadMetrics(CustomTabsSessionToken session, Bundle args) {
if (!mClientManager.shouldGetPageLoadMetrics(session)) return false;
if (safeExtraCallback(session, PAGE_LOAD_METRICS_CALLBACK, args)) {
logPageLoadMetricsCallback(args);
return true;
}
return false;
}
/**
* Notifies the application that the user has selected to open the page in their browser.
*
* @param session Session identifier.
* @param webContents the WebContents of the tab being taken out of CCT.
* @return true if success. To protect Chrome exceptions in the client application are swallowed
* and false is returned.
*/
public boolean notifyOpenInBrowser(CustomTabsSessionToken session, WebContents webContents) {
// Reset the client data header for the WebContents since it's not a CCT tab anymore.
if (webContents != null) CustomTabsConnectionJni.get().setClientDataHeader(webContents, "");
return safeExtraCallback(
session,
OPEN_IN_BROWSER_CALLBACK,
getExtrasBundleForNavigationEventForSession(session));
}
/**
* Wraps calling extraCallback in a try/catch so exceptions thrown by the host app don't crash
* Chrome. See https://crbug.com/517023.
*/
// The string passed is safe since it is a method name.
@SuppressWarnings("NoDynamicStringsInTraceEventCheck")
protected boolean safeExtraCallback(
CustomTabsSessionToken session, String callbackName, @Nullable Bundle args) {
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return false;
try (TraceEvent te =
TraceEvent.scoped("CustomTabsConnection::safeExtraCallback", callbackName)) {
callback.extraCallback(callbackName, args);
} catch (Exception e) {
return false;
}
return true;
}
/**
* Calls {@link CustomTabsCallback#extraCallbackWithResult)}.
* Wraps calling sendExtraCallbackWithResult in a try/catch so that exceptions thrown by the
* host app don't crash Chrome.
*/
@Nullable
// The string passed is safe since it is a method name.
@SuppressWarnings("NoDynamicStringsInTraceEventCheck")
public Bundle sendExtraCallbackWithResult(
CustomTabsSessionToken session, String callbackName, @Nullable Bundle args) {
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return null;
try (TraceEvent te =
TraceEvent.scoped(
"CustomTabsConnection::safeExtraCallbackWithResult", callbackName)) {
return callback.extraCallbackWithResult(callbackName, args);
} catch (Exception e) {
return null;
}
}
/**
* Keeps the application linked with a given session alive.
*
* The application is kept alive (that is, raised to at least the current process priority
* level) until {@link #dontKeepAliveForSession} is called.
*
* @param session The Binder object identifying the session.
* @param intent Intent describing the service to bind to.
* @return true for success.
*/
boolean keepAliveForSession(CustomTabsSessionToken session, Intent intent) {
return mClientManager.keepAliveForSession(session, intent);
}
/**
* Lets the lifetime of the process linked to a given sessionId be managed normally.
*
* Without a matching call to {@link #keepAliveForSession}, this is a no-op.
*
* @param session The Binder object identifying the session.
*/
void dontKeepAliveForSession(CustomTabsSessionToken session) {
mClientManager.dontKeepAliveForSession(session);
}
/**
* Returns whether /proc/PID/ is accessible.
*
* On devices where /proc is mounted with the "hidepid=2" option, cannot get access to the
* scheduler group, as this is under this directory, which is hidden unless PID == self (or
* its numeric value).
*/
@VisibleForTesting
static boolean canGetSchedulerGroup(int pid) {
String cgroupFilename = "/proc/" + pid;
File f = new File(cgroupFilename);
return f.exists() && f.isDirectory() && f.canExecute();
}
/**
* @return the CPU cgroup of a given process, identified by its PID, or null.
*/
@VisibleForTesting
static String getSchedulerGroup(int pid) {
// Android uses several cgroups for processes, depending on their priority. The list of
// cgroups a process is part of can be queried by reading /proc/<pid>/cgroup, which is
// world-readable.
String cgroupFilename = "/proc/" + pid + "/cgroup";
String controllerName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "cpuset" : "cpu";
try (BufferedReader reader = new BufferedReader(new FileReader(cgroupFilename))) {
String line = null;
while ((line = reader.readLine()) != null) {
// line format: 2:cpu:/bg_non_interactive
String[] fields = line.trim().split(":");
if (fields.length == 3 && fields[1].equals(controllerName)) return fields[2];
}
} catch (IOException e) {
return null;
}
return null;
}
private static boolean isBackgroundProcess(int pid) {
return BACKGROUND_GROUPS.contains(getSchedulerGroup(pid));
}
/**
* @return true when inside a Binder transaction and the caller is in the
* foreground or self. Don't use outside a Binder transaction.
*/
private boolean isCallerForegroundOrSelf() {
int uid = Binder.getCallingUid();
if (uid == Process.myUid()) return true;
// Starting with L MR1, AM.getRunningAppProcesses doesn't return all the processes so we use
// a workaround in this case.
int pid = Binder.getCallingPid();
boolean workaroundAvailable = canGetSchedulerGroup(pid);
// If we have no way to find out whether the calling process is in the foreground,
// optimistically assume it is. Otherwise we would effectively disable CCT warmup
// on these devices.
if (!workaroundAvailable) return true;
return isBackgroundProcess(pid);
}
void cleanupAllForTesting() {
ThreadUtils.assertOnUiThread();
mClientManager.cleanupAll();
mHiddenTabHolder.destroyHiddenTab(null);
}
/**
* Handle any clean up left after a session is destroyed.
* @param session The session that has been destroyed.
*/
@VisibleForTesting
void cleanUpSession(final CustomTabsSessionToken session) {
PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> mClientManager.cleanupSession(session));
}
/**
* Discards substantial objects that are not currently in use.
* @param level The type of signal as defined in {@link android.content.ComponentCallbacks2}.
*/
public static void onTrimMemory(int level) {
if (!hasInstance()) return;
if (ChromeApplicationImpl.isSevereMemorySignal(level)) {
getInstance().mClientManager.cleanupUnusedSessions();
}
}
boolean maySpeculate(CustomTabsSessionToken session) {
if (!DeviceClassManager.enablePrerendering()) {
return false;
}
Profile profile = ProfileManager.getLastUsedRegularProfile();
if (UserPrefs.get(profile).getInteger(COOKIE_CONTROLS_MODE)
== CookieControlsMode.BLOCK_THIRD_PARTY) {
return false;
}
if (PreloadPagesSettingsBridge.getState(profile) == PreloadPagesState.NO_PRELOADING) {
return false;
}
return true;
}
/** Cancels the speculation for a given session, or any session if null. */
public void cancelSpeculation(@Nullable CustomTabsSessionToken session) {
ThreadUtils.assertOnUiThread();
mHiddenTabHolder.destroyHiddenTab(session);
}
/*
* This function will do as much as it can to have a subsequent navigation
* to the specified url sped up, including speculatively loading a url, preconnecting,
* and starting a spare renderer.
*/
private void startSpeculation(
CustomTabsSessionToken session,
String url,
boolean useHiddenTab,
Bundle extras,
int uid,
boolean useSeparateStoragePartitionForExperiment) {
WarmupManager warmupManager = WarmupManager.getInstance();
Profile profile = ProfileManager.getLastUsedRegularProfile();
// At most one on-going speculation, clears the previous one.
cancelSpeculation(null);
if (useHiddenTab) {
launchUrlInHiddenTab(
session, profile, url, extras, useSeparateStoragePartitionForExperiment);
} else {
createSpareWebContents(profile);
}
warmupManager.maybePreconnectUrlAndSubResources(profile, url);
}
/** Creates a hidden tab and initiates a navigation. */
private void launchUrlInHiddenTab(
CustomTabsSessionToken session,
Profile profile,
String url,
@Nullable Bundle extras,
boolean useSeparateStoragePartitionForExperiment) {
ThreadUtils.assertOnUiThread();
WebContents webContents = null;
if (useSeparateStoragePartitionForExperiment) {
webContents =
WebContentsFactory.createWebContentsWithSeparateStoragePartitionForExperiment(
profile);
}
mHiddenTabHolder.launchUrlInHiddenTab(
(Tab tab) -> setClientDataHeaderForNewTab(session, tab.getWebContents()),
session,
profile,
mClientManager,
url,
extras,
webContents);
}
@VisibleForTesting
void resetThrottling(int uid) {
mClientManager.resetThrottling(uid);
}
@VisibleForTesting
void ban(int uid) {
mClientManager.ban(uid);
}
/**
* @return The referrer that is associated with the client owning the given session.
*/
public Referrer getDefaultReferrerForSession(CustomTabsSessionToken session) {
return mClientManager.getDefaultReferrerForSession(session);
}
/**
* @return The package name of a client for which the publisher URL from a trusted CDN can be
* shown, or null to disallow showing the publisher URL.
*/
public @Nullable String getTrustedCdnPublisherUrlPackage() {
return mTrustedPublisherUrlPackage;
}
/**
* @return Whether the publisher of the URL from a trusted CDN can be shown.
*/
public boolean isTrustedCdnPublisherUrlPackage(@Nullable String urlPackage) {
return urlPackage != null && urlPackage.equals(getTrustedCdnPublisherUrlPackage());
}
void setTrustedPublisherUrlPackageForTest(@Nullable String packageName) {
mTrustedPublisherUrlPackage = packageName;
}
public void setEngagementSignalsAvailableSupplier(
CustomTabsSessionToken session, Supplier<Boolean> supplier) {
mClientManager.setEngagementSignalsAvailableSupplierForSession(session, supplier);
}
public EngagementSignalsHandler getEngagementSignalsHandler(CustomTabsSessionToken session) {
return mClientManager.getEngagementSignalsHandlerForSession(session);
}
@CalledByNative
public static void notifyClientOfDetachedRequestCompletion(
CustomTabsSessionToken session, String url, int status) {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_REPORT_PARALLEL_REQUEST_STATUS)) {
return;
}
Bundle args = new Bundle();
args.putParcelable("url", Uri.parse(url));
args.putInt("net_error", status);
CustomTabsConnection connection = getInstance();
connection.safeExtraCallback(session, ON_DETACHED_REQUEST_COMPLETED, args);
if (connection.mLogRequests) {
connection.logCallback(ON_DETACHED_REQUEST_COMPLETED, bundleToJson(args).toString());
}
}
@VisibleForTesting
@Nullable
HiddenTabHolder.SpeculationParams getSpeculationParamsForTesting() {
return mHiddenTabHolder.getSpeculationParamsForTesting();
}
public static void createSpareWebContents(Profile profile) {
if (sSkipTabPrewarmingForTesting) return;
if (SysUtils.isLowEndDevice()) return;
if (WarmupManager.getInstance().isCCTPrewarmTabFeatureEnabled(true)) {
WarmupManager.getInstance().createRegularSpareTab(profile);
} else {
WarmupManager.getInstance().createSpareWebContents(profile);
}
}
public boolean receiveFile(
CustomTabsSessionToken sessionToken, Uri uri, int purpose, Bundle extras) {
return ChromeApplicationImpl.getComponent()
.resolveCustomTabsFileProcessor()
.processFile(sessionToken, uri, purpose, extras);
}
public void setCustomTabIsInForeground(
@Nullable CustomTabsSessionToken session, boolean isInForeground) {
mClientManager.setCustomTabIsInForeground(session, isInForeground);
}
public boolean isEngagementSignalsApiAvailable(
CustomTabsSessionToken sessionToken, Bundle extras) {
return isEngagementSignalsApiAvailableInternal(sessionToken);
}
public boolean setEngagementSignalsCallback(
CustomTabsSessionToken sessionToken,
EngagementSignalsCallback callback,
Bundle extras) {
if (!isEngagementSignalsApiAvailableInternal(sessionToken)) return false;
var engagementSignalsHandler =
mClientManager.getEngagementSignalsHandlerForSession(sessionToken);
if (engagementSignalsHandler == null) return false;
mClientManager.setEngagementSignalsCallbackForSession(sessionToken, callback);
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> engagementSignalsHandler.setEngagementSignalsCallback(callback));
return true;
}
private boolean isEngagementSignalsApiAvailableInternal(CustomTabsSessionToken session) {
var supplier = mClientManager.getEngagementSignalsAvailableSupplierForSession(session);
return supplier != null
? supplier.get()
: PrivacyPreferencesManagerImpl.getInstance().isUsageAndCrashReportingPermitted();
}
public boolean hasEngagementSignalsCallback(CustomTabsSessionToken session) {
return mClientManager.getEngagementSignalsCallbackForSession(session) != null;
}
/** Whether a CustomTabs instance should include interactive Omnibox. */
public boolean shouldEnableOmniboxForIntent(BrowserServicesIntentDataProvider intentData) {
return false;
}
/**
* Returns an alternate handler for taps on the Custom Tabs Omnibox, or null if the default
* handler should be used.
*/
@Nullable
public Consumer<Tab> getAlternateOmniboxTapHandler(
BrowserServicesIntentDataProvider intentData) {
return null;
}
/** Specifies what content should be presented by the CustomTabs instance in location bar. */
public int getTitleVisibilityState(BrowserServicesIntentDataProvider intentData) {
if (shouldEnableOmniboxForIntent(intentData)) {
return CustomTabsIntent.NO_TITLE;
}
return intentData.getTitleVisibilityState();
}
/**
* Whether Google Bottom Bar is enabled by the launching Intent. False by default.
*
* @param intentData {@link BrowserServicesIntentDataProvider} built from the Intent that
* launched this CCT.
*/
public boolean shouldEnableGoogleBottomBarForIntent(
BrowserServicesIntentDataProvider intentData) {
return false;
}
/**
* Checks whether Google Bottom Bar buttons are present in the Intent data. False by default.
*
* @param intentData {@link BrowserServicesIntentDataProvider} built from the Intent that
* launched this CCT.
*/
public boolean hasExtraGoogleBottomBarButtons(BrowserServicesIntentDataProvider intentData) {
return false;
}
/**
* Returns Google Bottom Bar buttons that are added to the Intent.
*
* @param intentData {@link BrowserServicesIntentDataProvider} built from the Intent that
* launched this CCT.
* @return An ArrayList of Bundles, each representing a Google Bottom Bar item.
*/
public List<Bundle> getGoogleBottomBarButtons(BrowserServicesIntentDataProvider intentData) {
return new ArrayList<>();
}
public GoogleBottomBarIntentParams getGoogleBottomBarIntentParams(
BrowserServicesIntentDataProvider intentData) {
return GoogleBottomBarIntentParams.getDefaultInstance();
}
/**
* Called when text fragment lookups on the current page has completed.
*
* @param session session object.
* @param stateKey unique key for the embedder to keep track of the request.
* @param foundTextFragments text fragments from the initial request that were found on the
* page.
*/
@CalledByNative
private static void notifyClientOfTextFragmentLookupCompletion(
CustomTabsSessionToken session, String stateKey, String[] foundTextFragments) {
getInstance()
.notifyClientOfTextFragmentLookupCompletionReportApp(
session, stateKey, new ArrayList(Arrays.asList(foundTextFragments)));
}
protected void notifyClientOfTextFragmentLookupCompletionReportApp(
CustomTabsSessionToken session,
String stateKey,
ArrayList<String> foundTextFragments) {}
/**
* @return The CalledWarmup state for the session.
*/
public @CalledWarmup int getWarmupState(CustomTabsSessionToken session) {
return mClientManager.getWarmupState(session);
}
public static void setInstanceForTesting(CustomTabsConnection connection) {
var oldValue = sInstance;
sInstance = connection;
ResettersForTesting.register(() -> sInstance = oldValue);
}
@NativeMethods
interface Natives {
void createAndStartDetachedResourceRequest(
@JniType("Profile*") Profile profile,
CustomTabsSessionToken session,
String packageName,
String url,
String origin,
int referrerPolicy,
@DetachedResourceRequestMotivation int motivation);
void setClientDataHeader(WebContents webContents, String header);
void textFragmentLookup(
CustomTabsSessionToken session,
WebContents webContents,
String stateKey,
String[] textFragment);
void textFragmentFindScrollAndHighlight(
CustomTabsSessionToken session, WebContents webContents, String textFragment);
}
}