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