1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.customtabs;
6 
7 import static org.chromium.components.content_settings.PrefNames.COOKIE_CONTROLS_MODE;
8 
9 import android.app.ActivityManager;
10 import android.app.PendingIntent;
11 import android.content.Context;
12 import android.content.Intent;
13 import android.content.pm.PackageManager;
14 import android.graphics.Bitmap;
15 import android.net.ConnectivityManager;
16 import android.net.Uri;
17 import android.os.Binder;
18 import android.os.Build;
19 import android.os.Bundle;
20 import android.os.Process;
21 import android.os.SystemClock;
22 import android.text.TextUtils;
23 import android.widget.RemoteViews;
24 
25 import androidx.annotation.IntDef;
26 import androidx.annotation.Nullable;
27 import androidx.annotation.VisibleForTesting;
28 import androidx.browser.customtabs.CustomTabsCallback;
29 import androidx.browser.customtabs.CustomTabsIntent;
30 import androidx.browser.customtabs.CustomTabsService;
31 import androidx.browser.customtabs.CustomTabsSessionToken;
32 import androidx.browser.customtabs.PostMessageServiceConnection;
33 
34 import org.json.JSONException;
35 import org.json.JSONObject;
36 
37 import org.chromium.base.Callback;
38 import org.chromium.base.CommandLine;
39 import org.chromium.base.ContextUtils;
40 import org.chromium.base.IntentUtils;
41 import org.chromium.base.Log;
42 import org.chromium.base.StrictModeContext;
43 import org.chromium.base.SysUtils;
44 import org.chromium.base.ThreadUtils;
45 import org.chromium.base.TimeUtilsJni;
46 import org.chromium.base.TraceEvent;
47 import org.chromium.base.annotations.CalledByNative;
48 import org.chromium.base.annotations.JNINamespace;
49 import org.chromium.base.annotations.NativeMethods;
50 import org.chromium.base.metrics.RecordHistogram;
51 import org.chromium.base.task.PostTask;
52 import org.chromium.base.task.TaskTraits;
53 import org.chromium.chrome.R;
54 import org.chromium.chrome.browser.AppHooks;
55 import org.chromium.chrome.browser.ChromeApplication;
56 import org.chromium.chrome.browser.IntentHandler;
57 import org.chromium.chrome.browser.WarmupManager;
58 import org.chromium.chrome.browser.browserservices.PostMessageHandler;
59 import org.chromium.chrome.browser.browserservices.SessionDataHolder;
60 import org.chromium.chrome.browser.browserservices.SessionHandler;
61 import org.chromium.chrome.browser.device.DeviceClassManager;
62 import org.chromium.chrome.browser.flags.ChromeFeatureList;
63 import org.chromium.chrome.browser.init.ChainedTasks;
64 import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
65 import org.chromium.chrome.browser.metrics.PageLoadMetrics;
66 import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
67 import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManager;
68 import org.chromium.chrome.browser.profiles.Profile;
69 import org.chromium.chrome.browser.tab.Tab;
70 import org.chromium.components.content_settings.CookieControlsMode;
71 import org.chromium.components.embedder_support.util.Origin;
72 import org.chromium.components.embedder_support.util.UrlConstants;
73 import org.chromium.components.user_prefs.UserPrefs;
74 import org.chromium.content_public.browser.BrowserStartupController;
75 import org.chromium.content_public.browser.ChildProcessLauncherHelper;
76 import org.chromium.content_public.browser.UiThreadTaskTraits;
77 import org.chromium.content_public.browser.WebContents;
78 import org.chromium.content_public.common.Referrer;
79 import org.chromium.network.mojom.ReferrerPolicy;
80 
81 import java.io.BufferedReader;
82 import java.io.File;
83 import java.io.FileReader;
84 import java.io.IOException;
85 import java.lang.annotation.Retention;
86 import java.lang.annotation.RetentionPolicy;
87 import java.util.ArrayList;
88 import java.util.Arrays;
89 import java.util.HashSet;
90 import java.util.List;
91 import java.util.Set;
92 import java.util.concurrent.atomic.AtomicBoolean;
93 
94 /**
95  * Implementation of the ICustomTabsService interface.
96  *
97  * Note: This class is meant to be package private, and is public to be
98  * accessible from {@link ChromeApplication}.
99  */
100 @JNINamespace("customtabs")
101 public class CustomTabsConnection {
102     private static final String TAG = "ChromeConnection";
103     private static final String LOG_SERVICE_REQUESTS = "custom-tabs-log-service-requests";
104 
105     // Callback names for |extraCallback()|.
106     @VisibleForTesting
107     static final String PAGE_LOAD_METRICS_CALLBACK = "NavigationMetrics";
108     static final String BOTTOM_BAR_SCROLL_STATE_CALLBACK = "onBottomBarScrollStateChanged";
109     @VisibleForTesting
110     static final String OPEN_IN_BROWSER_CALLBACK = "onOpenInBrowser";
111     @VisibleForTesting
112     static final String ON_WARMUP_COMPLETED = "onWarmupCompleted";
113     @VisibleForTesting
114     static final String ON_DETACHED_REQUEST_REQUESTED = "onDetachedRequestRequested";
115     @VisibleForTesting
116     static final String ON_DETACHED_REQUEST_COMPLETED = "onDetachedRequestCompleted";
117 
118     // For CustomTabs.SpeculationStatusOnStart, see tools/metrics/enums.xml. Append only.
119     private static final int SPECULATION_STATUS_ON_START_ALLOWED = 0;
120     // What kind of speculation was started, counted in addition to
121     // SPECULATION_STATUS_ALLOWED.
122     private static final int SPECULATION_STATUS_ON_START_PREFETCH = 1;
123     private static final int SPECULATION_STATUS_ON_START_PRERENDER = 2;
124     private static final int SPECULATION_STATUS_ON_START_BACKGROUND_TAB = 3;
125     private static final int SPECULATION_STATUS_ON_START_PRERENDER_NOT_STARTED = 4;
126     // The following describe reasons why a speculation was not allowed, and are
127     // counted instead of SPECULATION_STATUS_ALLOWED.
128     private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_DEVICE_CLASS = 5;
129     private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_BLOCK_3RD_PARTY_COOKIES = 6;
130     private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_PREDICTION_DISABLED =
131             7;
132     private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_DATA_REDUCTION_ENABLED = 8;
133     private static final int SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_METERED = 9;
134     private static final int SPECULATION_STATUS_ON_START_MAX = 10;
135 
136     // For CustomTabs.SpeculationStatusOnSwap, see tools/metrics/enums.xml. Append only.
137     private static final int SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_TAKEN = 0;
138     private static final int SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_NOT_MATCHED = 1;
139     private static final int SPECULATION_STATUS_ON_SWAP_PRERENDER_TAKEN = 2;
140     private static final int SPECULATION_STATUS_ON_SWAP_PRERENDER_NOT_MATCHED = 3;
141     private static final int SPECULATION_STATUS_ON_SWAP_MAX = 4;
142 
143     // Constants for sending connection characteristics.
144     public static final String DATA_REDUCTION_ENABLED = "dataReductionEnabled";
145 
146     // "/bg_non_interactive" is from L MR1, "/apps/bg_non_interactive" before,
147     // and "background" from O.
148     @VisibleForTesting
149     static final Set<String> BACKGROUND_GROUPS = new HashSet<>(
150             Arrays.asList("/bg_non_interactive", "/apps/bg_non_interactive", "/background"));
151 
152     // TODO(lizeb): Move to the support library.
153     @VisibleForTesting
154     static final String REDIRECT_ENDPOINT_KEY = "androidx.browser.REDIRECT_ENDPOINT";
155     @VisibleForTesting
156     static final String PARALLEL_REQUEST_REFERRER_KEY =
157             "android.support.customtabs.PARALLEL_REQUEST_REFERRER";
158     static final String PARALLEL_REQUEST_REFERRER_POLICY_KEY =
159             "android.support.customtabs.PARALLEL_REQUEST_REFERRER_POLICY";
160     @VisibleForTesting
161     static final String PARALLEL_REQUEST_URL_KEY =
162             "android.support.customtabs.PARALLEL_REQUEST_URL";
163     static final String RESOURCE_PREFETCH_URL_LIST_KEY =
164             "androidx.browser.RESOURCE_PREFETCH_URL_LIST";
165 
166     @IntDef({ParallelRequestStatus.NO_REQUEST, ParallelRequestStatus.SUCCESS,
167             ParallelRequestStatus.FAILURE_NOT_INITIALIZED,
168             ParallelRequestStatus.FAILURE_NOT_AUTHORIZED, ParallelRequestStatus.FAILURE_INVALID_URL,
169             ParallelRequestStatus.FAILURE_INVALID_REFERRER,
170             ParallelRequestStatus.FAILURE_INVALID_REFERRER_FOR_SESSION})
171     @Retention(RetentionPolicy.SOURCE)
172     @interface ParallelRequestStatus {
173         // Values should start from 0 and can't have gaps (they're used for indexing
174         // PARALLEL_REQUEST_MESSAGES).
175         @VisibleForTesting
176         int NO_REQUEST = 0;
177         @VisibleForTesting
178         int SUCCESS = 1;
179         @VisibleForTesting
180         int FAILURE_NOT_INITIALIZED = 2;
181         @VisibleForTesting
182         int FAILURE_NOT_AUTHORIZED = 3;
183         @VisibleForTesting
184         int FAILURE_INVALID_URL = 4;
185         @VisibleForTesting
186         int FAILURE_INVALID_REFERRER = 5;
187         @VisibleForTesting
188         int FAILURE_INVALID_REFERRER_FOR_SESSION = 6;
189         int NUM_ENTRIES = 7;
190     }
191 
192     private static final String[] PARALLEL_REQUEST_MESSAGES = {"No request", "Success",
193             "Chrome not initialized", "Not authorized", "Invalid URL", "Invalid referrer",
194             "Invalid referrer for session"};
195 
196     private static CustomTabsConnection sInstance;
197     private @Nullable String mTrustedPublisherUrlPackage;
198 
199     private final HiddenTabHolder mHiddenTabHolder = new HiddenTabHolder();
200     protected final SessionDataHolder mSessionDataHolder;
201     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
202     final ClientManager mClientManager;
203     protected final boolean mLogRequests;
204     private final AtomicBoolean mWarmupHasBeenCalled = new AtomicBoolean();
205     private final AtomicBoolean mWarmupHasBeenFinished = new AtomicBoolean();
206 
207     @Nullable
208     private Callback<CustomTabsSessionToken> mDisconnectCallback;
209 
210     // Conversion between native TimeTicks and SystemClock.uptimeMillis().
211     private long mNativeTickOffsetUs;
212     private boolean mNativeTickOffsetUsComputed;
213 
214     private volatile ChainedTasks mWarmupTasks;
215 
216     /**
217      * <strong>DO NOT CALL</strong>
218      * Public to be instanciable from {@link ChromeApplication}. This is however
219      * intended to be private.
220      */
CustomTabsConnection()221     public CustomTabsConnection() {
222         super();
223         mClientManager = new ClientManager();
224         mLogRequests = CommandLine.getInstance().hasSwitch(LOG_SERVICE_REQUESTS);
225         mSessionDataHolder = ChromeApplication.getComponent().resolveSessionDataHolder();
226     }
227 
228     /**
229      * @return The unique instance of ChromeCustomTabsConnection.
230      */
getInstance()231     public static CustomTabsConnection getInstance() {
232         if (sInstance == null) {
233             sInstance = AppHooks.get().createCustomTabsConnection();
234         }
235 
236         return sInstance;
237     }
238 
hasInstance()239     private static boolean hasInstance() {
240         return sInstance != null;
241     }
242 
243     /**
244      * If service requests logging is enabled, logs that a call was made.
245      *
246      * No rate-limiting, can be spammy if the app is misbehaved.
247      *
248      * @param name Call name to log.
249      * @param result The return value for the logged call.
250      */
logCall(String name, Object result)251     void logCall(String name, Object result) {
252         if (!mLogRequests) return;
253         Log.w(TAG, "%s = %b, Calling UID = %d", name, result, Binder.getCallingUid());
254     }
255 
256     /**
257      * If service requests logging is enabled, logs a callback.
258      *
259      * No rate-limiting, can be spammy if the app is misbehaved.
260      *
261      * @param name Callback name to log.
262      * @param args arguments of the callback.
263      */
logCallback(String name, Object args)264     void logCallback(String name, Object args) {
265         if (!mLogRequests) return;
266         Log.w(TAG, "%s args = %s", name, args);
267     }
268 
269     /**
270      * Converts a Bundle to JSON.
271      *
272      * The conversion is limited to Bundles not containing any array, and some elements are
273      * converted into strings.
274      *
275      * @param bundle a Bundle to convert.
276      * @return A JSON object, empty object if the parameter is null.
277      */
bundleToJson(Bundle bundle)278     protected static JSONObject bundleToJson(Bundle bundle) {
279         JSONObject json = new JSONObject();
280         if (bundle == null) return json;
281         for (String key : bundle.keySet()) {
282             Object o = bundle.get(key);
283             try {
284                 if (o instanceof Bundle) {
285                     json.put(key, bundleToJson((Bundle) o));
286                 } else if (o instanceof Integer || o instanceof Long || o instanceof Boolean) {
287                     json.put(key, o);
288                 } else if (o == null) {
289                     json.put(key, JSONObject.NULL);
290                 } else {
291                     json.put(key, o.toString());
292                 }
293             } catch (JSONException e) {
294                 // Ok, only used for logging.
295             }
296         }
297         return json;
298     }
299 
300     /*
301      * Logging for page load metrics callback, if service has enabled logging.
302      *
303      * No rate-limiting, can be spammy if the app is misbehaved.
304      *
305      * @param args arguments of the callback.
306      */
logPageLoadMetricsCallback(Bundle args)307     void logPageLoadMetricsCallback(Bundle args) {
308         if (!mLogRequests) return; // Don't build args if not necessary.
309         logCallback(
310                 "extraCallback(" + PAGE_LOAD_METRICS_CALLBACK + ")", bundleToJson(args).toString());
311     }
312 
313     /** Sets a callback to be triggered when a service connection is terminated. */
setDisconnectCallback(@ullable Callback<CustomTabsSessionToken> callback)314     public void setDisconnectCallback(@Nullable Callback<CustomTabsSessionToken> callback) {
315         mDisconnectCallback = callback;
316     }
317 
newSession(CustomTabsSessionToken session)318     public boolean newSession(CustomTabsSessionToken session) {
319         boolean success = newSessionInternal(session);
320         logCall("newSession()", success);
321         return success;
322     }
323 
newSessionInternal(CustomTabsSessionToken session)324     private boolean newSessionInternal(CustomTabsSessionToken session) {
325         if (session == null) return false;
326         ClientManager.DisconnectCallback onDisconnect = new ClientManager.DisconnectCallback() {
327             @Override
328             public void run(CustomTabsSessionToken session) {
329                 cancelSpeculation(session);
330                 if (mDisconnectCallback != null) {
331                     mDisconnectCallback.onResult(session);
332                 }
333 
334                 // TODO(pshmakov): invert this dependency by moving event dispatching to a separate
335                 // class.
336                 ChromeApplication.getComponent()
337                         .resolveCustomTabsFileProcessor()
338                         .onSessionDisconnected(session);
339             }
340         };
341 
342         // TODO(peconn): Make this not an anonymous class once PostMessageServiceConnection is made
343         // non-abstract in AndroidX.
344         PostMessageServiceConnection serviceConnection =
345                 new PostMessageServiceConnection(session) {};
346         PostMessageHandler handler = new PostMessageHandler(serviceConnection);
347         return mClientManager.newSession(
348                 session, Binder.getCallingUid(), onDisconnect, handler, serviceConnection);
349     }
350 
351     /**
352      * Overrides the given session's packageName if it is generated by Chrome. To be used for
353      * testing only. To be called before the session given is associated with a tab.
354      * @param session The session for which the package name should be overridden.
355      * @param packageName The new package name to set.
356      */
overridePackageNameForSessionForTesting( CustomTabsSessionToken session, String packageName)357     public void overridePackageNameForSessionForTesting(
358             CustomTabsSessionToken session, String packageName) {
359         String originalPackage = getClientPackageNameForSession(session);
360         String selfPackage = ContextUtils.getApplicationContext().getPackageName();
361         if (TextUtils.isEmpty(originalPackage) || !selfPackage.equals(originalPackage)) return;
362         mClientManager.overridePackageNameForSession(session, packageName);
363     }
364 
365     /** Warmup activities that should only happen once. */
initializeBrowser(final Context context)366     private static void initializeBrowser(final Context context) {
367         ThreadUtils.assertOnUiThread();
368         ChromeBrowserInitializer.getInstance().handleSynchronousStartupWithGpuWarmUp();
369         ChildProcessLauncherHelper.warmUp(context, true);
370     }
371 
warmup(long flags)372     public boolean warmup(long flags) {
373         try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.warmup")) {
374             boolean success = warmupInternal(true);
375             logCall("warmup()", success);
376             return success;
377         }
378     }
379 
380     /**
381      * @return Whether {@link CustomTabsConnection#warmup(long)} has been called.
382      */
hasWarmUpBeenFinished()383     public boolean hasWarmUpBeenFinished() {
384         return mWarmupHasBeenFinished.get();
385     }
386 
387     /**
388      * Starts as much as possible in anticipation of a future navigation.
389      *
390      * @param mayCreateSpareWebContents true if warmup() can create a spare renderer.
391      * @return true for success.
392      */
warmupInternal(final boolean mayCreateSpareWebContents)393     private boolean warmupInternal(final boolean mayCreateSpareWebContents) {
394         // Here and in mayLaunchUrl(), don't do expensive work for background applications.
395         if (!isCallerForegroundOrSelf()) return false;
396         int uid = Binder.getCallingUid();
397         mClientManager.recordUidHasCalledWarmup(uid);
398         final boolean initialized = !mWarmupHasBeenCalled.compareAndSet(false, true);
399 
400         // The call is non-blocking and this must execute on the UI thread, post chained tasks.
401         ChainedTasks tasks = new ChainedTasks();
402 
403         // Ordering of actions here:
404         // 1. Initializing the browser needs to be done once, and first.
405         // 2. Creating a spare renderer takes time, in other threads and processes, so start it
406         //    sooner rather than later. Can be done several times.
407         // 3. UI inflation has to be done for any new activity.
408         // 4. Initializing the LoadingPredictor is done once, and triggers work on other threads,
409         //    start it early.
410         // 5. RequestThrottler first access has to be done only once.
411 
412         // (1)
413         if (!initialized) {
414             tasks.add(UiThreadTaskTraits.BOOTSTRAP, () -> {
415                 try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.initializeBrowser()")) {
416                     initializeBrowser(ContextUtils.getApplicationContext());
417                     ChromeBrowserInitializer.getInstance().initNetworkChangeNotifier();
418                     mWarmupHasBeenFinished.set(true);
419                 }
420             });
421         }
422 
423         // (2)
424         if (mayCreateSpareWebContents && !mHiddenTabHolder.hasHiddenTab()) {
425             tasks.add(UiThreadTaskTraits.BOOTSTRAP, () -> {
426                 // Temporary fix for https://crbug.com/797832.
427                 // TODO(lizeb): Properly fix instead of papering over the bug, this code should
428                 // not be scheduled unless startup is done. See https://crbug.com/797832.
429                 if (!BrowserStartupController.getInstance().isFullBrowserStarted()) return;
430                 try (TraceEvent e = TraceEvent.scoped("CreateSpareWebContents")) {
431                     createSpareWebContents();
432                 }
433             });
434         }
435 
436         // (3)
437         tasks.add(UiThreadTaskTraits.BOOTSTRAP, () -> {
438             try (TraceEvent e = TraceEvent.scoped("InitializeViewHierarchy")) {
439                 WarmupManager.getInstance().initializeViewHierarchy(
440                         ContextUtils.getApplicationContext(),
441                         R.layout.custom_tabs_control_container, R.layout.custom_tabs_toolbar);
442             }
443         });
444 
445         if (!initialized) {
446             tasks.add(UiThreadTaskTraits.BOOTSTRAP, () -> {
447                 try (TraceEvent e = TraceEvent.scoped("WarmupInternalFinishInitialization")) {
448                     // (4)
449                     Profile profile = Profile.getLastUsedRegularProfile();
450                     WarmupManager.startPreconnectPredictorInitialization(profile);
451 
452                     // (5)
453                     // The throttling database uses shared preferences, that can cause a
454                     // StrictMode violation on the first access. Make sure that this access is
455                     // not in mayLauchUrl.
456                     RequestThrottler.loadInBackground();
457                 }
458             });
459         }
460 
461         tasks.add(UiThreadTaskTraits.BOOTSTRAP, () -> notifyWarmupIsDone(uid));
462         tasks.start(false);
463         mWarmupTasks = tasks;
464         return true;
465     }
466 
467     /** @return the URL or null if it's invalid. */
isValid(Uri uri)468     private static boolean isValid(Uri uri) {
469         if (uri == null) return false;
470         // Don't do anything for unknown schemes. Not having a scheme is allowed, as we allow
471         // "www.example.com".
472         String scheme = uri.normalizeScheme().getScheme();
473         boolean allowedScheme = scheme == null || scheme.equals(UrlConstants.HTTP_SCHEME)
474                 || scheme.equals(UrlConstants.HTTPS_SCHEME);
475         return allowedScheme;
476     }
477 
478     /**
479      * High confidence mayLaunchUrl() call, that is:
480      * - Tries to speculate if possible.
481      * - An empty URL cancels the current prerender if any.
482      * - Start a spare renderer if necessary.
483      */
highConfidenceMayLaunchUrl(CustomTabsSessionToken session, int uid, String url, Bundle extras, List<Bundle> otherLikelyBundles)484     private void highConfidenceMayLaunchUrl(CustomTabsSessionToken session, int uid, String url,
485             Bundle extras, List<Bundle> otherLikelyBundles) {
486         ThreadUtils.assertOnUiThread();
487         if (TextUtils.isEmpty(url)) {
488             cancelSpeculation(session);
489             return;
490         }
491 
492         if (maySpeculate(session)) {
493             boolean canUseHiddenTab = mClientManager.getCanUseHiddenTab(session);
494             startSpeculation(session, url, canUseHiddenTab, extras, uid);
495         }
496         preconnectUrls(otherLikelyBundles);
497     }
498 
499     /**
500      * Low confidence mayLaunchUrl() call, that is:
501      * - Preconnects to the ordered list of URLs.
502      * - Makes sure that there is a spare renderer.
503      */
504     @VisibleForTesting
lowConfidenceMayLaunchUrl(List<Bundle> likelyBundles)505     boolean lowConfidenceMayLaunchUrl(List<Bundle> likelyBundles) {
506         ThreadUtils.assertOnUiThread();
507         if (!preconnectUrls(likelyBundles)) return false;
508         createSpareWebContents();
509         return true;
510     }
511 
preconnectUrls(List<Bundle> likelyBundles)512     private boolean preconnectUrls(List<Bundle> likelyBundles) {
513         boolean atLeastOneUrl = false;
514         if (likelyBundles == null) return false;
515         WarmupManager warmupManager = WarmupManager.getInstance();
516         Profile profile = Profile.getLastUsedRegularProfile();
517         for (Bundle bundle : likelyBundles) {
518             Uri uri;
519             try {
520                 uri = IntentUtils.safeGetParcelable(bundle, CustomTabsService.KEY_URL);
521             } catch (ClassCastException e) {
522                 continue;
523             }
524             if (isValid(uri)) {
525                 warmupManager.maybePreconnectUrlAndSubResources(profile, uri.toString());
526                 atLeastOneUrl = true;
527             }
528         }
529         return atLeastOneUrl;
530     }
531 
mayLaunchUrl(CustomTabsSessionToken session, Uri url, Bundle extras, List<Bundle> otherLikelyBundles)532     public boolean mayLaunchUrl(CustomTabsSessionToken session, Uri url, Bundle extras,
533             List<Bundle> otherLikelyBundles) {
534         try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.mayLaunchUrl")) {
535             boolean success = mayLaunchUrlInternal(session, url, extras, otherLikelyBundles);
536             logCall("mayLaunchUrl(" + url + ")", success);
537             return success;
538         }
539     }
540 
mayLaunchUrlInternal(final CustomTabsSessionToken session, final Uri url, final Bundle extras, final List<Bundle> otherLikelyBundles)541     private boolean mayLaunchUrlInternal(final CustomTabsSessionToken session, final Uri url,
542             final Bundle extras, final List<Bundle> otherLikelyBundles) {
543         final boolean lowConfidence =
544                 (url == null || TextUtils.isEmpty(url.toString())) && otherLikelyBundles != null;
545         final String urlString = isValid(url) ? url.toString() : null;
546         if (url != null && urlString == null && !lowConfidence) return false;
547 
548         final int uid = Binder.getCallingUid();
549 
550         PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
551             reportNextLikelyNavigationsOnUiThread(uid, urlString, otherLikelyBundles);
552         });
553 
554         // Things below need the browser process to be initialized.
555 
556         // Forbids warmup() from creating a spare renderer, as prerendering wouldn't reuse
557         // it. Checking whether prerendering is enabled requires the native library to be loaded,
558         // which is not necessarily the case yet.
559         if (!warmupInternal(false)) return false; // Also does the foreground check.
560 
561         if (!mClientManager.updateStatsAndReturnWhetherAllowed(
562                     session, uid, urlString, otherLikelyBundles != null)) {
563             return false;
564         }
565 
566         PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
567             doMayLaunchUrlOnUiThread(
568                     lowConfidence, session, uid, urlString, extras, otherLikelyBundles, true);
569         });
570         return true;
571     }
572 
573     /**
574      * Reports the set of URLs of next likely navigations in the UI thread.
575      *
576      * @param uid: UID of the external Android app that reported the set of URLs.
577      * @param url: URL of the next likely navigation as reported by the external Android app.
578      * @param otherLikelyBundles: Bundle reported by the external Android app.
579      */
reportNextLikelyNavigationsOnUiThread( final int uid, final String url, final List<Bundle> otherLikelyBundles)580     private static void reportNextLikelyNavigationsOnUiThread(
581             final int uid, final String url, final List<Bundle> otherLikelyBundles) {
582         ThreadUtils.assertOnUiThread();
583 
584         ChromeBrowserInitializer.getInstance().runNowOrAfterFullBrowserStarted(() -> {
585             PostTask.postTask(TaskTraits.BEST_EFFORT,
586                     () -> { reportNextLikelyNavigationsToNative(uid, url, otherLikelyBundles); });
587         }
588 
589         );
590     }
591 
592     /**
593      * Reports the set of URLs of next likely navigations to native.
594      *
595      * @param uid: UID of the external Android app that reported the set of URLs.
596      * @param url: URL of the next likely navigation as reported by the external Android app.
597      * @param otherLikelyBundles: Bundle reported by the external Android app.
598      */
reportNextLikelyNavigationsToNative( final int uid, final String url, final List<Bundle> otherLikelyBundles)599     private static void reportNextLikelyNavigationsToNative(
600             final int uid, final String url, final List<Bundle> otherLikelyBundles) {
601         ThreadUtils.assertOnBackgroundThread();
602 
603         Context context = ContextUtils.getApplicationContext();
604         PackageManager pm = context.getApplicationContext().getPackageManager();
605         String[] packages = pm.getPackagesForUid(uid);
606 
607         if (packages == null || packages.length == 0) return;
608 
609         PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
610             List<String> urlsList = new ArrayList<String>();
611             if (url != null) urlsList.add(url);
612 
613             if (otherLikelyBundles != null) {
614                 for (Bundle bundle : otherLikelyBundles) {
615                     Uri uri = IntentUtils.safeGetParcelable(bundle, CustomTabsService.KEY_URL);
616                     if (isValid(uri)) urlsList.add(uri.toString());
617                 }
618             }
619 
620             String[] urlsArray = urlsList.toArray(new String[0]);
621             WarmupManager.reportNextLikelyNavigationsOnUiThread(
622                     Profile.getLastUsedRegularProfile(), packages, urlsArray);
623         });
624     }
625 
doMayLaunchUrlOnUiThread(final boolean lowConfidence, final CustomTabsSessionToken session, final int uid, final String urlString, final Bundle extras, final List<Bundle> otherLikelyBundles, boolean retryIfNotLoaded)626     private void doMayLaunchUrlOnUiThread(final boolean lowConfidence,
627             final CustomTabsSessionToken session, final int uid, final String urlString,
628             final Bundle extras, final List<Bundle> otherLikelyBundles, boolean retryIfNotLoaded) {
629         ThreadUtils.assertOnUiThread();
630         try (TraceEvent e = TraceEvent.scoped("CustomTabsConnection.mayLaunchUrlOnUiThread")) {
631             // doMayLaunchUrlInternal() is always called once the native level initialization is
632             // done, at least the initial profile load. However, at that stage the startup callback
633             // may not have run, which causes Profile.getLastUsedRegularProfile() to throw an
634             // exception. But the tasks have been posted by then, so reschedule ourselves, only
635             // once.
636             if (!BrowserStartupController.getInstance().isFullBrowserStarted()) {
637                 if (retryIfNotLoaded) {
638                     PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
639                         doMayLaunchUrlOnUiThread(lowConfidence, session, uid, urlString, extras,
640                                 otherLikelyBundles, false);
641                     });
642                 }
643                 return;
644             }
645 
646             if (lowConfidence) {
647                 lowConfidenceMayLaunchUrl(otherLikelyBundles);
648             } else {
649                 highConfidenceMayLaunchUrl(session, uid, urlString, extras, otherLikelyBundles);
650             }
651         }
652     }
653 
extraCommand(String commandName, Bundle args)654     public Bundle extraCommand(String commandName, Bundle args) {
655         return null;
656     }
657 
updateVisuals(final CustomTabsSessionToken session, Bundle bundle)658     public boolean updateVisuals(final CustomTabsSessionToken session, Bundle bundle) {
659         if (mLogRequests) Log.w(TAG, "updateVisuals: %s", bundleToJson(bundle));
660         SessionHandler handler = mSessionDataHolder.getActiveHandler(session);
661         if (handler == null) return false;
662 
663         final Bundle actionButtonBundle =
664                 IntentUtils.safeGetBundle(bundle, CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE);
665         boolean result = true;
666         List<Integer> ids = new ArrayList<>();
667         List<String> descriptions = new ArrayList<>();
668         List<Bitmap> icons = new ArrayList<>();
669         if (actionButtonBundle != null) {
670             int id = IntentUtils.safeGetInt(actionButtonBundle, CustomTabsIntent.KEY_ID,
671                     CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID);
672             Bitmap bitmap = CustomButtonParams.parseBitmapFromBundle(actionButtonBundle);
673             String description = CustomButtonParams.parseDescriptionFromBundle(actionButtonBundle);
674             if (bitmap != null && description != null) {
675                 ids.add(id);
676                 descriptions.add(description);
677                 icons.add(bitmap);
678             }
679         }
680 
681         List<Bundle> bundleList = IntentUtils.safeGetParcelableArrayList(
682                 bundle, CustomTabsIntent.EXTRA_TOOLBAR_ITEMS);
683         if (bundleList != null) {
684             for (Bundle toolbarItemBundle : bundleList) {
685                 int id = IntentUtils.safeGetInt(toolbarItemBundle, CustomTabsIntent.KEY_ID,
686                         CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID);
687                 if (ids.contains(id)) continue;
688 
689                 Bitmap bitmap = CustomButtonParams.parseBitmapFromBundle(toolbarItemBundle);
690                 if (bitmap == null) continue;
691 
692                 String description =
693                         CustomButtonParams.parseDescriptionFromBundle(toolbarItemBundle);
694                 if (description == null) continue;
695 
696                 ids.add(id);
697                 descriptions.add(description);
698                 icons.add(bitmap);
699             }
700         }
701 
702         if (!ids.isEmpty()) {
703             result &= PostTask.runSynchronously(UiThreadTaskTraits.DEFAULT, () -> {
704                 boolean res = true;
705                 for (int i = 0; i < ids.size(); i++) {
706                     res &= handler.updateCustomButton(
707                             ids.get(i), icons.get(i), descriptions.get(i));
708                 }
709                 return res;
710             });
711         }
712 
713         if (bundle.containsKey(CustomTabsIntent.EXTRA_REMOTEVIEWS)) {
714             final RemoteViews remoteViews =
715                     IntentUtils.safeGetParcelable(bundle, CustomTabsIntent.EXTRA_REMOTEVIEWS);
716             final int[] clickableIDs = IntentUtils.safeGetIntArray(
717                     bundle, CustomTabsIntent.EXTRA_REMOTEVIEWS_VIEW_IDS);
718             final PendingIntent pendingIntent = IntentUtils.safeGetParcelable(
719                     bundle, CustomTabsIntent.EXTRA_REMOTEVIEWS_PENDINGINTENT);
720             result &= PostTask.runSynchronously(UiThreadTaskTraits.DEFAULT,
721                     () -> handler.updateRemoteViews(remoteViews, clickableIDs, pendingIntent));
722         }
723         logCall("updateVisuals()", result);
724         return result;
725     }
726 
requestPostMessageChannel( CustomTabsSessionToken session, Origin postMessageOrigin)727     public boolean requestPostMessageChannel(
728             CustomTabsSessionToken session, Origin postMessageOrigin) {
729         boolean success = requestPostMessageChannelInternal(session, postMessageOrigin);
730         logCall("requestPostMessageChannel() with origin "
731                         + (postMessageOrigin != null ? postMessageOrigin.toString() : ""),
732                 success);
733         return success;
734     }
735 
requestPostMessageChannelInternal( final CustomTabsSessionToken session, final Origin postMessageOrigin)736     private boolean requestPostMessageChannelInternal(
737             final CustomTabsSessionToken session, final Origin postMessageOrigin) {
738         if (!mWarmupHasBeenCalled.get()) return false;
739         if (!isCallerForegroundOrSelf() && !mSessionDataHolder.isActiveSession(session)) {
740             return false;
741         }
742         if (!mClientManager.bindToPostMessageServiceForSession(session)) return false;
743 
744         final int uid = Binder.getCallingUid();
745         PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
746             // If the API is not enabled, we don't set the post message origin, which will avoid
747             // PostMessageHandler initialization and disallow postMessage calls.
748             if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_POST_MESSAGE_API)) return;
749 
750             // Attempt to verify origin synchronously. If successful directly initialize postMessage
751             // channel for session.
752             Uri verifiedOrigin = verifyOriginForSession(session, uid, postMessageOrigin);
753             if (verifiedOrigin == null) {
754                 mClientManager.verifyAndInitializeWithPostMessageOriginForSession(
755                         session, postMessageOrigin, CustomTabsService.RELATION_USE_AS_ORIGIN);
756             } else {
757                 mClientManager.initializeWithPostMessageOriginForSession(session, verifiedOrigin);
758             }
759         });
760         return true;
761     }
762 
763     /**
764      * Acquire the origin for the client that owns the given session.
765      * @param session The session to use for getting client information.
766      * @param clientUid The UID for the client controlling the session.
767      * @param origin The origin that is suggested by the client. The validated origin may be this or
768      *               a derivative of this.
769      * @return The validated origin {@link Uri} for the given session's client.
770      */
verifyOriginForSession( CustomTabsSessionToken session, int clientUid, Origin origin)771     protected Uri verifyOriginForSession(
772             CustomTabsSessionToken session, int clientUid, Origin origin) {
773         if (clientUid == Process.myUid()) return Uri.EMPTY;
774         return null;
775     }
776 
777     /**
778      * Returns whether an intent is first-party with respect to its session, that is if the
779      * application linked to the session has a relation with the provided origin.
780      *
781      * @param intent The intent to verify.
782      */
isFirstPartyOriginForIntent(Intent intent)783     public boolean isFirstPartyOriginForIntent(Intent intent) {
784         CustomTabsSessionToken session = CustomTabsSessionToken.getSessionTokenFromIntent(intent);
785         if (session == null) return false;
786 
787         Origin origin = Origin.create(intent.getData());
788         if (origin == null) return false;
789 
790         return mClientManager.isFirstPartyOriginForSession(session, origin);
791     }
792 
postMessage(CustomTabsSessionToken session, String message, Bundle extras)793     public int postMessage(CustomTabsSessionToken session, String message, Bundle extras) {
794         int result;
795         if (!mWarmupHasBeenCalled.get()) result = CustomTabsService.RESULT_FAILURE_DISALLOWED;
796         if (!isCallerForegroundOrSelf() && !mSessionDataHolder.isActiveSession(session)) {
797             result = CustomTabsService.RESULT_FAILURE_DISALLOWED;
798         }
799         // If called before a validatePostMessageOrigin, the post message origin will be invalid and
800         // will return a failure result here.
801         result = mClientManager.postMessage(session, message);
802         logCall("postMessage", result);
803         return result;
804     }
805 
validateRelationship( CustomTabsSessionToken sessionToken, int relation, Origin origin, Bundle extras)806     public boolean validateRelationship(
807             CustomTabsSessionToken sessionToken, int relation, Origin origin, Bundle extras) {
808         // Essential parts of the verification will depend on native code and will be run sync on UI
809         // thread. Make sure the client has called warmup() beforehand.
810         if (!mWarmupHasBeenCalled.get()) {
811             Log.d(TAG, "Verification failed due to warmup not having been previously called.");
812             mClientManager.getCallbackForSession(sessionToken)
813                     .onRelationshipValidationResult(
814                             relation, Uri.parse(origin.toString()), false, null);
815             return false;
816         }
817         return mClientManager.validateRelationship(sessionToken, relation, origin, extras);
818     }
819 
820     /**
821      * See
822      * {@link ClientManager#resetPostMessageHandlerForSession(CustomTabsSessionToken, WebContents)}.
823      */
resetPostMessageHandlerForSession( CustomTabsSessionToken session, WebContents webContents)824     public void resetPostMessageHandlerForSession(
825             CustomTabsSessionToken session, WebContents webContents) {
826         mClientManager.resetPostMessageHandlerForSession(session, webContents);
827     }
828 
829     /**
830      * Registers a launch of a |url| for a given |session|.
831      *
832      * This is used for accounting.
833      */
registerLaunch(CustomTabsSessionToken session, String url)834     void registerLaunch(CustomTabsSessionToken session, String url) {
835         mClientManager.registerLaunch(session, url);
836     }
837 
838     @Nullable
getSpeculatedUrl(CustomTabsSessionToken session)839     public String getSpeculatedUrl(CustomTabsSessionToken session) {
840         return mHiddenTabHolder.getSpeculatedUrl(session);
841     }
842 
843     /**
844      * Returns the preloaded {@link Tab} if it matches the given |url| and |referrer|. Null if no
845      * such {@link Tab}. If a {@link Tab} is preloaded but it does not match, it is discarded.
846      *
847      * @param session The Binder object identifying a session.
848      * @param url The URL the tab is for.
849      * @param referrer The referrer to use for |url|.
850      * @return The hidden tab, or null.
851      */
852     @Nullable
takeHiddenTab( @ullable CustomTabsSessionToken session, String url, @Nullable String referrer)853     public Tab takeHiddenTab(
854             @Nullable CustomTabsSessionToken session, String url, @Nullable String referrer) {
855         return mHiddenTabHolder.takeHiddenTab(
856                 session, mClientManager.getIgnoreFragmentsForSession(session), url, referrer);
857     }
858 
859     /**
860      * Called when an intent is handled by either an existing or a new CustomTabActivity.
861      *
862      * @param session Session extracted from the intent.
863      * @param intent incoming intent.
864      */
onHandledIntent(CustomTabsSessionToken session, Intent intent)865     public void onHandledIntent(CustomTabsSessionToken session, Intent intent) {
866         String url = IntentHandler.getUrlFromIntent(intent);
867         if (TextUtils.isEmpty(url)) {
868             return;
869         }
870         if (mLogRequests) {
871             Log.w(TAG, "onHandledIntent, URL: %s, extras: %s", url,
872                     bundleToJson(intent.getExtras()));
873         }
874 
875         // If we still have pending warmup tasks, don't continue as they would only delay intent
876         // processing from now on.
877         if (mWarmupTasks != null) mWarmupTasks.cancel();
878 
879         maybePreconnectToRedirectEndpoint(session, url, intent);
880         ChromeBrowserInitializer.getInstance().runNowOrAfterFullBrowserStarted(
881                 () -> handleParallelRequest(session, intent));
882         maybePrefetchResources(session, intent);
883     }
884 
885     /**
886      * Called each time a CCT tab is created to check if a client data header was set and if so
887      * forward it along to the native side.
888      * @param session Session identifier.
889      * @param webContents the WebContents of the new tab.
890      */
setClientDataHeaderForNewTab( CustomTabsSessionToken session, WebContents webContents)891     public void setClientDataHeaderForNewTab(
892             CustomTabsSessionToken session, WebContents webContents) {}
893 
setClientDataHeader(WebContents webContents, String header)894     protected void setClientDataHeader(WebContents webContents, String header) {
895         if (TextUtils.isEmpty(header)) return;
896 
897         CustomTabsConnectionJni.get().setClientDataHeader(webContents, header);
898     }
899 
maybePreconnectToRedirectEndpoint( CustomTabsSessionToken session, String url, Intent intent)900     private void maybePreconnectToRedirectEndpoint(
901             CustomTabsSessionToken session, String url, Intent intent) {
902         // For the preconnection to not be a no-op, we need more than just the native library.
903         if (!ChromeBrowserInitializer.getInstance().isFullBrowserInitialized()) {
904             return;
905         }
906         if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_REDIRECT_PRECONNECT)) return;
907 
908         // Conditions:
909         // - There is a valid redirect endpoint.
910         // - The URL's origin is first party with respect to the app.
911         Uri redirectEndpoint = intent.getParcelableExtra(REDIRECT_ENDPOINT_KEY);
912         if (redirectEndpoint == null || !isValid(redirectEndpoint)) return;
913 
914         Origin origin = Origin.create(url);
915         if (origin == null) return;
916         if (!mClientManager.isFirstPartyOriginForSession(session, origin)) return;
917 
918         WarmupManager.getInstance().maybePreconnectUrlAndSubResources(
919                 Profile.getLastUsedRegularProfile(), redirectEndpoint.toString());
920     }
921 
922     @VisibleForTesting
923     @ParallelRequestStatus
handleParallelRequest(CustomTabsSessionToken session, Intent intent)924     int handleParallelRequest(CustomTabsSessionToken session, Intent intent) {
925         int status = maybeStartParallelRequest(session, intent);
926         RecordHistogram.recordEnumeratedHistogram("CustomTabs.ParallelRequestStatusOnStart", status,
927                 ParallelRequestStatus.NUM_ENTRIES);
928 
929         if (mLogRequests) {
930             Log.w(TAG, "handleParallelRequest() = " + PARALLEL_REQUEST_MESSAGES[status]);
931         }
932 
933         if ((status != ParallelRequestStatus.NO_REQUEST)
934                 && (status != ParallelRequestStatus.FAILURE_NOT_INITIALIZED)
935                 && (status != ParallelRequestStatus.FAILURE_NOT_AUTHORIZED)
936                 && ChromeFeatureList.isEnabled(
937                         ChromeFeatureList.CCT_REPORT_PARALLEL_REQUEST_STATUS)) {
938             Bundle args = new Bundle();
939             Uri url = intent.getParcelableExtra(PARALLEL_REQUEST_URL_KEY);
940             args.putParcelable("url", url);
941             args.putInt("status", status);
942             safeExtraCallback(session, ON_DETACHED_REQUEST_REQUESTED, args);
943             if (mLogRequests) {
944                 logCallback(ON_DETACHED_REQUEST_REQUESTED, bundleToJson(args).toString());
945             }
946         }
947 
948         return status;
949     }
950 
951     /**
952      * Maybe starts a parallel request.
953      *
954      * @param session Calling context session.
955      * @param intent Incoming intent with the extras.
956      * @return Whether the request was started, with reason in case of failure.
957      */
958     @ParallelRequestStatus
maybeStartParallelRequest(CustomTabsSessionToken session, Intent intent)959     private int maybeStartParallelRequest(CustomTabsSessionToken session, Intent intent) {
960         ThreadUtils.assertOnUiThread();
961 
962         if (!intent.hasExtra(PARALLEL_REQUEST_URL_KEY)) return ParallelRequestStatus.NO_REQUEST;
963         if (!ChromeBrowserInitializer.getInstance().isFullBrowserInitialized()) {
964             return ParallelRequestStatus.FAILURE_NOT_INITIALIZED;
965         }
966         if (!mClientManager.getAllowParallelRequestForSession(session)) {
967             return ParallelRequestStatus.FAILURE_NOT_AUTHORIZED;
968         }
969         Uri referrer = intent.getParcelableExtra(PARALLEL_REQUEST_REFERRER_KEY);
970         Uri url = intent.getParcelableExtra(PARALLEL_REQUEST_URL_KEY);
971         int policy =
972                 intent.getIntExtra(PARALLEL_REQUEST_REFERRER_POLICY_KEY, ReferrerPolicy.DEFAULT);
973         if (url == null) return ParallelRequestStatus.FAILURE_INVALID_URL;
974         if (referrer == null) return ParallelRequestStatus.FAILURE_INVALID_REFERRER;
975         if (policy < ReferrerPolicy.MIN_VALUE || policy > ReferrerPolicy.MAX_VALUE) {
976             policy = ReferrerPolicy.DEFAULT;
977         }
978 
979         if (url.toString().equals("") || !isValid(url)) {
980             return ParallelRequestStatus.FAILURE_INVALID_URL;
981         }
982         if (!canDoParallelRequest(session, referrer)) {
983             return ParallelRequestStatus.FAILURE_INVALID_REFERRER_FOR_SESSION;
984         }
985 
986         String urlString = url.toString();
987         String referrerString = referrer.toString();
988         String packageName = mClientManager.getClientPackageNameForSession(session);
989         CustomTabsConnectionJni.get().createAndStartDetachedResourceRequest(
990                 Profile.getLastUsedRegularProfile(), session, packageName, urlString,
991                 referrerString, policy, DetachedResourceRequestMotivation.PARALLEL_REQUEST);
992         if (mLogRequests) {
993             Log.w(TAG, "startParallelRequest(%s, %s, %d)", urlString, referrerString, policy);
994         }
995 
996         return ParallelRequestStatus.SUCCESS;
997     }
998 
999     /**
1000      * Maybe starts a resource prefetch.
1001      *
1002      * @param session Calling context session.
1003      * @param intent Incoming intent with the extras.
1004      * @return Number of prefetch requests that have been sent.
1005      */
1006     @VisibleForTesting
maybePrefetchResources(CustomTabsSessionToken session, Intent intent)1007     int maybePrefetchResources(CustomTabsSessionToken session, Intent intent) {
1008         ThreadUtils.assertOnUiThread();
1009 
1010         if (!mClientManager.getAllowResourcePrefetchForSession(session)) return 0;
1011 
1012         List<Uri> resourceList = intent.getParcelableArrayListExtra(RESOURCE_PREFETCH_URL_LIST_KEY);
1013         Uri referrer = intent.getParcelableExtra(PARALLEL_REQUEST_REFERRER_KEY);
1014         int policy =
1015                 intent.getIntExtra(PARALLEL_REQUEST_REFERRER_POLICY_KEY, ReferrerPolicy.DEFAULT);
1016 
1017         if (resourceList == null || referrer == null) return 0;
1018         if (policy < 0 || policy > ReferrerPolicy.MAX_VALUE) policy = ReferrerPolicy.DEFAULT;
1019         Origin origin = Origin.create(referrer);
1020         if (origin == null) return 0;
1021         if (!mClientManager.isFirstPartyOriginForSession(session, origin)) return 0;
1022 
1023         String referrerString = referrer.toString();
1024         int requestsSent = 0;
1025         for (Uri url : resourceList) {
1026             String urlString = url.toString();
1027             if (urlString.isEmpty() || !isValid(url)) continue;
1028 
1029             // Session is null because we don't need completion notifications.
1030             CustomTabsConnectionJni.get().createAndStartDetachedResourceRequest(
1031                     Profile.getLastUsedRegularProfile(), null, null, urlString, referrerString,
1032                     policy, DetachedResourceRequestMotivation.RESOURCE_PREFETCH);
1033             ++requestsSent;
1034 
1035             if (mLogRequests) {
1036                 Log.w(TAG, "startResourcePrefetch(%s, %s, %d)", urlString, referrerString, policy);
1037             }
1038         }
1039 
1040         return requestsSent;
1041     }
1042 
1043     /**
1044      * @return Whether {@code session} can create a parallel request for a given
1045      * {@code referrer}.
1046      */
1047     @VisibleForTesting
canDoParallelRequest(CustomTabsSessionToken session, Uri referrer)1048     boolean canDoParallelRequest(CustomTabsSessionToken session, Uri referrer) {
1049         ThreadUtils.assertOnUiThread();
1050         Origin origin = Origin.create(referrer);
1051         if (origin == null) return false;
1052         return mClientManager.isFirstPartyOriginForSession(session, origin);
1053     }
1054 
1055     /** @see ClientManager#shouldHideDomainForSession(CustomTabsSessionToken) */
shouldHideDomainForSession(CustomTabsSessionToken session)1056     public boolean shouldHideDomainForSession(CustomTabsSessionToken session) {
1057         return mClientManager.shouldHideDomainForSession(session);
1058     }
1059 
1060     /** @see ClientManager#shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken) */
shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session)1061     public boolean shouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session) {
1062         return mClientManager.shouldSpeculateLoadOnCellularForSession(session);
1063     }
1064 
1065     /** @see ClientManager#shouldSendNavigationInfoForSession(CustomTabsSessionToken) */
shouldSendNavigationInfoForSession(CustomTabsSessionToken session)1066     public boolean shouldSendNavigationInfoForSession(CustomTabsSessionToken session) {
1067         return mClientManager.shouldSendNavigationInfoForSession(session);
1068     }
1069 
1070     /** @see ClientManager#shouldSendBottomBarScrollStateForSession(CustomTabsSessionToken) */
shouldSendBottomBarScrollStateForSession(CustomTabsSessionToken session)1071     public boolean shouldSendBottomBarScrollStateForSession(CustomTabsSessionToken session) {
1072         return mClientManager.shouldSendBottomBarScrollStateForSession(session);
1073     }
1074 
1075     /** See {@link ClientManager#getClientPackageNameForSession(CustomTabsSessionToken)} */
getClientPackageNameForSession(CustomTabsSessionToken session)1076     public String getClientPackageNameForSession(CustomTabsSessionToken session) {
1077         return mClientManager.getClientPackageNameForSession(session);
1078     }
1079 
1080     /** @return Whether the client of the {@code session} is a first-party application. */
isSessionFirstParty(CustomTabsSessionToken session)1081     public boolean isSessionFirstParty(CustomTabsSessionToken session) {
1082         String packageName = getClientPackageNameForSession(session);
1083         if (packageName == null) return false;
1084         return AppHooks.get().getExternalAuthUtils().isGoogleSigned(packageName);
1085     }
1086 
setIgnoreUrlFragmentsForSession(CustomTabsSessionToken session, boolean value)1087     void setIgnoreUrlFragmentsForSession(CustomTabsSessionToken session, boolean value) {
1088         mClientManager.setIgnoreFragmentsForSession(session, value);
1089     }
1090 
1091     @VisibleForTesting
getIgnoreUrlFragmentsForSession(CustomTabsSessionToken session)1092     boolean getIgnoreUrlFragmentsForSession(CustomTabsSessionToken session) {
1093         return mClientManager.getIgnoreFragmentsForSession(session);
1094     }
1095 
1096     @VisibleForTesting
setShouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session, boolean value)1097     void setShouldSpeculateLoadOnCellularForSession(CustomTabsSessionToken session, boolean value) {
1098         mClientManager.setSpeculateLoadOnCellularForSession(session, value);
1099     }
1100 
1101     @VisibleForTesting
setCanUseHiddenTabForSession(CustomTabsSessionToken session, boolean value)1102     void setCanUseHiddenTabForSession(CustomTabsSessionToken session, boolean value) {
1103         mClientManager.setCanUseHiddenTab(session, value);
1104     }
1105 
1106     /**
1107      * See {@link ClientManager#setSendNavigationInfoForSession(CustomTabsSessionToken, boolean)}.
1108      */
setSendNavigationInfoForSession(CustomTabsSessionToken session, boolean send)1109     void setSendNavigationInfoForSession(CustomTabsSessionToken session, boolean send) {
1110         mClientManager.setSendNavigationInfoForSession(session, send);
1111     }
1112 
1113     /**
1114      * Shows a toast about any possible sign in issues encountered during custom tab startup.
1115      * @param session The session that corresponding custom tab is assigned.
1116      * @param intent The intent that launched the custom tab.
1117      */
showSignInToastIfNecessary(CustomTabsSessionToken session, Intent intent)1118     void showSignInToastIfNecessary(CustomTabsSessionToken session, Intent intent) {}
1119 
1120     /**
1121      * Sends a callback using {@link CustomTabsCallback} with the first run result if necessary.
1122      * @param intentExtras The extras for the initial VIEW intent that initiated first run.
1123      * @param resultOK Whether first run was successful.
1124      */
sendFirstRunCallbackIfNecessary(Bundle intentExtras, boolean resultOK)1125     public void sendFirstRunCallbackIfNecessary(Bundle intentExtras, boolean resultOK) {}
1126 
1127     /**
1128      * Sends the navigation info that was captured to the client callback.
1129      * @param session The session to use for getting client callback.
1130      * @param url The current url for the tab.
1131      * @param title The current title for the tab.
1132      * @param snapshotPath Uri location for screenshot of the tab contents which is publicly
1133      *         available for sharing.
1134      */
sendNavigationInfo( CustomTabsSessionToken session, String url, String title, Uri snapshotPath)1135     public void sendNavigationInfo(
1136             CustomTabsSessionToken session, String url, String title, Uri snapshotPath) {}
1137 
1138     // TODO(yfriedman): Remove when internal code is deleted.
sendNavigationInfo( CustomTabsSessionToken session, String url, String title, Bitmap snapshotPath)1139     public void sendNavigationInfo(
1140             CustomTabsSessionToken session, String url, String title, Bitmap snapshotPath) {}
1141 
1142     /**
1143      * Called when the bottom bar for the custom tab has been hidden or shown completely by user
1144      * scroll.
1145      *
1146      * @param session The session that is linked with the custom tab.
1147      * @param hidden Whether the bottom bar is hidden or shown.
1148      */
onBottomBarScrollStateChanged(CustomTabsSessionToken session, boolean hidden)1149     public void onBottomBarScrollStateChanged(CustomTabsSessionToken session, boolean hidden) {
1150         Bundle args = new Bundle();
1151         args.putBoolean("hidden", hidden);
1152 
1153         if (safeExtraCallback(session, BOTTOM_BAR_SCROLL_STATE_CALLBACK, args) && mLogRequests) {
1154             logCallback("extraCallback(" + BOTTOM_BAR_SCROLL_STATE_CALLBACK + ")", hidden);
1155         }
1156     }
1157 
1158     /**
1159      * Notifies the application of a navigation event.
1160      *
1161      * Delivers the {@link CustomTabsCallback#onNavigationEvent} callback to the application.
1162      *
1163      * @param session The Binder object identifying the session.
1164      * @param navigationEvent The navigation event code, defined in {@link CustomTabsCallback}
1165      * @return true for success.
1166      */
notifyNavigationEvent(CustomTabsSessionToken session, int navigationEvent)1167     public boolean notifyNavigationEvent(CustomTabsSessionToken session, int navigationEvent) {
1168         CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
1169         if (callback == null) return false;
1170         try {
1171             callback.onNavigationEvent(
1172                     navigationEvent, getExtrasBundleForNavigationEventForSession(session));
1173         } catch (Exception e) {
1174             // Catching all exceptions is really bad, but we need it here,
1175             // because Android exposes us to client bugs by throwing a variety
1176             // of exceptions. See crbug.com/517023.
1177             return false;
1178         }
1179         logCallback("onNavigationEvent()", navigationEvent);
1180         return true;
1181     }
1182 
1183     /**
1184      * @return The {@link Bundle} to use as extra to
1185      *         {@link CustomTabsCallback#onNavigationEvent(int, Bundle)}
1186      */
getExtrasBundleForNavigationEventForSession(CustomTabsSessionToken session)1187     protected Bundle getExtrasBundleForNavigationEventForSession(CustomTabsSessionToken session) {
1188         // SystemClock.uptimeMillis() is used here as it (as of June 2017) uses the same system call
1189         // as all the native side of Chrome, and this is the same clock used for page load metrics.
1190         Bundle extras = new Bundle();
1191         extras.putLong("timestampUptimeMillis", SystemClock.uptimeMillis());
1192         return extras;
1193     }
1194 
notifyWarmupIsDone(int uid)1195     private void notifyWarmupIsDone(int uid) {
1196         ThreadUtils.assertOnUiThread();
1197         // Notifies all the sessions, as warmup() is tied to a UID, not a session.
1198         for (CustomTabsSessionToken session : mClientManager.uidToSessions(uid)) {
1199             safeExtraCallback(session, ON_WARMUP_COMPLETED, null);
1200         }
1201     }
1202 
1203     /**
1204      * Creates a Bundle with a value for navigation start and the specified page load metric.
1205      *
1206      * @param metricName Name of the page load metric.
1207      * @param navigationStartTick Absolute navigation start time, as TimeTicks taken from native.
1208      * @param offsetMs Offset in ms from navigationStart for the page load metric.
1209      *
1210      * @return A Bundle containing navigation start and the page load metric.
1211      */
createBundleWithNavigationStartAndPageLoadMetric( String metricName, long navigationStartTick, long offsetMs)1212     Bundle createBundleWithNavigationStartAndPageLoadMetric(
1213             String metricName, long navigationStartTick, long offsetMs) {
1214         if (!mNativeTickOffsetUsComputed) {
1215             // Compute offset from time ticks to uptimeMillis.
1216             mNativeTickOffsetUsComputed = true;
1217             long nativeNowUs = TimeUtilsJni.get().getTimeTicksNowUs();
1218             long javaNowUs = SystemClock.uptimeMillis() * 1000;
1219             mNativeTickOffsetUs = nativeNowUs - javaNowUs;
1220         }
1221         Bundle args = new Bundle();
1222         args.putLong(metricName, offsetMs);
1223         // SystemClock.uptimeMillis() is used here as it (as of June 2017) uses the same system call
1224         // as all the native side of Chrome, that is clock_gettime(CLOCK_MONOTONIC). Meaning that
1225         // the offset relative to navigationStart is to be compared with a
1226         // SystemClock.uptimeMillis() value.
1227         args.putLong(PageLoadMetrics.NAVIGATION_START,
1228                 (navigationStartTick - mNativeTickOffsetUs) / 1000);
1229         return args;
1230     }
1231 
1232     /**
1233      * Notifies the application of a page load metric for a single metric.
1234      *
1235      * @param session Session identifier.
1236      * @param metricName Name of the page load metric.
1237      * @param navigationStartTick Absolute navigation start time, as TimeTicks taken from native.
1238      * @param offsetMs Offset in ms from navigationStart for the page load metric.
1239      *
1240      * @return Whether the metric has been dispatched to the client.
1241      */
notifySinglePageLoadMetric(CustomTabsSessionToken session, String metricName, long navigationStartTick, long offsetMs)1242     boolean notifySinglePageLoadMetric(CustomTabsSessionToken session, String metricName,
1243             long navigationStartTick, long offsetMs) {
1244         return notifyPageLoadMetrics(session,
1245                 createBundleWithNavigationStartAndPageLoadMetric(
1246                         metricName, navigationStartTick, offsetMs));
1247     }
1248 
1249     /**
1250      * Notifies the application of a general page load metrics.
1251      *
1252      * TODD(lizeb): Move this to a proper method in {@link CustomTabsCallback} once one is
1253      * available.
1254      *
1255      * @param session Session identifier.
1256      * @param args Bundle containing metric information to update. Each item in the bundle
1257      *     should be a key specifying the metric name and the metric value as the value.
1258      */
notifyPageLoadMetrics(CustomTabsSessionToken session, Bundle args)1259     boolean notifyPageLoadMetrics(CustomTabsSessionToken session, Bundle args) {
1260         if (!mClientManager.shouldGetPageLoadMetrics(session)) return false;
1261         if (safeExtraCallback(session, PAGE_LOAD_METRICS_CALLBACK, args)) {
1262             logPageLoadMetricsCallback(args);
1263             return true;
1264         }
1265         return false;
1266     }
1267 
1268     /**
1269      * Notifies the application that the user has selected to open the page in their browser.
1270      * @param session Session identifier.
1271      * @param webContents the WebContents of the tab being taken out of CCT.
1272      * @return true if success. To protect Chrome exceptions in the client application are swallowed
1273      *     and false is returned.
1274      */
notifyOpenInBrowser(CustomTabsSessionToken session, WebContents webContents)1275     boolean notifyOpenInBrowser(CustomTabsSessionToken session, WebContents webContents) {
1276         // Reset the client data header for the WebContents since it's not a CCT tab anymore.
1277         if (webContents != null) CustomTabsConnectionJni.get().setClientDataHeader(webContents, "");
1278         return safeExtraCallback(session, OPEN_IN_BROWSER_CALLBACK,
1279                 getExtrasBundleForNavigationEventForSession(session));
1280     }
1281 
1282     /**
1283      * Wraps calling extraCallback in a try/catch so exceptions thrown by the host app don't crash
1284      * Chrome. See https://crbug.com/517023.
1285      */
safeExtraCallback( CustomTabsSessionToken session, String callbackName, @Nullable Bundle args)1286     protected boolean safeExtraCallback(
1287             CustomTabsSessionToken session, String callbackName, @Nullable Bundle args) {
1288         CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
1289         if (callback == null) return false;
1290 
1291         try {
1292             callback.extraCallback(callbackName, args);
1293         } catch (Exception e) {
1294             return false;
1295         }
1296         return true;
1297     }
1298 
1299     /**
1300      * Calls {@link CustomTabsCallback#extraCallbackWithResult)}.
1301      * Wraps calling sendExtraCallbackWithResult in a try/catch so that exceptions thrown by the
1302      * host app don't crash Chrome.
1303      */
1304     @Nullable
sendExtraCallbackWithResult( CustomTabsSessionToken session, String callbackName, @Nullable Bundle args)1305     public Bundle sendExtraCallbackWithResult(
1306             CustomTabsSessionToken session, String callbackName, @Nullable Bundle args) {
1307         CustomTabsCallback callback = mClientManager.getCallbackForSession(session);
1308         if (callback == null) return null;
1309 
1310         try {
1311             return callback.extraCallbackWithResult(callbackName, args);
1312         } catch (Exception e) {
1313             return null;
1314         }
1315     }
1316 
1317     /**
1318      * Keeps the application linked with a given session alive.
1319      *
1320      * The application is kept alive (that is, raised to at least the current process priority
1321      * level) until {@link #dontKeepAliveForSession} is called.
1322      *
1323      * @param session The Binder object identifying the session.
1324      * @param intent Intent describing the service to bind to.
1325      * @return true for success.
1326      */
keepAliveForSession(CustomTabsSessionToken session, Intent intent)1327     boolean keepAliveForSession(CustomTabsSessionToken session, Intent intent) {
1328         return mClientManager.keepAliveForSession(session, intent);
1329     }
1330 
1331     /**
1332      * Lets the lifetime of the process linked to a given sessionId be managed normally.
1333      *
1334      * Without a matching call to {@link #keepAliveForSession}, this is a no-op.
1335      *
1336      * @param session The Binder object identifying the session.
1337      */
dontKeepAliveForSession(CustomTabsSessionToken session)1338     void dontKeepAliveForSession(CustomTabsSessionToken session) {
1339         mClientManager.dontKeepAliveForSession(session);
1340     }
1341 
1342     /**
1343      * Returns whether /proc/PID/ is accessible.
1344      *
1345      * On devices where /proc is mounted with the "hidepid=2" option, cannot get access to the
1346      * scheduler group, as this is under this directory, which is hidden unless PID == self (or
1347      * its numeric value).
1348      */
1349     @VisibleForTesting
canGetSchedulerGroup(int pid)1350     static boolean canGetSchedulerGroup(int pid) {
1351         String cgroupFilename = "/proc/" + pid;
1352         File f = new File(cgroupFilename);
1353         return f.exists() && f.isDirectory() && f.canExecute();
1354     }
1355 
1356     /**
1357      * @return the CPU cgroup of a given process, identified by its PID, or null.
1358      */
1359     @VisibleForTesting
getSchedulerGroup(int pid)1360     static String getSchedulerGroup(int pid) {
1361         // Android uses several cgroups for processes, depending on their priority. The list of
1362         // cgroups a process is part of can be queried by reading /proc/<pid>/cgroup, which is
1363         // world-readable.
1364         String cgroupFilename = "/proc/" + pid + "/cgroup";
1365         String controllerName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "cpuset" : "cpu";
1366         // Reading from /proc does not cause disk IO, but strict mode doesn't like it.
1367         // crbug.com/567143
1368         try (StrictModeContext ignored = StrictModeContext.allowDiskReads();
1369                 BufferedReader reader = new BufferedReader(new FileReader(cgroupFilename))) {
1370             String line = null;
1371             while ((line = reader.readLine()) != null) {
1372                 // line format: 2:cpu:/bg_non_interactive
1373                 String[] fields = line.trim().split(":");
1374                 if (fields.length == 3 && fields[1].equals(controllerName)) return fields[2];
1375             }
1376         } catch (IOException e) {
1377             return null;
1378         }
1379         return null;
1380     }
1381 
isBackgroundProcess(int pid)1382     private static boolean isBackgroundProcess(int pid) {
1383         return BACKGROUND_GROUPS.contains(getSchedulerGroup(pid));
1384     }
1385 
1386     /**
1387      * @return true when inside a Binder transaction and the caller is in the
1388      * foreground or self. Don't use outside a Binder transaction.
1389      */
isCallerForegroundOrSelf()1390     private boolean isCallerForegroundOrSelf() {
1391         int uid = Binder.getCallingUid();
1392         if (uid == Process.myUid()) return true;
1393 
1394         // Starting with L MR1, AM.getRunningAppProcesses doesn't return all the
1395         // processes. We use a workaround in this case.
1396         boolean useWorkaround = true;
1397         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
1398             do {
1399                 Context context = ContextUtils.getApplicationContext();
1400                 ActivityManager am =
1401                         (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
1402                 // Extra paranoia here and below, some L 5.0.x devices seem to throw NPE somewhere
1403                 // in this code.
1404                 // See https://crbug.com/654705.
1405                 if (am == null) break;
1406                 List<ActivityManager.RunningAppProcessInfo> running = am.getRunningAppProcesses();
1407                 if (running == null) break;
1408                 for (ActivityManager.RunningAppProcessInfo rpi : running) {
1409                     if (rpi == null) continue;
1410                     boolean matchingUid = rpi.uid == uid;
1411                     boolean isForeground = rpi.importance
1412                             == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
1413                     useWorkaround &= !matchingUid;
1414                     if (matchingUid && isForeground) return true;
1415                 }
1416             } while (false);
1417         }
1418         if (useWorkaround) {
1419             int pid = Binder.getCallingPid();
1420             boolean workaroundAvailable = canGetSchedulerGroup(pid);
1421             // If we have no way to find out whether the calling process is in the foreground,
1422             // optimistically assume it is. Otherwise we would effectively disable CCT warmup
1423             // on these devices.
1424             if (!workaroundAvailable) return true;
1425             return isBackgroundProcess(pid);
1426         }
1427         return false;
1428     }
1429 
cleanupAllForTesting()1430     void cleanupAllForTesting() {
1431         ThreadUtils.assertOnUiThread();
1432         mClientManager.cleanupAll();
1433         mHiddenTabHolder.destroyHiddenTab(null);
1434     }
1435 
1436     /**
1437      * Handle any clean up left after a session is destroyed.
1438      * @param session The session that has been destroyed.
1439      */
1440     @VisibleForTesting
cleanUpSession(final CustomTabsSessionToken session)1441     void cleanUpSession(final CustomTabsSessionToken session) {
1442         PostTask.runOrPostTask(
1443                 UiThreadTaskTraits.DEFAULT, () -> mClientManager.cleanupSession(session));
1444     }
1445 
1446     /**
1447      * Discards substantial objects that are not currently in use.
1448      * @param level The type of signal as defined in {@link android.content.ComponentCallbacks2}.
1449      */
onTrimMemory(int level)1450     public static void onTrimMemory(int level) {
1451         if (!hasInstance()) return;
1452 
1453         if (ChromeApplication.isSevereMemorySignal(level)) {
1454             getInstance().mClientManager.cleanupUnusedSessions();
1455         }
1456     }
1457 
1458     @VisibleForTesting
maySpeculateWithResult(CustomTabsSessionToken session)1459     int maySpeculateWithResult(CustomTabsSessionToken session) {
1460         if (!DeviceClassManager.enablePrerendering()) {
1461             return SPECULATION_STATUS_ON_START_NOT_ALLOWED_DEVICE_CLASS;
1462         }
1463         if (UserPrefs.get(Profile.getLastUsedRegularProfile()).getInteger(COOKIE_CONTROLS_MODE)
1464                 == CookieControlsMode.BLOCK_THIRD_PARTY) {
1465             return SPECULATION_STATUS_ON_START_NOT_ALLOWED_BLOCK_3RD_PARTY_COOKIES;
1466         }
1467         // TODO(yusufo): The check for prerender in PrivacyPreferencesManager now checks for the
1468         // network connection type as well, we should either change that or add another check for
1469         // custom tabs. Then that method should be used to make the below check.
1470         if (!PrivacyPreferencesManager.getInstance().getNetworkPredictionEnabled()) {
1471             return SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_PREDICTION_DISABLED;
1472         }
1473         if (DataReductionProxySettings.getInstance().isDataReductionProxyEnabled()
1474                 && !ChromeFeatureList.isEnabled(
1475                         ChromeFeatureList.PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES)) {
1476             return SPECULATION_STATUS_ON_START_NOT_ALLOWED_DATA_REDUCTION_ENABLED;
1477         }
1478         ConnectivityManager cm =
1479                 (ConnectivityManager) ContextUtils.getApplicationContext().getSystemService(
1480                         Context.CONNECTIVITY_SERVICE);
1481         if (cm.isActiveNetworkMetered() && !shouldSpeculateLoadOnCellularForSession(session)
1482                 && !ChromeFeatureList.isEnabled(
1483                         ChromeFeatureList.PREDICTIVE_PREFETCHING_ALLOWED_ON_ALL_CONNECTION_TYPES)) {
1484             return SPECULATION_STATUS_ON_START_NOT_ALLOWED_NETWORK_METERED;
1485         }
1486         return SPECULATION_STATUS_ON_START_ALLOWED;
1487     }
1488 
maySpeculate(CustomTabsSessionToken session)1489     boolean maySpeculate(CustomTabsSessionToken session) {
1490         int speculationResult = maySpeculateWithResult(session);
1491         recordSpeculationStatusOnStart(speculationResult);
1492         return speculationResult == SPECULATION_STATUS_ON_START_ALLOWED;
1493     }
1494 
1495     /** Cancels the speculation for a given session, or any session if null. */
cancelSpeculation(@ullable CustomTabsSessionToken session)1496     public void cancelSpeculation(@Nullable CustomTabsSessionToken session) {
1497         ThreadUtils.assertOnUiThread();
1498         mHiddenTabHolder.destroyHiddenTab(session);
1499     }
1500 
1501     /*
1502      * This function will do as much as it can to have a subsequent navigation
1503      * to the specified url sped up, including speculatively loading a url, preconnecting,
1504      * and starting a spare renderer.
1505      */
startSpeculation(CustomTabsSessionToken session, String url, boolean useHiddenTab, Bundle extras, int uid)1506     private void startSpeculation(CustomTabsSessionToken session, String url, boolean useHiddenTab,
1507             Bundle extras, int uid) {
1508         WarmupManager warmupManager = WarmupManager.getInstance();
1509         Profile profile = Profile.getLastUsedRegularProfile();
1510 
1511         // At most one on-going speculation, clears the previous one.
1512         cancelSpeculation(null);
1513 
1514         if (useHiddenTab) {
1515             recordSpeculationStatusOnStart(SPECULATION_STATUS_ON_START_BACKGROUND_TAB);
1516             launchUrlInHiddenTab(session, url, extras);
1517         } else {
1518             createSpareWebContents();
1519         }
1520         warmupManager.maybePreconnectUrlAndSubResources(profile, url);
1521     }
1522 
1523     /**
1524      * Creates a hidden tab and initiates a navigation.
1525      */
launchUrlInHiddenTab( CustomTabsSessionToken session, String url, @Nullable Bundle extras)1526     private void launchUrlInHiddenTab(
1527             CustomTabsSessionToken session, String url, @Nullable Bundle extras) {
1528         ThreadUtils.assertOnUiThread();
1529         mHiddenTabHolder.launchUrlInHiddenTab(session, mClientManager, url, extras);
1530     }
1531 
1532     @VisibleForTesting
resetThrottling(int uid)1533     void resetThrottling(int uid) {
1534         mClientManager.resetThrottling(uid);
1535     }
1536 
1537     @VisibleForTesting
ban(int uid)1538     void ban(int uid) {
1539         mClientManager.ban(uid);
1540     }
1541 
1542     /**
1543      * @return The referrer that is associated with the client owning the given session.
1544      */
getDefaultReferrerForSession(CustomTabsSessionToken session)1545     public Referrer getDefaultReferrerForSession(CustomTabsSessionToken session) {
1546         return mClientManager.getDefaultReferrerForSession(session);
1547     }
1548 
1549     /**
1550      * @return The package name of a client for which the publisher URL from a trusted CDN can be
1551      *         shown, or null to disallow showing the publisher URL.
1552      */
getTrustedCdnPublisherUrlPackage()1553     public @Nullable String getTrustedCdnPublisherUrlPackage() {
1554         return mTrustedPublisherUrlPackage;
1555     }
1556 
setTrustedPublisherUrlPackageForTest(@ullable String packageName)1557     void setTrustedPublisherUrlPackageForTest(@Nullable String packageName) {
1558         mTrustedPublisherUrlPackage = packageName;
1559     }
1560 
recordSpeculationStatusOnStart(int status)1561     private static void recordSpeculationStatusOnStart(int status) {
1562         RecordHistogram.recordEnumeratedHistogram(
1563                 "CustomTabs.SpeculationStatusOnStart", status, SPECULATION_STATUS_ON_START_MAX);
1564     }
1565 
recordSpeculationStatusOnSwap(int status)1566     private static void recordSpeculationStatusOnSwap(int status) {
1567         RecordHistogram.recordEnumeratedHistogram(
1568                 "CustomTabs.SpeculationStatusOnSwap", status, SPECULATION_STATUS_ON_SWAP_MAX);
1569     }
1570 
recordSpeculationStatusSwapTabTaken()1571     static void recordSpeculationStatusSwapTabTaken() {
1572         recordSpeculationStatusOnSwap(SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_TAKEN);
1573     }
1574 
recordSpeculationStatusSwapTabNotMatched()1575     static void recordSpeculationStatusSwapTabNotMatched() {
1576         recordSpeculationStatusOnSwap(SPECULATION_STATUS_ON_SWAP_BACKGROUND_TAB_NOT_MATCHED);
1577     }
1578 
1579     @CalledByNative
notifyClientOfDetachedRequestCompletion( CustomTabsSessionToken session, String url, int status)1580     public static void notifyClientOfDetachedRequestCompletion(
1581             CustomTabsSessionToken session, String url, int status) {
1582         if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_REPORT_PARALLEL_REQUEST_STATUS)) {
1583             return;
1584         }
1585         Bundle args = new Bundle();
1586         args.putParcelable("url", Uri.parse(url));
1587         args.putInt("net_error", status);
1588         CustomTabsConnection connection = getInstance();
1589         connection.safeExtraCallback(session, ON_DETACHED_REQUEST_COMPLETED, args);
1590         if (connection.mLogRequests) {
1591             connection.logCallback(ON_DETACHED_REQUEST_COMPLETED, bundleToJson(args).toString());
1592         }
1593     }
1594 
1595     @VisibleForTesting
1596     @Nullable
getSpeculationParamsForTesting()1597     HiddenTabHolder.SpeculationParams getSpeculationParamsForTesting() {
1598         return mHiddenTabHolder.getSpeculationParamsForTesting();
1599     }
1600 
createSpareWebContents()1601     public static void createSpareWebContents() {
1602         if (SysUtils.isLowEndDevice()) return;
1603         WarmupManager.getInstance().createSpareWebContents(WarmupManager.FOR_CCT);
1604     }
1605 
receiveFile( CustomTabsSessionToken sessionToken, Uri uri, int purpose, Bundle extras)1606     public boolean receiveFile(
1607             CustomTabsSessionToken sessionToken, Uri uri, int purpose, Bundle extras) {
1608         return ChromeApplication.getComponent().resolveCustomTabsFileProcessor().processFile(
1609                 sessionToken, uri, purpose, extras);
1610     }
1611 
1612     @VisibleForTesting
setInstanceForTesting(CustomTabsConnection connection)1613     public static void setInstanceForTesting(CustomTabsConnection connection) {
1614         sInstance = connection;
1615     }
1616 
1617     @NativeMethods
1618     interface Natives {
createAndStartDetachedResourceRequest(Profile profile, CustomTabsSessionToken session, String packageName, String url, String origin, int referrerPolicy, @DetachedResourceRequestMotivation int motivation)1619         void createAndStartDetachedResourceRequest(Profile profile, CustomTabsSessionToken session,
1620                 String packageName, String url, String origin, int referrerPolicy,
1621                 @DetachedResourceRequestMotivation int motivation);
setClientDataHeader(WebContents webContents, String header)1622         void setClientDataHeader(WebContents webContents, String header);
1623     }
1624 }
1625