// 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.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.os.StrictMode;
import android.support.customtabs.CustomTabsCallback;
import android.support.customtabs.CustomTabsIntent;
import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsSessionToken;
import android.text.TextUtils;
import android.widget.RemoteViews;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.SysUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.WarmupManager;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.prerender.ExternalPrerenderHandler;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.content.browser.ChildProcessCreationParams;
import org.chromium.content.browser.ChildProcessLauncher;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.Referrer;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Implementation of the ICustomTabsConnectionService interface.
*
* Note: This class is meant to be package private, and is public to be
* accessible from {@link ChromeApplication}.
*/
public class CustomTabsConnection {
private static final String TAG = "ChromeConnection";
private static final String LOG_SERVICE_REQUESTS = "custom-tabs-log-service-requests";
@VisibleForTesting
static final String PAGE_LOAD_METRICS_CALLBACK = "NavigationMetrics";
@VisibleForTesting
static final String NO_PRERENDERING_KEY =
"android.support.customtabs.maylaunchurl.NO_PRERENDERING";
private static AtomicReference<CustomTabsConnection> sInstance =
new AtomicReference<CustomTabsConnection>();
@VisibleForTesting
static final class PrerenderedUrlParams {
public final CustomTabsSessionToken mSession;
public final WebContents mWebContents;
public final String mUrl;
public final String mReferrer;
public final Bundle mExtras;
PrerenderedUrlParams(CustomTabsSessionToken session, WebContents webContents,
String url, String referrer, Bundle extras) {
mSession = session;
mWebContents = webContents;
mUrl = url;
mReferrer = referrer;
mExtras = extras;
}
}
@VisibleForTesting
PrerenderedUrlParams mPrerender;
protected final Application mApplication;
protected final ClientManager mClientManager;
private final boolean mLogRequests;
private final AtomicBoolean mWarmupHasBeenCalled = new AtomicBoolean();
private final AtomicBoolean mWarmupHasBeenFinished = new AtomicBoolean();
private ExternalPrerenderHandler mExternalPrerenderHandler;
/**
* <strong>DO NOT CALL</strong>
* Public to be instanciable from {@link ChromeApplication}. This is however
* intended to be private.
*/
public CustomTabsConnection(Application application) {
super();
mApplication = application;
mClientManager = new ClientManager(mApplication);
mLogRequests = CommandLine.getInstance().hasSwitch(LOG_SERVICE_REQUESTS);
}
/**
* @return The unique instance of ChromeCustomTabsConnection.
*/
@SuppressFBWarnings("BC_UNCONFIRMED_CAST")
public static CustomTabsConnection getInstance(Application application) {
if (sInstance.get() == null) {
ChromeApplication chromeApplication = (ChromeApplication) application;
chromeApplication.initCommandLine();
sInstance.compareAndSet(null, chromeApplication.createCustomTabsConnection());
}
return sInstance.get();
}
/**
* 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 success Whether the call was successful.
*/
void logCall(String name, boolean success) {
if (mLogRequests) {
Log.w(TAG, "%s = %b, Calling UID = %d", name, success, Binder.getCallingUid());
}
}
public boolean newSession(CustomTabsSessionToken session) {
boolean success = newSessionInternal(session);
logCall("newSession()", success);
return success;
}
private boolean newSessionInternal(CustomTabsSessionToken session) {
ClientManager.DisconnectCallback onDisconnect = new ClientManager.DisconnectCallback() {
@Override
public void run(CustomTabsSessionToken session) {
cancelPrerender(session);
}
};
return mClientManager.newSession(session, Binder.getCallingUid(), onDisconnect);
}
/** Warmup activities that should only happen once. */
@SuppressFBWarnings("DM_EXIT")
private static void initializeBrowser(final Application app) {
ThreadUtils.assertOnUiThread();
try {
ChromeBrowserInitializer.getInstance(app).handleSynchronousStartup();
} 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);
}
final Context context = app.getApplicationContext();
final ChromeApplication chrome = (ChromeApplication) context;
ChildProcessCreationParams.set(chrome.getChildProcessCreationParams());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ChildProcessLauncher.warmUp(context);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
ChromeBrowserInitializer.initNetworkChangeNotifier(context);
WarmupManager.getInstance().initializeViewHierarchy(
context, R.layout.custom_tabs_control_container);
}
public boolean warmup(long flags) {
boolean success = warmupInternal(true);
logCall("warmup()", success);
return success;
}
/**
* @return Whether {@link CustomTabsConnection#warmup(long)} has been called.
*/
public static boolean hasWarmUpBeenFinished(Application application) {
return getInstance(application).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;
mClientManager.recordUidHasCalledWarmup(Binder.getCallingUid());
final boolean initialized = !mWarmupHasBeenCalled.compareAndSet(false, true);
// The call is non-blocking and this must execute on the UI thread, post a task.
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
if (!initialized) initializeBrowser(mApplication);
if (mayCreateSpareWebContents && mPrerender == null && !SysUtils.isLowEndDevice()) {
WarmupManager.getInstance().createSpareWebContents();
}
mWarmupHasBeenFinished.set(true);
}
});
return true;
}
/** @return the URL converted to string, or null if it's invalid. */
private static String checkAndConvertUri(Uri uri) {
if (uri == null) return null;
// 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("http") || scheme.equals("https");
if (!allowedScheme) return null;
return uri.toString();
}
/**
* High confidence mayLaunchUrl() call, that is:
* - Tries to prerender if possible.
* - An empty URL cancels the current prerender if any.
* - If prerendering is not possible, makes sure that there is a spare renderer.
*/
private void highConfidenceMayLaunchUrl(CustomTabsSessionToken session,
int uid, String url, Bundle extras, List<Bundle> otherLikelyBundles) {
ThreadUtils.assertOnUiThread();
if (TextUtils.isEmpty(url)) {
cancelPrerender(session);
return;
}
url = DataReductionProxySettings.getInstance().maybeRewriteWebliteUrl(url);
boolean noPrerendering =
extras != null ? extras.getBoolean(NO_PRERENDERING_KEY, false) : false;
WarmupManager.getInstance().maybePreconnectUrlAndSubResources(
Profile.getLastUsedProfile(), url);
boolean didStartPrerender = false;
if (!noPrerendering && mayPrerender(session)) {
didStartPrerender = prerenderUrl(session, url, extras, uid);
}
preconnectUrls(otherLikelyBundles);
if (!didStartPrerender) WarmupManager.getInstance().createSpareWebContents();
}
/**
* 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();
for (Bundle bundle : likelyBundles) {
Uri uri;
try {
uri = IntentUtils.safeGetParcelable(bundle, CustomTabsService.KEY_URL);
} catch (ClassCastException e) {
continue;
}
String url = checkAndConvertUri(uri);
if (url != null) {
warmupManager.maybePreconnectUrlAndSubResources(profile, url);
atLeastOneUrl = true;
}
}
return atLeastOneUrl;
}
public boolean mayLaunchUrl(CustomTabsSessionToken session, Uri url, Bundle extras,
List<Bundle> otherLikelyBundles) {
boolean success = mayLaunchUrlInternal(session, url, extras, otherLikelyBundles);
logCall("mayLaunchUrl()", success);
return success;
}
private boolean mayLaunchUrlInternal(final CustomTabsSessionToken session, Uri url,
final Bundle extras, final List<Bundle> otherLikelyBundles) {
final boolean lowConfidence =
(url == null || TextUtils.isEmpty(url.toString())) && otherLikelyBundles != null;
final String urlString = checkAndConvertUri(url);
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();
// TODO(lizeb): Also throttle low-confidence mode.
if (!lowConfidence
&& !mClientManager.updateStatsAndReturnWhetherAllowed(session, uid, urlString)) {
return false;
}
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
if (lowConfidence) {
lowConfidenceMayLaunchUrl(otherLikelyBundles);
} else {
highConfidenceMayLaunchUrl(session, uid, urlString, extras, otherLikelyBundles);
}
}
});
return true;
}
public Bundle extraCommand(String commandName, Bundle args) {
return null;
}
public boolean updateVisuals(final CustomTabsSessionToken session, Bundle bundle) {
final Bundle actionButtonBundle = IntentUtils.safeGetBundle(bundle,
CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE);
boolean result = true;
if (actionButtonBundle != null) {
final int id = IntentUtils.safeGetInt(actionButtonBundle, CustomTabsIntent.KEY_ID,
CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID);
final Bitmap bitmap = CustomButtonParams.parseBitmapFromBundle(actionButtonBundle);
final String description = CustomButtonParams
.parseDescriptionFromBundle(actionButtonBundle);
if (bitmap != null && description != null) {
try {
result &= ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return CustomTabActivity.updateCustomButton(session, id,
bitmap, description);
}
});
} catch (ExecutionException e) {
result = false;
}
}
}
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);
try {
result &= ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return CustomTabActivity.updateRemoteViews(session,
remoteViews, clickableIDs, pendingIntent);
}
});
} catch (ExecutionException e) {
result = false;
}
}
return result;
}
/**
* 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);
}
/**
* Transfers a prerendered WebContents if one exists.
*
* This resets the internal WebContents; a subsequent call to this method
* returns null. Must be called from the UI thread.
* If a prerender exists for a different URL with the same sessionId or with
* a different referrer, then this is treated as a mispredict from the
* client application, and cancels the previous prerender. This is done to
* avoid keeping resources laying around for too long, but is subject to a
* race condition, as the following scenario is possible:
* The application calls:
* 1. mayLaunchUrl(url1) <- IPC
* 2. loadUrl(url2) <- Intent
* 3. mayLaunchUrl(url3) <- IPC
* If the IPC for url3 arrives before the intent for url2, then this methods
* cancels the prerender for url3, which is unexpected. On the other
* hand, not cancelling the previous prerender leads to wasted resources, as
* a WebContents is lingering. This can be solved by requiring applications
* to call mayLaunchUrl(null) to cancel a current prerender before 2, that
* is for a mispredict.
*
* Note that this methods accepts URLs that don't exactly match the initially
* prerendered URL. More precisely, the #fragment is ignored. In this case,
* the client needs to navigate to the correct URL after the WebContents
* swap. This can be tested using {@link UrlUtilities#urlsFragmentsDiffer()}.
*
* @param session The Binder object identifying a session.
* @param url The URL the WebContents is for.
* @param referrer The referrer to use for |url|.
* @return The prerendered WebContents, or null.
*/
WebContents takePrerenderedUrl(CustomTabsSessionToken session, String url, String referrer) {
ThreadUtils.assertOnUiThread();
if (mPrerender == null || session == null || !session.equals(mPrerender.mSession)) {
return null;
}
WebContents webContents = mPrerender.mWebContents;
String prerenderedUrl = mPrerender.mUrl;
String prerenderReferrer = mPrerender.mReferrer;
if (referrer == null) referrer = "";
boolean ignoreFragments = mClientManager.getIgnoreFragmentsForSession(session);
boolean urlsMatch = TextUtils.equals(prerenderedUrl, url)
|| (ignoreFragments
&& UrlUtilities.urlsMatchIgnoringFragments(prerenderedUrl, url));
WebContents result = null;
if (urlsMatch && TextUtils.equals(prerenderReferrer, referrer)) {
result = webContents;
mPrerender = null;
} else {
cancelPrerender(session);
}
if (!mClientManager.usesDefaultSessionParameters(session) && webContents != null) {
RecordHistogram.recordBooleanHistogram(
"CustomTabs.NonDefaultSessionPrerenderMatched", result != null);
}
return result;
}
/** Returns the URL prerendered for a session, or null. */
String getPrerenderedUrl(CustomTabsSessionToken session) {
if (mPrerender == null || session == null || !session.equals(mPrerender.mSession)) {
return null;
}
return mPrerender.mUrl;
}
/** 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#shouldPrerenderOnCellularForSession(CustomTabsSessionToken) */
public boolean shouldPrerenderOnCellularForSession(CustomTabsSessionToken session) {
return mClientManager.shouldPrerenderOnCellularForSession(session);
}
/** @see ClientManager#shouldSendNavigationInfoForSession(CustomTabsSessionToken) */
public boolean shouldSendNavigationInfoForSession(CustomTabsSessionToken session) {
return mClientManager.shouldSendNavigationInfoForSession(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 setShouldPrerenderOnCellularForSession(CustomTabsSessionToken session, boolean value) {
mClientManager.setPrerenderCellularForSession(session, value);
}
/**
* 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} about the first run result if necessary.
* @param intent The initial VIEW intent that initiated first run.
* @param resultOK Whether first run was successful.
*/
public void sendFirstRunCallbackIfNecessary(Intent intent, 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 screenshot A screenshot of the tab contents.
*/
public void sendNavigationInfo(
CustomTabsSessionToken session, String url, String title, Bitmap screenshot) { }
/**
* Notifies the application of a navigation event.
*
* Delivers the {@link CustomTabsConnectionCallback#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.
*/
boolean notifyNavigationEvent(CustomTabsSessionToken session, int navigationEvent) {
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return false;
try {
callback.onNavigationEvent(navigationEvent, null);
} 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;
}
return true;
}
/**
* Notifies the application of a page load 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 offsetMs Offset in ms from navigationStart.
*/
boolean notifyPageLoadMetric(CustomTabsSessionToken session, String metricName, long offsetMs) {
CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
if (callback == null) return false;
Bundle args = new Bundle();
args.putLong(metricName, offsetMs);
try {
callback.extraCallback(PAGE_LOAD_METRICS_CALLBACK, args);
} catch (Exception e) {
// Pokemon exception handling, see above and crbug.com/517023.
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 dontKeepAliveForSessionId()} 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 keepAliveForSessionId}, 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 two cgroups for the processes: the root cgroup, and the
// "/bg_non_interactive" one for background processes. 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";
// Reading from /proc does not cause disk IO, but strict mode doesn't like it.
// crbug.com/567143
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
FileReader fileReader = new FileReader(cgroupFilename);
BufferedReader reader = new BufferedReader(fileReader);
try {
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("cpu")) return fields[2];
}
} finally {
reader.close();
}
} catch (IOException e) {
return null;
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
return null;
}
private static boolean isBackgroundProcess(int pid) {
String schedulerGroup = getSchedulerGroup(pid);
// "/bg_non_interactive" is from L MR1, "/apps/bg_non_interactive" before.
return "/bg_non_interactive".equals(schedulerGroup)
|| "/apps/bg_non_interactive".equals(schedulerGroup);
}
/**
* @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) mApplication.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(new Runnable() {
@Override
public void run() {
mClientManager.cleanupSession(session);
}
});
}
private boolean mayPrerender(CustomTabsSessionToken session) {
if (!DeviceClassManager.enablePrerendering()) return false;
// TODO(yusufo): The check for prerender in PrivacyManager 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 (!PrefServiceBridge.getInstance().getNetworkPredictionEnabled()) return false;
if (DataReductionProxySettings.getInstance().isDataReductionProxyEnabled()) return false;
ConnectivityManager cm =
(ConnectivityManager) mApplication.getApplicationContext().getSystemService(
Context.CONNECTIVITY_SERVICE);
return !cm.isActiveNetworkMetered() || shouldPrerenderOnCellularForSession(session);
}
/** Cancels a prerender for a given session, or any session if null. */
void cancelPrerender(CustomTabsSessionToken session) {
ThreadUtils.assertOnUiThread();
if (mPrerender != null && (session == null || session.equals(mPrerender.mSession))) {
mExternalPrerenderHandler.cancelCurrentPrerender();
mPrerender.mWebContents.destroy();
mPrerender = null;
}
}
/**
* Tries to request a prerender for a given URL.
*
* @param session Session the request comes from.
* @param url URL to prerender.
* @param extras extra parameters.
* @param uid UID of the caller.
* @return true if a prerender has been initiated.
*/
private boolean prerenderUrl(
CustomTabsSessionToken session, String url, Bundle extras, int uid) {
ThreadUtils.assertOnUiThread();
// Ignores mayPrerender() for an empty URL, since it cancels an existing prerender.
if (!mayPrerender(session) && !TextUtils.isEmpty(url)) return false;
if (!mWarmupHasBeenCalled.get()) return false;
// Last one wins and cancels the previous prerender.
cancelPrerender(null);
if (TextUtils.isEmpty(url)) return false;
boolean throttle = !shouldPrerenderOnCellularForSession(session);
if (throttle && !mClientManager.isPrerenderingAllowed(uid)) return false;
// A prerender will be requested. Time to destroy the spare WebContents.
WarmupManager.getInstance().destroySpareWebContents();
Intent extrasIntent = new Intent();
if (extras != null) extrasIntent.putExtras(extras);
if (IntentHandler.getExtraHeadersFromIntent(extrasIntent) != null) return false;
if (mExternalPrerenderHandler == null) {
mExternalPrerenderHandler = new ExternalPrerenderHandler();
}
Rect contentBounds = ExternalPrerenderHandler.estimateContentSize(mApplication, true);
Context context = mApplication.getApplicationContext();
String referrer = IntentHandler.getReferrerUrlIncludingExtraHeaders(extrasIntent, context);
if (referrer == null && getReferrerForSession(session) != null) {
referrer = getReferrerForSession(session).getUrl();
}
if (referrer == null) referrer = "";
WebContents webContents = mExternalPrerenderHandler.addPrerender(
Profile.getLastUsedProfile(), url, referrer,
contentBounds,
shouldPrerenderOnCellularForSession(session));
if (webContents == null) return false;
if (throttle) mClientManager.registerPrerenderRequest(uid, url);
mPrerender = new PrerenderedUrlParams(session, webContents, url, referrer, extras);
RecordHistogram.recordBooleanHistogram("CustomTabs.PrerenderSessionUsesDefaultParameters",
mClientManager.usesDefaultSessionParameters(session));
return true;
}
@VisibleForTesting
void resetThrottling(Context context, int uid) {
mClientManager.resetThrottling(uid);
}
@VisibleForTesting
void ban(Context context, int uid) {
mClientManager.ban(uid);
}
}