blob: 5f440b189042cfe84644dd3cb8191f6fad7c6de0 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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.app.ActivityManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
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.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsCallback;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsSessionToken;
import android.support.customtabs.PostMessageServiceConnection;
import android.text.TextUtils;
import android.widget.RemoteViews;
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.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TimeUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.CachedMetrics.EnumeratedHistogramSample;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.browserservices.BrowserSessionContentUtils;
import org.chromium.chrome.browser.browserservices.Origin;
import org.chromium.chrome.browser.browserservices.PostMessageHandler;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleLoader;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleMetrics;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.init.ChainedTasks;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.metrics.PageLoadMetrics;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.ChildProcessLauncherHelper;
import org.chromium.content_public.browser.UiThreadTaskTraits;
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.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.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Implementation of the ICustomTabsService interface.
*
* Note: This class is meant to be package private, and is public to be
* accessible from {@link ChromeApplication}.
*/
@JNINamespace("customtabs")
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";
// For CustomTabs.SpeculationStatusOnStart, see tools/metrics/enums.xml. Append only.
private static final int SPECULATION_STATUS_ON_START_ALLOWED = 0;
// What kind of speculation was started, counted in addition to
// SPECULATION_STATUS_ALLOWED.
private static final int SPECULATION_STATUS_ON_START_PREFETCH = 1;
private static final int SPECULATION_STATUS_ON_START_PRERENDER = 2;
private static final int SPECULATION_STATUS_ON_START_BACKGROUND_TAB = 3;
private static final int SPECULATION_STATUS_ON_START_PRERENDER_NOT_STARTED = 4;
// The following describe reasons why a speculation was not allowed, and are
// counted instead of SPECULATION_STATUS_ALLOWED.
private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_DEVICE_CLASS = 5;
private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_BLOCK_3RD_PARTY_COOKIES = 6;
private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_PREDICTION_DISABLED =
7;
private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_DATA_REDUCTION_ENABLED = 8;
private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_METERED = 9;
private static final int SPECULATION_STATUS_ON_START_MAX = 10;
// For CustomTabs.SpeculationStatusOnSwap, see tools/metrics/enums.xml. Append only.
private static final int SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_TAKEN = 0;
private static final int SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_NOT_MATCHED = 1;
private static final int SPECULATION_STATUS_ON_SWAP_PRERENDER_TAKEN = 2;
private static final int SPECULATION_STATUS_ON_SWAP_PRERENDER_NOT_MATCHED = 3;
private static final int SPECULATION_STATUS_ON_SWAP_MAX = 4;
// 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 = "android.support.customtabs.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 =
"android.support.customtabs.RESOURCE_PREFETCH_URL_LIST";
@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 EnumeratedHistogramSample sParallelRequestStatusOnStart =
new EnumeratedHistogramSample(
"CustomTabs.ParallelRequestStatusOnStart", ParallelRequestStatus.NUM_ENTRIES);
private static CustomTabsConnection sInstance;
private @Nullable String mTrustedPublisherUrlPackage;
private final HiddenTabHolder mHiddenTabHolder = new HiddenTabHolder();
/** @deprecated Use {@link ContextUtils} instead */
protected final Context mContext;
@VisibleForTesting
final ClientManager mClientManager;
protected final boolean mLogRequests;
private final AtomicBoolean mWarmupHasBeenCalled = new AtomicBoolean();
private final AtomicBoolean mWarmupHasBeenFinished = new AtomicBoolean();
@Nullable
private Callback<CustomTabsSessionToken> mDisconnectCallback;
// Conversion between native TimeTicks and SystemClock.uptimeMillis().
private long mNativeTickOffsetUs;
private boolean mNativeTickOffsetUsComputed;
private volatile ChainedTasks mWarmupTasks;
private @Nullable ModuleLoader mModuleLoader;
/**
* <strong>DO NOT CALL</strong>
* Public to be instanciable from {@link ChromeApplication}. This is however
* intended to be private.
*/
public CustomTabsConnection() {
super();
mContext = ContextUtils.getApplicationContext();
mClientManager = new ClientManager();
mLogRequests = CommandLine.getInstance().hasSwitch(LOG_SERVICE_REQUESTS);
}
/**
* @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) {
json.put(key, bundleToJson((Bundle) o));
} 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);
}
}
};
PostMessageServiceConnection serviceConnection = new PostMessageServiceConnection(session);
PostMessageHandler handler = new PostMessageHandler(serviceConnection);
return mClientManager.newSession(
session, Binder.getCallingUid(), onDisconnect, handler, serviceConnection);
}
/**
* 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.overridePackageNameForSession(session, packageName);
}
/** Warmup activities that should only happen once. */
private static void initializeBrowser(final Context context) {
ThreadUtils.assertOnUiThread();
try {
ChromeBrowserInitializer.getInstance(context).handleSynchronousStartupWithGpuWarmUp();
} catch (ProcessInitException e) {
Log.e(TAG, "ProcessInitException while starting the browser process.");
// Cannot do anything without the native library, and cannot show a
// dialog to the user.
System.exit(-1);
}
ChildProcessLauncherHelper.warmUp(context);
}
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(() -> {
try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.initializeBrowser()")) {
initializeBrowser(ContextUtils.getApplicationContext());
ChromeBrowserInitializer.getInstance().initNetworkChangeNotifier();
mWarmupHasBeenFinished.set(true);
}
});
}
// (2)
if (mayCreateSpareWebContents && !mHiddenTabHolder.hasHiddenTab()) {
tasks.add(() -> {
// 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.get(LibraryProcessType.PROCESS_BROWSER)
.isStartupSuccessfullyCompleted()) {
return;
}
try (TraceEvent e = TraceEvent.scoped("CreateSpareWebContents")) {
WarmupManager.getInstance().createSpareWebContents();
}
});
}
// (3)
tasks.add(() -> {
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(() -> {
try (TraceEvent e = TraceEvent.scoped("WarmupInternalFinishInitialization")) {
// (4)
Profile profile = Profile.getLastUsedProfile();
WarmupManager.getInstance().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(ContextUtils.getApplicationContext());
}
});
}
tasks.add(() -> notifyWarmupIsDone(uid));
tasks.start(false);
mWarmupTasks = tasks;
return true;
}
/** @return the URL or null if it's invalid. */
private 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);
if (!allowedScheme) return false;
return true;
}
/**
* 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;
}
url = DataReductionProxySettings.getInstance().maybeRewriteWebliteUrl(url);
if (maySpeculate(session)) {
boolean canUseHiddenTab = mClientManager.getCanUseHiddenTab(session);
startSpeculation(session, url, canUseHiddenTab, extras, uid);
}
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;
WarmupManager.getInstance().createSpareWebContents();
return true;
}
private boolean preconnectUrls(List<Bundle> likelyBundles) {
boolean atLeastOneUrl = false;
if (likelyBundles == null) return false;
WarmupManager warmupManager = WarmupManager.getInstance();
Profile profile = Profile.getLastUsedProfile().getOriginalProfile();
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) {
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;
// 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.
final int uid = Binder.getCallingUid();
if (!mClientManager.updateStatsAndReturnWhetherAllowed(
session, uid, urlString, otherLikelyBundles != null)) {
return false;
}
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
doMayLaunchUrlOnUiThread(
lowConfidence, session, uid, urlString, extras, otherLikelyBundles, true);
});
return true;
}
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 Profile.getLastUsedProfile() to throw an
// exception. But the tasks have been posted by then, so reschedule ourselves, only
// once.
if (!BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
.isStartupSuccessfullyCompleted()) {
if (retryIfNotLoaded) {
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
doMayLaunchUrlOnUiThread(lowConfidence, session, uid, urlString, extras,
otherLikelyBundles, false);
});
}
return;
}
if (lowConfidence) {
lowConfidenceMayLaunchUrl(otherLikelyBundles);
} else {
highConfidenceMayLaunchUrl(session, uid, urlString, extras, otherLikelyBundles);
}
}
}
public Bundle extraCommand(String commandName, Bundle args) {
return null;
}
public boolean updateVisuals(final CustomTabsSessionToken session, Bundle bundle) {
if (mLogRequests) Log.w(TAG, "updateVisuals: %s", bundleToJson(bundle));
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 = CustomButtonParams.parseBitmapFromBundle(actionButtonBundle);
String description = CustomButtonParams.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 = CustomButtonParams.parseBitmapFromBundle(toolbarItemBundle);
if (bitmap == null) continue;
String description =
CustomButtonParams.parseDescriptionFromBundle(toolbarItemBundle);
if (description == null) continue;
ids.add(id);
descriptions.add(description);
icons.add(bitmap);
}
}
if (!ids.isEmpty()) {
result &= ThreadUtils.runOnUiThreadBlockingNoException(() -> {
boolean res = true;
for (int i = 0; i < ids.size(); i++) {
res &= BrowserSessionContentUtils.updateCustomButton(
session, 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 &= ThreadUtils.runOnUiThreadBlockingNoException(() -> {
return BrowserSessionContentUtils.updateRemoteViews(
session, remoteViews, clickableIDs, pendingIntent);
});
}
logCall("updateVisuals()", result);
return result;
}
public boolean requestPostMessageChannel(CustomTabsSessionToken session,
Origin postMessageOrigin) {
boolean success = requestPostMessageChannelInternal(session, postMessageOrigin);
logCall("requestPostMessageChannel() with origin "
+ (postMessageOrigin != null ? postMessageOrigin.toString() : ""), success);
return success;
}
private boolean requestPostMessageChannelInternal(final CustomTabsSessionToken session,
final Origin postMessageOrigin) {
if (!mWarmupHasBeenCalled.get()) return false;
if (!isCallerForegroundOrSelf() && !BrowserSessionContentUtils.isActiveSession(session)) {
return false;
}
if (!mClientManager.bindToPostMessageServiceForSession(session)) return false;
final int uid = Binder.getCallingUid();
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
// If the API is not enabled, we don't set the post message origin, which will avoid
// PostMessageHandler initialization and disallow postMessage calls.
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_POST_MESSAGE_API)) return;
// 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, CustomTabsService.RELATION_USE_AS_ORIGIN);
} else {
mClientManager.initializeWithPostMessageOriginForSession(session, verifiedOrigin);
}
});
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;
}
public int postMessage(CustomTabsSessionToken session, String message, Bundle extras) {
int result;
if (!mWarmupHasBeenCalled.get()) result = CustomTabsService.RESULT_FAILURE_DISALLOWED;
if (!isCallerForegroundOrSelf() && !BrowserSessionContentUtils.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);
}
@Nullable public 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.
*/
@Nullable public 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().runNowOrAfterNativeInitialization(
() -> handleParallelRequest(session, intent));
maybePrefetchResources(session, intent);
}
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(ContextUtils.getApplicationContext())
.hasNativeInitializationCompleted()) {
return;
}
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_REDIRECT_PRECONNECT)) 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 = new Origin(url);
if (origin == null) return;
if (!mClientManager.isFirstPartyOriginForSession(session, origin)) return;
WarmupManager.getInstance().maybePreconnectUrlAndSubResources(
Profile.getLastUsedProfile(), redirectEndpoint.toString());
}
@VisibleForTesting
@ParallelRequestStatus
int handleParallelRequest(CustomTabsSessionToken session, Intent intent) {
int status = maybeStartParallelRequest(session, intent);
sParallelRequestStatusOnStart.record(status);
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);
}
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.
*/
@ParallelRequestStatus
private int maybeStartParallelRequest(CustomTabsSessionToken session, Intent intent) {
ThreadUtils.assertOnUiThread();
if (!intent.hasExtra(PARALLEL_REQUEST_URL_KEY)) return ParallelRequestStatus.NO_REQUEST;
if (!ChromeBrowserInitializer.getInstance(ContextUtils.getApplicationContext())
.hasNativeInitializationCompleted()) {
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 < 0 || policy > ReferrerPolicy.LAST) 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();
nativeCreateAndStartDetachedResourceRequest(Profile.getLastUsedProfile(), session,
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.LAST) policy = ReferrerPolicy.DEFAULT;
if (!mClientManager.isFirstPartyOriginForSession(session, new Origin(referrer))) 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.
nativeCreateAndStartDetachedResourceRequest(Profile.getLastUsedProfile(), 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();
return mClientManager.isFirstPartyOriginForSession(session, new Origin(referrer));
}
/** See {@link ClientManager#getReferrerForSession(CustomTabsSessionToken)} */
public Referrer getReferrerForSession(CustomTabsSessionToken session) {
return mClientManager.getReferrerForSession(session);
}
/** @see ClientManager#shouldHideDomainForSession(CustomTabsSessionToken) */
public boolean shouldHideDomainForSession(CustomTabsSessionToken session) {
return mClientManager.shouldHideDomainForSession(session);
}
/** @see ClientManager#shouldHideTopBarOnModuleManagedUrlsForSession(CustomTabsSessionToken) */
public boolean shouldHideTopBarOnModuleManagedUrlsForSession(CustomTabsSessionToken session) {
return mClientManager.shouldHideTopBarOnModuleManagedUrlsForSession(session);
}
/** @see ClientManager#shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken) */
public boolean shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session) {
return mClientManager.shouldSpeculateLoadOnCellularForSession(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);
}
@VisibleForTesting
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
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);
}
/**
* Extracts the creator package name from the intent.
* @param intent The intent to get the package name from.
* @return the package name which can be null.
*/
String extractCreatorPackage(Intent intent) {
return null;
}
/**
* 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) {}
// TODO(yfriedman): Remove when internal code is deleted.
public void sendNavigationInfo(
CustomTabsSessionToken session, String url, String title, Bitmap 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);
}
}
/**
* Notifies the application of a navigation event.
*
* 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}
* @return true for success.
*/
public boolean notifyNavigationEvent(CustomTabsSessionToken session, int navigationEvent) {
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return false;
try {
callback.onNavigationEvent(
navigationEvent, getExtrasBundleForNavigationEventForSession(session));
} 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;
}
/**
* @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();
// Notifies all the sessions, as warmup() is tied to a UID, not a session.
for (CustomTabsSessionToken session : mClientManager.uidToSessions(uid)) {
safeExtraCallback(session, ON_WARMUP_COMPLETED, null);
}
}
/**
* Notifies the application of a page load metric for a single metric.
*
* TODD(lizeb): Move this to a proper method in {@link CustomTabsCallback} once one is
* available.
*
* @param session Session identifier.
* @param metricName Name of the page load metric.
* @param navigationStartTick Absolute navigation start time, as TimeTicks taken from native.
* @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 navigationStartTick, long offsetMs) {
if (!mClientManager.shouldGetPageLoadMetrics(session)) return false;
if (!mNativeTickOffsetUsComputed) {
// Compute offset from time ticks to uptimeMillis.
mNativeTickOffsetUsComputed = true;
long nativeNowUs = TimeUtils.nativeGetTimeTicksNowUs();
long javaNowUs = SystemClock.uptimeMillis() * 1000;
mNativeTickOffsetUs = nativeNowUs - javaNowUs;
}
Bundle args = new Bundle();
args.putLong(metricName, offsetMs);
// SystemClock.uptimeMillis() is used here as it (as of June 2017) uses the same system call
// as all the native side of Chrome, that is clock_gettime(CLOCK_MONOTONIC). Meaning that
// the offset relative to navigationStart is to be compared with a
// SystemClock.uptimeMillis() value.
args.putLong(PageLoadMetrics.NAVIGATION_START,
(navigationStartTick - mNativeTickOffsetUs) / 1000);
return notifyPageLoadMetrics(session, args);
}
/**
* 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 (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.
* @return true if success. To protect Chrome exceptions in the client application are swallowed
* and false is returned.
*/
boolean notifyOpenInBrowser(CustomTabsSessionToken session) {
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.
*/
private boolean safeExtraCallback(
CustomTabsSessionToken session, String callbackName, @Nullable Bundle args) {
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return false;
try {
callback.extraCallback(callbackName, args);
} catch (Exception e) {
return false;
}
return true;
}
/**
* 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);
}
/**
* @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";
// Reading from /proc does not cause disk IO, but strict mode doesn't like it.
// crbug.com/567143
try (StrictModeContext ctx = StrictModeContext.allowDiskReads();
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. We use a workaround in this case.
boolean useWorkaround = true;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
do {
ActivityManager am =
(ActivityManager) ContextUtils.getApplicationContext().getSystemService(
Context.ACTIVITY_SERVICE);
// Extra paranoia here and below, some L 5.0.x devices seem to throw NPE somewhere
// in this code.
// See https://crbug.com/654705.
if (am == null) break;
List<ActivityManager.RunningAppProcessInfo> running = am.getRunningAppProcesses();
if (running == null) break;
for (ActivityManager.RunningAppProcessInfo rpi : running) {
if (rpi == null) continue;
boolean matchingUid = rpi.uid == uid;
boolean isForeground = rpi.importance
== ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
useWorkaround &= !matchingUid;
if (matchingUid && isForeground) return true;
}
} while (false);
}
return useWorkaround ? !isBackgroundProcess(Binder.getCallingPid()) : false;
}
@VisibleForTesting
void cleanupAll() {
ThreadUtils.assertOnUiThread();
mClientManager.cleanupAll();
}
/**
* Handle any clean up left after a session is destroyed.
* @param session The session that has been destroyed.
*/
@VisibleForTesting
void cleanUpSession(final CustomTabsSessionToken session) {
ThreadUtils.runOnUiThread(() -> 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 (ChromeApplication.isSevereMemorySignal(level)) {
getInstance().mClientManager.cleanupUnusedSessions();
}
if (getInstance().mModuleLoader != null) getInstance().mModuleLoader.onTrimMemory(level);
}
@VisibleForTesting
int maySpeculateWithResult(CustomTabsSessionToken session) {
if (!DeviceClassManager.enablePrerendering()) {
return SPECULATION_STATUS_ON_START_NOT_ALLOWED_DEVICE_CLASS;
}
PrefServiceBridge prefs = PrefServiceBridge.getInstance();
if (prefs.isBlockThirdPartyCookiesEnabled()) {
return SPECULATION_STATUS_ON_START_NOT_ALLOWED_BLOCK_3RD_PARTY_COOKIES;
}
// TODO(yusufo): The check for prerender in PrivacyPreferencesManager now checks for the
// network connection type as well, we should either change that or add another check for
// custom tabs. Then PrivacyManager should be used to make the below check.
if (!prefs.getNetworkPredictionEnabled()) {
return SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_PREDICTION_DISABLED;
}
if (DataReductionProxySettings.getInstance().isDataReductionProxyEnabled()
&& !ChromeFeatureList.isEnabled(
ChromeFeatureList.PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES)) {
return SPECULATION_STATUS_ON_START_NOT_ALLOWED_DATA_REDUCTION_ENABLED;
}
ConnectivityManager cm =
(ConnectivityManager) ContextUtils.getApplicationContext().getSystemService(
Context.CONNECTIVITY_SERVICE);
if (cm.isActiveNetworkMetered() && !shouldSpeculateLoadOnCellularForSession(session)
&& !ChromeFeatureList.isEnabled(
ChromeFeatureList.PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES)) {
return SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_METERED;
}
return SPECULATION_STATUS_ON_START_ALLOWED;
}
boolean maySpeculate(CustomTabsSessionToken session) {
int speculationResult = maySpeculateWithResult(session);
recordSpeculationStatusOnStart(speculationResult);
return speculationResult == SPECULATION_STATUS_ON_START_ALLOWED;
}
/** 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) {
WarmupManager warmupManager = WarmupManager.getInstance();
Profile profile = Profile.getLastUsedProfile();
// At most one on-going speculation, clears the previous one.
cancelSpeculation(null);
if (useHiddenTab) {
recordSpeculationStatusOnStart(SPECULATION_STATUS_ON_START_BACKGROUND_TAB);
launchUrlInHiddenTab(session, url, extras);
} else {
warmupManager.createSpareWebContents();
}
warmupManager.maybePreconnectUrlAndSubResources(profile, url);
}
/**
* Creates a hidden tab and initiates a navigation.
*/
private void launchUrlInHiddenTab(CustomTabsSessionToken session, String url,
@Nullable Bundle extras) {
ThreadUtils.assertOnUiThread();
mHiddenTabHolder.launchUrlInHiddenTab(session, mClientManager, url, extras);
}
@VisibleForTesting
void resetThrottling(int uid) {
mClientManager.resetThrottling(uid);
}
@VisibleForTesting
void ban(int uid) {
mClientManager.ban(uid);
}
/**
* Get any referrer that has been explicitly set.
*
* Inspects the two possible sources for the referrer:
* - A session for which the referrer might have been set.
* - An intent for a navigation that contains a referer in the headers.
*
* @param session session to inspect for referrer settings.
* @param intent intent to inspect for referrer header.
* @return referrer URL as a string if any was found, empty string otherwise.
*/
public String getReferrer(CustomTabsSessionToken session, Intent intent) {
String referrer = IntentHandler.getReferrerUrlIncludingExtraHeaders(intent);
if (referrer == null && getReferrerForSession(session) != null) {
referrer = getReferrerForSession(session).getUrl();
}
if (referrer == null) referrer = "";
return referrer;
}
/**
* @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;
}
void setTrustedPublisherUrlPackageForTest(@Nullable String packageName) {
mTrustedPublisherUrlPackage = packageName;
}
private static void recordSpeculationStatusOnStart(int status) {
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.SpeculationStatusOnStart", status, SPECULATION_STATUS_ON_START_MAX);
}
private static void recordSpeculationStatusOnSwap(int status) {
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.SpeculationStatusOnSwap", status, SPECULATION_STATUS_ON_SWAP_MAX);
}
static void recordSpeculationStatusSwapTabTaken() {
recordSpeculationStatusOnSwap(SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_TAKEN);
}
static void recordSpeculationStatusSwapTabNotMatched() {
recordSpeculationStatusOnSwap(SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_NOT_MATCHED);
}
private static native void nativeCreateAndStartDetachedResourceRequest(Profile profile,
CustomTabsSessionToken session, String url, String origin, int referrerPolicy,
@DetachedResourceRequestMotivation int motivation);
public ModuleLoader getModuleLoader(ComponentName componentName, int resourceId) {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_MODULE_DEX_LOADING)) {
resourceId = 0;
}
if (mModuleLoader != null &&
(!componentName.equals(mModuleLoader.getComponentName()) ||
resourceId != mModuleLoader.getDexResourceId())) {
mModuleLoader.destroyModule(ModuleMetrics.DestructionReason.MODULE_LOADER_CHANGED);
mModuleLoader = null;
}
if (mModuleLoader == null) mModuleLoader = new ModuleLoader(componentName, resourceId);
return mModuleLoader;
}
@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);
getInstance().safeExtraCallback(session, ON_DETACHED_REQUEST_COMPLETED, args);
}
@VisibleForTesting
@Nullable HiddenTabHolder.SpeculationParams getSpeculationParamsForTesting() {
return mHiddenTabHolder.getSpeculationParamsForTesting();
}
}