// 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 android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsService.Relation;
import androidx.browser.customtabs.CustomTabsSessionToken;
import androidx.browser.customtabs.EngagementSignalsCallback;
import androidx.browser.customtabs.PostMessageServiceConnection;
import org.chromium.base.ContextUtils;
import org.chromium.base.SysUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.browserservices.PostMessageHandler;
import org.chromium.chrome.browser.browserservices.verification.ChromeOriginVerifier;
import org.chromium.chrome.browser.browserservices.verification.ChromeOriginVerifierFactory;
import org.chromium.chrome.browser.browserservices.verification.ChromeOriginVerifierFactoryImpl;
import org.chromium.chrome.browser.customtabs.content.EngagementSignalsHandler;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.components.content_relationship_verification.OriginVerifier.OriginVerificationListener;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.installedapp.InstalledAppProviderImpl;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.Referrer;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Manages the clients' state for Custom Tabs. This class is threadsafe. */
class ClientManager {
// Values for the "CustomTabs.MayLaunchUrlType" UMA histogram. Append-only.
@IntDef({
MayLaunchUrlType.NO_MAY_LAUNCH_URL,
MayLaunchUrlType.LOW_CONFIDENCE,
MayLaunchUrlType.HIGH_CONFIDENCE,
MayLaunchUrlType.BOTH,
MayLaunchUrlType.INVALID_SESSION,
MayLaunchUrlType.NUM_ENTRIES
})
@Retention(RetentionPolicy.SOURCE)
@interface MayLaunchUrlType {
@VisibleForTesting int NO_MAY_LAUNCH_URL = 0;
@VisibleForTesting int LOW_CONFIDENCE = 1;
@VisibleForTesting int HIGH_CONFIDENCE = 2;
@VisibleForTesting int BOTH = 3; // LOW + HIGH.
int INVALID_SESSION = 4;
int NUM_ENTRIES = 5;
}
// Values for the PredictionStatus. Append-only.
@IntDef({PredictionStatus.NONE, PredictionStatus.GOOD, PredictionStatus.BAD})
@Retention(RetentionPolicy.SOURCE)
@interface PredictionStatus {
@VisibleForTesting int NONE = 0;
@VisibleForTesting int GOOD = 1;
@VisibleForTesting int BAD = 2;
int NUM_ENTRIES = 3;
}
// Values for the "CustomTabs.CalledWarmup" UMA histogram. Append-only.
@IntDef({
CalledWarmup.NO_SESSION_NO_WARMUP,
CalledWarmup.NO_SESSION_WARMUP,
CalledWarmup.SESSION_NO_WARMUP_ALREADY_CALLED,
CalledWarmup.SESSION_NO_WARMUP_NOT_CALLED,
CalledWarmup.SESSION_WARMUP
})
@Retention(RetentionPolicy.SOURCE)
@interface CalledWarmup {
@VisibleForTesting int NO_SESSION_NO_WARMUP = 0;
@VisibleForTesting int NO_SESSION_WARMUP = 1;
@VisibleForTesting int SESSION_NO_WARMUP_ALREADY_CALLED = 2;
@VisibleForTesting int SESSION_NO_WARMUP_NOT_CALLED = 3;
@VisibleForTesting int SESSION_WARMUP = 4;
@VisibleForTesting int NUM_ENTRIES = 5;
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// Values for the "CustomTabs.SessionDisconnectStatus" UMA histogram. Append-only.
@IntDef({
SessionDisconnectStatus.UNKNOWN,
SessionDisconnectStatus.CT_FOREGROUND,
SessionDisconnectStatus.CT_FOREGROUND_KEEP_ALIVE,
SessionDisconnectStatus.CT_BACKGROUND,
SessionDisconnectStatus.CT_BACKGROUND_KEEP_ALIVE,
SessionDisconnectStatus.LOW_MEMORY_CT_FOREGROUND,
SessionDisconnectStatus.LOW_MEMORY_CT_FOREGROUND_KEEP_ALIVE,
SessionDisconnectStatus.LOW_MEMORY_CT_BACKGROUND,
SessionDisconnectStatus.LOW_MEMORY_CT_BACKGROUND_KEEP_ALIVE,
SessionDisconnectStatus.NUM_ENTRIES
})
@Retention(RetentionPolicy.SOURCE)
@interface SessionDisconnectStatus {
@VisibleForTesting int UNKNOWN = 0;
@VisibleForTesting int CT_FOREGROUND = 1;
@VisibleForTesting int CT_FOREGROUND_KEEP_ALIVE = 2;
@VisibleForTesting int CT_BACKGROUND = 3;
@VisibleForTesting int CT_BACKGROUND_KEEP_ALIVE = 4;
@VisibleForTesting int LOW_MEMORY_CT_FOREGROUND = 5;
@VisibleForTesting int LOW_MEMORY_CT_FOREGROUND_KEEP_ALIVE = 6;
@VisibleForTesting int LOW_MEMORY_CT_BACKGROUND = 7;
@VisibleForTesting int LOW_MEMORY_CT_BACKGROUND_KEEP_ALIVE = 8;
@VisibleForTesting int NUM_ENTRIES = 9;
}
/** To be called when a client gets disconnected. */
public interface DisconnectCallback {
public void run(CustomTabsSessionToken session);
}
private static class KeepAliveServiceConnection implements ServiceConnection {
private final Context mContext;
private final Intent mServiceIntent;
private boolean mHasDied;
private boolean mIsBound;
public KeepAliveServiceConnection(Context context, Intent serviceIntent) {
mContext = context;
mServiceIntent = serviceIntent;
}
/**
* Connects to the service identified by |serviceIntent|. Does not reconnect if the service
* got disconnected at some point from the other end (remote process death).
*/
public boolean connect() {
if (mIsBound) return true;
// If the remote process died at some point, it doesn't make sense to resurrect it.
if (mHasDied) return false;
boolean ok;
try {
ok = mContext.bindService(mServiceIntent, this, Context.BIND_AUTO_CREATE);
} catch (SecurityException e) {
return false;
}
mIsBound = ok;
return ok;
}
/**
* Disconnects from the remote process. Safe to call even if {@link #connect} returned
* false, or if the remote service died.
*/
public void disconnect() {
if (mIsBound) {
mContext.unbindService(this);
mIsBound = false;
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mIsBound) {
// The remote process has died. This typically happens if the system is low enough
// on memory to kill one of the last process on the "kill list". In this case, we
// shouldn't resurrect the process (which happens with BIND_AUTO_CREATE) because
// that could create a "restart/kill" loop.
mHasDied = true;
disconnect();
}
}
}
/** Per-session values. */
private static class SessionParams {
public final int uid;
private CustomTabsCallback mCustomTabsCallback;
private EngagementSignalsCallback mEngagementSignalsCallback;
public final DisconnectCallback disconnectCallback;
public final PostMessageHandler postMessageHandler;
public final PostMessageServiceConnection serviceConnection;
public final Set<Origin> mLinkedOrigins = new HashSet<>();
public ChromeOriginVerifier originVerifier;
public boolean mIgnoreFragments;
public boolean lowConfidencePrediction;
public boolean highConfidencePrediction;
private String mPackageName;
private boolean mShouldHideDomain;
private boolean mShouldSpeculateLoadOnCellular;
private boolean mShouldSendNavigationInfo;
private boolean mShouldSendBottomBarScrollState;
private KeepAliveServiceConnection mKeepAliveConnection;
private String mPredictedUrl;
private long mLastMayLaunchUrlTimestamp;
private boolean mCanUseHiddenTab;
private boolean mAllowParallelRequest;
private boolean mAllowResourcePrefetch;
private boolean mShouldGetPageLoadMetrics;
private boolean mCustomTabIsInForeground;
private boolean mWasSessionDisconnectStatusLogged;
private Supplier<Boolean> mEngagementSignalsAvailableSupplier;
private final EngagementSignalsHandler mEngagementSignalsHandler;
public SessionParams(
Context context,
int uid,
CustomTabsCallback customTabsCallback,
DisconnectCallback callback,
PostMessageHandler postMessageHandler,
PostMessageServiceConnection serviceConnection,
EngagementSignalsHandler engagementSignalsHandler) {
this.uid = uid;
mPackageName = getPackageName(context, uid);
mCustomTabsCallback = customTabsCallback;
disconnectCallback = callback;
this.postMessageHandler = postMessageHandler;
this.serviceConnection = serviceConnection;
if (postMessageHandler != null) this.serviceConnection.setPackageName(mPackageName);
mEngagementSignalsHandler = engagementSignalsHandler;
}
/** Overrides package name with given String. TO be used for testing only. */
void overridePackageNameForTesting(String newPackageName) {
mPackageName = newPackageName;
}
/**
* @return The package name for this session.
*/
public String getPackageName() {
return mPackageName;
}
private static String getPackageName(Context context, int uid) {
PackageManager packageManager = context.getPackageManager();
String[] packageList = packageManager.getPackagesForUid(uid);
if (packageList.length != 1 || TextUtils.isEmpty(packageList[0])) return null;
return packageList[0];
}
public KeepAliveServiceConnection getKeepAliveConnection() {
return mKeepAliveConnection;
}
public void setKeepAliveConnection(KeepAliveServiceConnection serviceConnection) {
mKeepAliveConnection = serviceConnection;
}
public void setPredictionMetrics(
String predictedUrl, long lastMayLaunchUrlTimestamp, boolean lowConfidence) {
mPredictedUrl = predictedUrl;
mLastMayLaunchUrlTimestamp = lastMayLaunchUrlTimestamp;
highConfidencePrediction |= !TextUtils.isEmpty(predictedUrl);
lowConfidencePrediction |= lowConfidence;
}
/**
* Resets the prediction metrics. This clears the predicted URL, last prediction time,
* and whether a low and/or high confidence prediction has been done.
*/
public void resetPredictionMetrics() {
mPredictedUrl = null;
mLastMayLaunchUrlTimestamp = 0;
highConfidencePrediction = false;
lowConfidencePrediction = false;
}
public String getPredictedUrl() {
return mPredictedUrl;
}
public long getLastMayLaunchUrlTimestamp() {
return mLastMayLaunchUrlTimestamp;
}
/**
* @return Whether the default parameters are used for this session.
*/
public boolean isDefault() {
return !mIgnoreFragments && !mShouldSpeculateLoadOnCellular;
}
public CustomTabsCallback getCustomTabsCallback() {
return mCustomTabsCallback;
}
public void setCustomTabsCallback(CustomTabsCallback customTabsCallback) {
mCustomTabsCallback = customTabsCallback;
}
public EngagementSignalsCallback getEngagementSignalsCallback() {
return mEngagementSignalsCallback;
}
public void setEngagementSignalsCallback(EngagementSignalsCallback callback) {
mEngagementSignalsCallback = callback;
}
public void setEngagementSignalsAvailableSupplier(Supplier<Boolean> supplier) {
mEngagementSignalsAvailableSupplier = supplier;
}
public Supplier<Boolean> getEngagementSignalsAvailableSupplier() {
return mEngagementSignalsAvailableSupplier;
}
public EngagementSignalsHandler getEngagementSignalsHandler() {
return mEngagementSignalsHandler;
}
}
/** A wrapper around {@link InstalledAppProviderImpl} to aid testing. */
interface InstalledAppProviderWrapper {
/**
* Calls through to {@link InstalledAppProviderImpl#isAppInstalledAndAssociatedWithOrigin}.
*/
boolean isAppInstalledAndAssociatedWithOrigin(String packageName, Origin origin);
}
private static class ProdInstalledAppProviderWrapper implements InstalledAppProviderWrapper {
@Override
public boolean isAppInstalledAndAssociatedWithOrigin(String packageName, Origin origin) {
return InstalledAppProviderImpl.isAppInstalledAndAssociatedWithOrigin(
packageName, new GURL(origin.toString()));
}
}
private final ChromeOriginVerifierFactory mOriginVerifierFactory;
private final InstalledAppProviderWrapper mInstalledAppProviderWrapper;
private final ChromeBrowserInitializer mChromeBrowserInitializer;
private final Map<CustomTabsSessionToken, SessionParams> mSessionParams = new HashMap<>();
private final SparseBooleanArray mUidHasCalledWarmup = new SparseBooleanArray();
private boolean mWarmupHasBeenCalled;
public ClientManager() {
this(
new ChromeOriginVerifierFactoryImpl(),
new ProdInstalledAppProviderWrapper(),
ChromeBrowserInitializer.getInstance());
}
public ClientManager(
ChromeOriginVerifierFactory originVerifierFactory,
InstalledAppProviderWrapper installedAppProviderWrapper,
ChromeBrowserInitializer chromeBrowserInitializer) {
mOriginVerifierFactory = originVerifierFactory;
mInstalledAppProviderWrapper = installedAppProviderWrapper;
mChromeBrowserInitializer = chromeBrowserInitializer;
RequestThrottler.loadInBackground();
}
/**
* Creates a new session.
*
* @param session Session provided by the client.
* @param uid Client UID, as returned by Binder.getCallingUid(),
* @param onDisconnect To be called on the UI thread when a client gets disconnected.
* @param postMessageHandler The handler to be used for postMessage related operations.
* @return true for success.
*/
public synchronized boolean newSession(
CustomTabsSessionToken session,
int uid,
DisconnectCallback onDisconnect,
@NonNull PostMessageHandler postMessageHandler,
@NonNull PostMessageServiceConnection serviceConnection,
@NonNull EngagementSignalsHandler engagementSignalsHandler) {
if (session == null || session.getCallback() == null) return false;
if (mSessionParams.containsKey(session)) {
SessionParams params = mSessionParams.get(session);
params.setCustomTabsCallback(session.getCallback());
params.mWasSessionDisconnectStatusLogged = false;
} else {
SessionParams params =
new SessionParams(
ContextUtils.getApplicationContext(),
uid,
session.getCallback(),
onDisconnect,
postMessageHandler,
serviceConnection,
engagementSignalsHandler);
mSessionParams.put(session, params);
}
return true;
}
/**
* Records that {@link CustomTabsConnection#warmup(long)} has been called from the given uid.
*/
public synchronized void recordUidHasCalledWarmup(int uid) {
mWarmupHasBeenCalled = true;
mUidHasCalledWarmup.put(uid, true);
}
/**
* @return all the sessions originating from a given {@code uid}.
*/
public synchronized List<CustomTabsSessionToken> uidToSessions(int uid) {
List<CustomTabsSessionToken> sessions = new ArrayList<>();
for (Map.Entry<CustomTabsSessionToken, SessionParams> entry : mSessionParams.entrySet()) {
if (entry.getValue().uid == uid) sessions.add(entry.getKey());
}
return sessions;
}
/** Updates the client behavior stats and returns whether speculation is allowed.
*
* The first call to the "low priority" mode is not throttled. Subsequent ones are.
*
* @param session Client session.
* @param uid As returned by Binder.getCallingUid().
* @param url Predicted URL.
* @param lowConfidence whether the request contains some "low confidence" URLs.
* @return true if speculation is allowed.
*/
public synchronized boolean updateStatsAndReturnWhetherAllowed(
CustomTabsSessionToken session, int uid, String url, boolean lowConfidence) {
SessionParams params = mSessionParams.get(session);
if (params == null || params.uid != uid) return false;
boolean firstLowConfidencePrediction =
TextUtils.isEmpty(url) && lowConfidence && !params.lowConfidencePrediction;
params.setPredictionMetrics(url, SystemClock.elapsedRealtime(), lowConfidence);
if (firstLowConfidencePrediction) return true;
RequestThrottler throttler = RequestThrottler.getForUid(uid);
return throttler.updateStatsAndReturnWhetherAllowed();
}
@VisibleForTesting
synchronized @CalledWarmup int getWarmupState(CustomTabsSessionToken session) {
SessionParams params = mSessionParams.get(session);
boolean hasValidSession = params != null;
boolean hasUidCalledWarmup = hasValidSession && mUidHasCalledWarmup.get(params.uid);
int result =
mWarmupHasBeenCalled
? CalledWarmup.NO_SESSION_WARMUP
: CalledWarmup.NO_SESSION_NO_WARMUP;
if (hasValidSession) {
if (hasUidCalledWarmup) {
result = CalledWarmup.SESSION_WARMUP;
} else {
result =
mWarmupHasBeenCalled
? CalledWarmup.SESSION_NO_WARMUP_ALREADY_CALLED
: CalledWarmup.SESSION_NO_WARMUP_NOT_CALLED;
}
}
return result;
}
/**
* @return the prediction outcome. PredictionStatus.NONE if mSessionParams.get(session) returns
* null.
*/
@VisibleForTesting
synchronized @PredictionStatus int getPredictionOutcome(
CustomTabsSessionToken session, String url) {
SessionParams params = mSessionParams.get(session);
if (params == null) return PredictionStatus.NONE;
String predictedUrl = params.getPredictedUrl();
if (predictedUrl == null) return PredictionStatus.NONE;
boolean urlsMatch =
TextUtils.equals(predictedUrl, url)
|| (params.mIgnoreFragments
&& UrlUtilities.urlsMatchIgnoringFragments(predictedUrl, url));
return urlsMatch ? PredictionStatus.GOOD : PredictionStatus.BAD;
}
/** Registers that a client has launched a URL inside a Custom Tab. */
public synchronized void registerLaunch(CustomTabsSessionToken session, String url) {
@PredictionStatus int outcome = getPredictionOutcome(session, url);
SessionParams params = mSessionParams.get(session);
if (outcome == PredictionStatus.GOOD) {
RequestThrottler.getForUid(params.uid).registerSuccess(params.mPredictedUrl);
}
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.WarmupStateOnLaunch",
getWarmupState(session),
CalledWarmup.NUM_ENTRIES);
if (params == null) {
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.MayLaunchUrlType",
MayLaunchUrlType.INVALID_SESSION,
MayLaunchUrlType.NUM_ENTRIES);
return;
}
@MayLaunchUrlType
int value =
(params.lowConfidencePrediction ? MayLaunchUrlType.LOW_CONFIDENCE : 0)
+ (params.highConfidencePrediction ? MayLaunchUrlType.HIGH_CONFIDENCE : 0);
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.MayLaunchUrlType", value, MayLaunchUrlType.NUM_ENTRIES);
params.resetPredictionMetrics();
}
public int postMessage(CustomTabsSessionToken session, String message) {
return callOnSession(
session,
CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR,
params -> params.postMessageHandler.postMessageFromClientApp(message));
}
/**
* See {@link PostMessageServiceConnection#bindSessionToPostMessageService(Context, String)}.
*/
public boolean bindToPostMessageServiceForSession(CustomTabsSessionToken session) {
return callOnSession(
session,
false,
params ->
params.serviceConnection.bindSessionToPostMessageService(
ContextUtils.getApplicationContext()));
}
/** See {@link PostMessageHandler#initializeWithPostMessageUri(Uri, Uri)}. */
public void initializeWithPostMessageOriginForSession(
CustomTabsSessionToken session, Uri origin, Uri targetOrigin) {
callOnSession(
session,
params ->
params.postMessageHandler.initializeWithPostMessageUri(
origin, targetOrigin));
}
public synchronized boolean validateRelationship(
CustomTabsSessionToken session, int relation, Origin origin, Bundle extras) {
return validateRelationshipInternal(session, relation, origin, null, false);
}
/** Validates the link between the client and the origin. */
public synchronized void verifyAndInitializeWithPostMessageOriginForSession(
CustomTabsSessionToken session,
Origin origin,
Origin targetOrigin,
@Relation int relation) {
validateRelationshipInternal(session, relation, origin, targetOrigin, true);
}
/** Can't be called on UI Thread. */
private synchronized boolean validateRelationshipInternal(
CustomTabsSessionToken session,
int relation,
Origin origin,
@Nullable Origin targetOrigin,
boolean initializePostMessageChannel) {
SessionParams params = mSessionParams.get(session);
if (params == null || TextUtils.isEmpty(params.getPackageName())) return false;
OriginVerificationListener listener =
(packageName, verifiedOrigin, verified, online) -> {
assert origin.equals(verifiedOrigin);
CustomTabsCallback callback = getCallbackForSession(session);
if (callback != null) {
Bundle extras = null;
if (verified && online != null) {
extras = new Bundle();
extras.putBoolean(CustomTabsCallback.ONLINE_EXTRAS_KEY, online);
}
callback.onRelationshipValidationResult(
relation, origin.uri(), verified, extras);
}
if (initializePostMessageChannel) {
if (targetOrigin != null) {
params.postMessageHandler.setPostMessageTargetUri(targetOrigin.uri());
}
params.postMessageHandler.onOriginVerified(
packageName, verifiedOrigin, verified, online);
}
};
params.originVerifier =
mOriginVerifierFactory.create(
params.getPackageName(),
relation,
/* webContents= */ null,
/* externalAuthUtils= */ null);
mChromeBrowserInitializer.runNowOrAfterFullBrowserStarted(
() -> {
PostTask.runOrPostTask(
TaskTraits.UI_DEFAULT,
() -> {
params.originVerifier.start(listener, origin);
});
});
if (relation == CustomTabsService.RELATION_HANDLE_ALL_URLS
&& mInstalledAppProviderWrapper.isAppInstalledAndAssociatedWithOrigin(
params.getPackageName(), origin)) {
params.mLinkedOrigins.add(origin);
}
return true;
}
/**
* @return The postMessage origin for the given session.
*/
Uri getPostMessageOriginForSessionForTesting(CustomTabsSessionToken session) {
return callOnSession(
session,
null,
params -> params.postMessageHandler.getPostMessageUriForTesting() // IN-TEST
);
}
/**
* @return The postMessage target origin for the given session.
*/
Uri getPostMessageTargetOriginForSessionForTesting(CustomTabsSessionToken session) {
return callOnSession(
session,
null,
params -> params.postMessageHandler.getPostMessageTargetUriForTesting() // IN-TEST
);
}
/** See {@link PostMessageHandler#reset(WebContents)}. */
public void resetPostMessageHandlerForSession(
CustomTabsSessionToken session, WebContents webContents) {
callOnSession(session, params -> params.postMessageHandler.reset(webContents));
}
/**
* @return The referrer that is associated with the client owning given session.
*/
public synchronized Referrer getDefaultReferrerForSession(CustomTabsSessionToken session) {
return IntentHandler.constructValidReferrerForAuthority(
getClientPackageNameForSession(session));
}
/**
* @return The package name associated with the client owning the given session.
*/
public String getClientPackageNameForSession(CustomTabsSessionToken session) {
return callOnSession(session, null, params -> params.getPackageName());
}
/**
* Overrides the package name for the given session to be the given package name. To be used
* for testing only.
*/
public void overridePackageNameForSessionForTesting(
CustomTabsSessionToken session, String packageName) {
callOnSession(
session, params -> params.overridePackageNameForTesting(packageName) // IN-TEST
);
}
/**
* @return The callback {@link CustomTabsSessionToken} for the given session.
*/
public CustomTabsCallback getCallbackForSession(CustomTabsSessionToken session) {
return callOnSession(session, null, params -> params.getCustomTabsCallback());
}
/**
* @return Whether the urlbar should be hidden for the session on first page load. Urls are
* foced to show up after the user navigates away.
*/
public boolean shouldHideDomainForSession(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mShouldHideDomain);
}
/** Sets whether the urlbar should be hidden for a given session. */
public void setHideDomainForSession(CustomTabsSessionToken session, boolean hide) {
callOnSession(session, params -> params.mShouldHideDomain = hide);
}
/**
* @return Whether bottom bar scrolling state should be recorded and shared for the session.
*/
public boolean shouldSendBottomBarScrollStateForSession(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mShouldSendBottomBarScrollState);
}
/** Sets whether bottom bar scrolling state should be recorded and shared for the session. */
public void setSendBottomBarScrollingStateForSessionn(
CustomTabsSessionToken session, boolean send) {
callOnSession(session, params -> params.mShouldSendBottomBarScrollState = send);
}
/**
* @return Whether navigation info should be recorded and shared for the session.
*/
public boolean shouldSendNavigationInfoForSession(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mShouldSendNavigationInfo);
}
/**
* Sets whether navigation info should be recorded and shared for the current navigation in this
* session.
*/
public void setSendNavigationInfoForSession(CustomTabsSessionToken session, boolean send) {
callOnSession(session, params -> params.mShouldSendNavigationInfo = send);
}
/**
* @return Whether the fragment should be ignored for speculation matching.
*/
public boolean getIgnoreFragmentsForSession(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mIgnoreFragments);
}
/** Sets whether the fragment should be ignored for speculation matching. */
public void setIgnoreFragmentsForSession(CustomTabsSessionToken session, boolean value) {
callOnSession(session, params -> params.mIgnoreFragments = value);
}
/**
* @return Whether load speculation should be turned on for cellular networks for given session.
*/
public boolean shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mShouldSpeculateLoadOnCellular);
}
/**
* @return Whether the session is using the default parameters (that is, don't ignore
* fragments and don't speculate loads on cellular connections).
*/
public boolean usesDefaultSessionParameters(CustomTabsSessionToken session) {
return callOnSession(session, true, params -> params.isDefault());
}
/**
* Sets whether speculation should be turned on for mobile networks for given session.
* If it is turned on, hidden tab speculation is turned on as well.
*/
public void setSpeculateLoadOnCellularForSession(
CustomTabsSessionToken session, boolean shouldSpeculate) {
callOnSession(
session,
params -> {
params.mShouldSpeculateLoadOnCellular = shouldSpeculate;
params.mCanUseHiddenTab = shouldSpeculate;
});
}
/** Sets whether hidden tab speculation can be used. */
public void setCanUseHiddenTab(CustomTabsSessionToken session, boolean canUseHiddenTab) {
callOnSession(session, params -> params.mCanUseHiddenTab = canUseHiddenTab);
}
/** Get whether hidden tab speculation can be used. The default is false. */
public boolean getCanUseHiddenTab(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mCanUseHiddenTab);
}
public void setAllowParallelRequestForSession(CustomTabsSessionToken session, boolean allowed) {
callOnSession(session, params -> params.mAllowParallelRequest = allowed);
}
public boolean getAllowParallelRequestForSession(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mAllowParallelRequest);
}
public void setAllowResourcePrefetchForSession(
CustomTabsSessionToken session, boolean allowed) {
callOnSession(session, params -> params.mAllowResourcePrefetch = allowed);
}
public boolean getAllowResourcePrefetchForSession(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mAllowResourcePrefetch);
}
public void setShouldGetPageLoadMetricsForSession(
CustomTabsSessionToken session, boolean allowed) {
callOnSession(session, params -> params.mShouldGetPageLoadMetrics = allowed);
}
public boolean shouldGetPageLoadMetrics(CustomTabsSessionToken session) {
return callOnSession(session, false, params -> params.mShouldGetPageLoadMetrics);
}
/** Returns the uid associated with the session, {@code -1} if there is no matching session. */
public int getUidForSession(CustomTabsSessionToken session) {
return callOnSession(session, -1, params -> params.uid);
}
/**
* Returns whether an origin is first-party with respect to a session, that is if the
* application linked to the session has a relation with the provided origin. This does not
* calls OriginVerifier, but only checks the cached relations.
*
* @param session The session.
* @param origin Origin to verify
*/
public synchronized boolean isFirstPartyOriginForSession(
CustomTabsSessionToken session, Origin origin) {
return ChromeOriginVerifier.wasPreviouslyVerified(
getClientPackageNameForSession(session),
origin,
CustomTabsService.RELATION_USE_AS_ORIGIN);
}
/** Tries to bind to a client to keep it alive, and returns true for success. */
public synchronized boolean keepAliveForSession(CustomTabsSessionToken session, Intent intent) {
// When an application is bound to a service, its priority is raised to
// be at least equal to the application's one. This binds to a placeholder
// service (no calls to this service are made).
if (intent == null || intent.getComponent() == null) return false;
SessionParams params = mSessionParams.get(session);
if (params == null) return false;
KeepAliveServiceConnection connection = params.getKeepAliveConnection();
if (connection == null) {
String packageName = intent.getComponent().getPackageName();
PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
// Only binds to the application associated to this session.
if (!Arrays.asList(pm.getPackagesForUid(params.uid)).contains(packageName)) {
return false;
}
Intent serviceIntent = new Intent().setComponent(intent.getComponent());
connection =
new KeepAliveServiceConnection(
ContextUtils.getApplicationContext(), serviceIntent);
}
boolean ok = connection.connect();
if (ok) params.setKeepAliveConnection(connection);
return ok;
}
/** Unbind from the KeepAlive service for a client. */
public void dontKeepAliveForSession(CustomTabsSessionToken session) {
callOnSession(
session,
params -> {
KeepAliveServiceConnection connection = params.getKeepAliveConnection();
if (connection == null) return;
connection.disconnect();
});
}
/** See {@link RequestThrottler#isPrerenderingAllowed()} */
public synchronized boolean isPrerenderingAllowed(int uid) {
return RequestThrottler.getForUid(uid).isPrerenderingAllowed();
}
/** See {@link RequestThrottler#registerPrerenderRequest(String)} */
public synchronized void registerPrerenderRequest(int uid, String url) {
RequestThrottler.getForUid(uid).registerPrerenderRequest(url);
}
/** See {@link RequestThrottler#reset()} */
public synchronized void resetThrottling(int uid) {
RequestThrottler.getForUid(uid).reset();
}
/** See {@link RequestThrottler#ban()} */
public synchronized void ban(int uid) {
RequestThrottler.getForUid(uid).ban();
}
/** Cleans up all data associated with all sessions. */
public synchronized void cleanupAll() {
// cleanupSessionInternal modifies mSessionParams therefore we need a copy
List<CustomTabsSessionToken> sessions = new ArrayList<>(mSessionParams.keySet());
for (CustomTabsSessionToken session : sessions) cleanupSession(session);
}
/**
* Handle any clean up left after a session is destroyed.
* @param session The session that has been destroyed.
*/
private void cleanupSessionInternal(CustomTabsSessionToken session) {
callOnSession(
session,
params -> {
logConnectionClosed(params);
mSessionParams.remove(session);
if (params.serviceConnection != null) {
params.serviceConnection.cleanup(ContextUtils.getApplicationContext());
}
if (params.originVerifier != null) params.originVerifier.cleanUp();
if (params.disconnectCallback != null) params.disconnectCallback.run(session);
mUidHasCalledWarmup.delete(params.uid);
});
}
/**
* Destroys session when its callback become invalid if the callback is used as identifier.
*
* @param session The session with invalid callback.
*/
public synchronized void cleanupSession(CustomTabsSessionToken session) {
if (session.hasId() && mSessionParams.containsKey(session)) {
SessionParams params = mSessionParams.get(session);
// Logging as soon as we know a session has been disconnected.
logConnectionClosed(params);
// Leave session parameters, so client might update callback later.
// The session will be completely removed when system runs low on memory.
// {@see #cleanupUnusedSessions}
params.setCustomTabsCallback(null);
} else {
cleanupSessionInternal(session);
}
}
/** Clean up all sessions which are not currently used. */
public synchronized void cleanupUnusedSessions() {
// cleanupSessionInternal modifies mSessionParams therefore we need a copy
List<CustomTabsSessionToken> sessions = new ArrayList<>(mSessionParams.keySet());
for (CustomTabsSessionToken session : sessions) {
if (mSessionParams.get(session).getCustomTabsCallback() == null) {
cleanupSessionInternal(session);
}
}
}
public void setCustomTabIsInForeground(
@Nullable CustomTabsSessionToken session, boolean isInForeground) {
callOnSession(
session,
params -> {
params.mCustomTabIsInForeground = isInForeground;
});
}
public void setEngagementSignalsCallbackForSession(
CustomTabsSessionToken session, EngagementSignalsCallback callback) {
callOnSession(session, params -> params.setEngagementSignalsCallback(callback));
}
public @Nullable EngagementSignalsCallback getEngagementSignalsCallbackForSession(
CustomTabsSessionToken session) {
return callOnSession(session, null, SessionParams::getEngagementSignalsCallback);
}
public void setEngagementSignalsAvailableSupplierForSession(
CustomTabsSessionToken session, Supplier<Boolean> supplier) {
callOnSession(session, params -> params.setEngagementSignalsAvailableSupplier(supplier));
}
public @Nullable Supplier<Boolean> getEngagementSignalsAvailableSupplierForSession(
CustomTabsSessionToken session) {
return callOnSession(session, null, SessionParams::getEngagementSignalsAvailableSupplier);
}
public @Nullable EngagementSignalsHandler getEngagementSignalsHandlerForSession(
CustomTabsSessionToken session) {
return callOnSession(session, null, SessionParams::getEngagementSignalsHandler);
}
private void logConnectionClosed(SessionParams sessionParams) {
if (sessionParams.mWasSessionDisconnectStatusLogged) return;
boolean isCustomTabInForeground = sessionParams.mCustomTabIsInForeground;
boolean isKeepAlive = sessionParams.getKeepAliveConnection() != null;
boolean isLowMemory = SysUtils.isCurrentlyLowMemory();
@SessionDisconnectStatus int status = SessionDisconnectStatus.UNKNOWN;
if (isLowMemory && isCustomTabInForeground && isKeepAlive) {
status = SessionDisconnectStatus.LOW_MEMORY_CT_FOREGROUND_KEEP_ALIVE;
} else if (isLowMemory && isCustomTabInForeground && !isKeepAlive) {
status = SessionDisconnectStatus.LOW_MEMORY_CT_FOREGROUND;
} else if (isLowMemory && !isCustomTabInForeground && isKeepAlive) {
status = SessionDisconnectStatus.LOW_MEMORY_CT_BACKGROUND_KEEP_ALIVE;
} else if (isLowMemory && !isCustomTabInForeground && !isKeepAlive) {
status = SessionDisconnectStatus.LOW_MEMORY_CT_BACKGROUND;
} else if (isCustomTabInForeground && !isKeepAlive) {
status = SessionDisconnectStatus.CT_FOREGROUND;
} else if (isCustomTabInForeground && isKeepAlive) {
status = SessionDisconnectStatus.CT_FOREGROUND_KEEP_ALIVE;
} else if (!isCustomTabInForeground && !isKeepAlive) {
status = SessionDisconnectStatus.CT_BACKGROUND;
} else if (!isCustomTabInForeground && isKeepAlive) {
status = SessionDisconnectStatus.CT_BACKGROUND_KEEP_ALIVE;
}
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.SessionDisconnectStatus", status, SessionDisconnectStatus.NUM_ENTRIES);
sessionParams.mWasSessionDisconnectStatusLogged = true;
}
private interface SessionParamsCallback<T> {
T run(SessionParams params);
}
private synchronized <T> T callOnSession(
CustomTabsSessionToken session, T fallback, SessionParamsCallback<T> callback) {
SessionParams params = mSessionParams.get(session);
if (params == null) return fallback;
return callback.run(params);
}
private interface SessionParamsRunnable {
void run(SessionParams params);
}
private synchronized <T> void callOnSession(
CustomTabsSessionToken session, SessionParamsRunnable runnable) {
SessionParams params = mSessionParams.get(session);
if (params == null) return;
runnable.run(params);
}
}