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 android.content.ComponentName; 8 import android.content.Context; 9 import android.content.Intent; 10 import android.content.ServiceConnection; 11 import android.content.pm.PackageManager; 12 import android.net.Uri; 13 import android.os.Bundle; 14 import android.os.IBinder; 15 import android.os.SystemClock; 16 import android.text.TextUtils; 17 import android.text.format.DateUtils; 18 import android.util.SparseBooleanArray; 19 20 import androidx.annotation.IntDef; 21 import androidx.annotation.NonNull; 22 import androidx.annotation.VisibleForTesting; 23 import androidx.browser.customtabs.CustomTabsCallback; 24 import androidx.browser.customtabs.CustomTabsService; 25 import androidx.browser.customtabs.CustomTabsService.Relation; 26 import androidx.browser.customtabs.CustomTabsSessionToken; 27 import androidx.browser.customtabs.PostMessageServiceConnection; 28 29 import org.chromium.base.ContextUtils; 30 import org.chromium.base.metrics.RecordHistogram; 31 import org.chromium.base.task.PostTask; 32 import org.chromium.chrome.browser.IntentHandler; 33 import org.chromium.chrome.browser.browserservices.OriginVerifier; 34 import org.chromium.chrome.browser.browserservices.OriginVerifier.OriginVerificationListener; 35 import org.chromium.chrome.browser.browserservices.PostMessageHandler; 36 import org.chromium.chrome.browser.installedapp.InstalledAppProviderImpl; 37 import org.chromium.chrome.browser.installedapp.PackageManagerDelegate; 38 import org.chromium.components.embedder_support.util.Origin; 39 import org.chromium.components.embedder_support.util.UrlUtilities; 40 import org.chromium.content_public.browser.UiThreadTaskTraits; 41 import org.chromium.content_public.browser.WebContents; 42 import org.chromium.content_public.common.Referrer; 43 import org.chromium.url.URI; 44 45 import java.lang.annotation.Retention; 46 import java.lang.annotation.RetentionPolicy; 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.HashMap; 50 import java.util.HashSet; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Set; 54 55 /** Manages the clients' state for Custom Tabs. This class is threadsafe. */ 56 class ClientManager { 57 // Values for the "CustomTabs.MayLaunchUrlType" UMA histogram. Append-only. 58 @IntDef({MayLaunchUrlType.NO_MAY_LAUNCH_URL, MayLaunchUrlType.LOW_CONFIDENCE, 59 MayLaunchUrlType.HIGH_CONFIDENCE, MayLaunchUrlType.BOTH}) 60 @Retention(RetentionPolicy.SOURCE) 61 @interface MayLaunchUrlType { 62 @VisibleForTesting 63 int NO_MAY_LAUNCH_URL = 0; 64 @VisibleForTesting 65 int LOW_CONFIDENCE = 1; 66 @VisibleForTesting 67 int HIGH_CONFIDENCE = 2; 68 @VisibleForTesting 69 int BOTH = 3; // LOW + HIGH. 70 int NUM_ENTRIES = 4; 71 } 72 73 // Values for the "CustomTabs.PredictionStatus" UMA histogram. Append-only. 74 @IntDef({PredictionStatus.NONE, PredictionStatus.GOOD, PredictionStatus.BAD}) 75 @Retention(RetentionPolicy.SOURCE) 76 @interface PredictionStatus { 77 @VisibleForTesting 78 int NONE = 0; 79 @VisibleForTesting 80 int GOOD = 1; 81 @VisibleForTesting 82 int BAD = 2; 83 int NUM_ENTRIES = 3; 84 } 85 86 // Values for the "CustomTabs.CalledWarmup" UMA histogram. Append-only. 87 @IntDef({CalledWarmup.NO_SESSION_NO_WARMUP, CalledWarmup.NO_SESSION_WARMUP, 88 CalledWarmup.SESSION_NO_WARMUP_ALREADY_CALLED, 89 CalledWarmup.SESSION_NO_WARMUP_NOT_CALLED, CalledWarmup.SESSION_WARMUP}) 90 @Retention(RetentionPolicy.SOURCE) 91 @interface CalledWarmup { 92 @VisibleForTesting 93 int NO_SESSION_NO_WARMUP = 0; 94 @VisibleForTesting 95 int NO_SESSION_WARMUP = 1; 96 @VisibleForTesting 97 int SESSION_NO_WARMUP_ALREADY_CALLED = 2; 98 @VisibleForTesting 99 int SESSION_NO_WARMUP_NOT_CALLED = 3; 100 @VisibleForTesting 101 int SESSION_WARMUP = 4; 102 @VisibleForTesting 103 int NUM_ENTRIES = 5; 104 } 105 106 /** To be called when a client gets disconnected. */ run(CustomTabsSessionToken session)107 public interface DisconnectCallback { public void run(CustomTabsSessionToken session); } 108 109 private static class KeepAliveServiceConnection implements ServiceConnection { 110 private final Context mContext; 111 private final Intent mServiceIntent; 112 private boolean mHasDied; 113 private boolean mIsBound; 114 KeepAliveServiceConnection(Context context, Intent serviceIntent)115 public KeepAliveServiceConnection(Context context, Intent serviceIntent) { 116 mContext = context; 117 mServiceIntent = serviceIntent; 118 } 119 120 /** 121 * Connects to the service identified by |serviceIntent|. Does not reconnect if the service 122 * got disconnected at some point from the other end (remote process death). 123 */ connect()124 public boolean connect() { 125 if (mIsBound) return true; 126 // If the remote process died at some point, it doesn't make sense to resurrect it. 127 if (mHasDied) return false; 128 129 boolean ok; 130 try { 131 ok = mContext.bindService(mServiceIntent, this, Context.BIND_AUTO_CREATE); 132 } catch (SecurityException e) { 133 return false; 134 } 135 mIsBound = ok; 136 return ok; 137 } 138 139 /** 140 * Disconnects from the remote process. Safe to call even if {@link #connect} returned 141 * false, or if the remote service died. 142 */ disconnect()143 public void disconnect() { 144 if (mIsBound) { 145 mContext.unbindService(this); 146 mIsBound = false; 147 } 148 } 149 150 @Override onServiceConnected(ComponentName name, IBinder service)151 public void onServiceConnected(ComponentName name, IBinder service) {} 152 153 @Override onServiceDisconnected(ComponentName name)154 public void onServiceDisconnected(ComponentName name) { 155 if (mIsBound) { 156 // The remote process has died. This typically happens if the system is low enough 157 // on memory to kill one of the last process on the "kill list". In this case, we 158 // shouldn't resurrect the process (which happens with BIND_AUTO_CREATE) because 159 // that could create a "restart/kill" loop. 160 mHasDied = true; 161 disconnect(); 162 } 163 } 164 } 165 166 /** Per-session values. */ 167 private static class SessionParams { 168 public final int uid; 169 private CustomTabsCallback mCustomTabsCallback; 170 public final DisconnectCallback disconnectCallback; 171 public final PostMessageHandler postMessageHandler; 172 public final PostMessageServiceConnection serviceConnection; 173 public final Set<Origin> mLinkedOrigins = new HashSet<>(); 174 public OriginVerifier originVerifier; 175 public boolean mIgnoreFragments; 176 public boolean lowConfidencePrediction; 177 public boolean highConfidencePrediction; 178 private String mPackageName; 179 private boolean mShouldHideDomain; 180 private boolean mShouldSpeculateLoadOnCellular; 181 private boolean mShouldSendNavigationInfo; 182 private boolean mShouldSendBottomBarScrollState; 183 private KeepAliveServiceConnection mKeepAliveConnection; 184 private String mPredictedUrl; 185 private long mLastMayLaunchUrlTimestamp; 186 private boolean mCanUseHiddenTab; 187 private boolean mAllowParallelRequest; 188 private boolean mAllowResourcePrefetch; 189 private boolean mShouldGetPageLoadMetrics; 190 SessionParams(Context context, int uid, CustomTabsCallback customTabsCallback, DisconnectCallback callback, PostMessageHandler postMessageHandler, PostMessageServiceConnection serviceConnection)191 public SessionParams(Context context, int uid, CustomTabsCallback customTabsCallback, 192 DisconnectCallback callback, PostMessageHandler postMessageHandler, 193 PostMessageServiceConnection serviceConnection) { 194 this.uid = uid; 195 mPackageName = getPackageName(context, uid); 196 mCustomTabsCallback = customTabsCallback; 197 disconnectCallback = callback; 198 this.postMessageHandler = postMessageHandler; 199 this.serviceConnection = serviceConnection; 200 if (postMessageHandler != null) this.serviceConnection.setPackageName(mPackageName); 201 } 202 203 /** 204 * Overrides package name with given String. TO be used for testing only. 205 */ overridePackageNameForTesting(String newPackageName)206 void overridePackageNameForTesting(String newPackageName) { 207 mPackageName = newPackageName; 208 } 209 210 /** 211 * @return The package name for this session. 212 */ getPackageName()213 public String getPackageName() { 214 return mPackageName; 215 } 216 getPackageName(Context context, int uid)217 private static String getPackageName(Context context, int uid) { 218 PackageManager packageManager = context.getPackageManager(); 219 String[] packageList = packageManager.getPackagesForUid(uid); 220 if (packageList.length != 1 || TextUtils.isEmpty(packageList[0])) return null; 221 return packageList[0]; 222 } 223 getKeepAliveConnection()224 public KeepAliveServiceConnection getKeepAliveConnection() { 225 return mKeepAliveConnection; 226 } 227 setKeepAliveConnection(KeepAliveServiceConnection serviceConnection)228 public void setKeepAliveConnection(KeepAliveServiceConnection serviceConnection) { 229 mKeepAliveConnection = serviceConnection; 230 } 231 setPredictionMetrics( String predictedUrl, long lastMayLaunchUrlTimestamp, boolean lowConfidence)232 public void setPredictionMetrics( 233 String predictedUrl, long lastMayLaunchUrlTimestamp, boolean lowConfidence) { 234 mPredictedUrl = predictedUrl; 235 mLastMayLaunchUrlTimestamp = lastMayLaunchUrlTimestamp; 236 highConfidencePrediction |= !TextUtils.isEmpty(predictedUrl); 237 lowConfidencePrediction |= lowConfidence; 238 } 239 240 /** 241 * Resets the prediction metrics. This clears the predicted URL, last prediction time, 242 * and whether a low and/or high confidence prediction has been done. 243 */ resetPredictionMetrics()244 public void resetPredictionMetrics() { 245 mPredictedUrl = null; 246 mLastMayLaunchUrlTimestamp = 0; 247 highConfidencePrediction = false; 248 lowConfidencePrediction = false; 249 } 250 getPredictedUrl()251 public String getPredictedUrl() { 252 return mPredictedUrl; 253 } 254 getLastMayLaunchUrlTimestamp()255 public long getLastMayLaunchUrlTimestamp() { 256 return mLastMayLaunchUrlTimestamp; 257 } 258 259 /** 260 * @return Whether the default parameters are used for this session. 261 */ isDefault()262 public boolean isDefault() { 263 return !mIgnoreFragments && !mShouldSpeculateLoadOnCellular; 264 } 265 getCustomTabsCallback()266 public CustomTabsCallback getCustomTabsCallback() { 267 return mCustomTabsCallback; 268 } 269 setCustomTabsCallback(CustomTabsCallback customTabsCallback)270 public void setCustomTabsCallback(CustomTabsCallback customTabsCallback) { 271 mCustomTabsCallback = customTabsCallback; 272 } 273 } 274 275 private final Map<CustomTabsSessionToken, SessionParams> mSessionParams = new HashMap<>(); 276 277 private final SparseBooleanArray mUidHasCalledWarmup = new SparseBooleanArray(); 278 private boolean mWarmupHasBeenCalled; 279 ClientManager()280 public ClientManager() { 281 RequestThrottler.loadInBackground(); 282 } 283 284 /** Creates a new session. 285 * 286 * @param session Session provided by the client. 287 * @param uid Client UID, as returned by Binder.getCallingUid(), 288 * @param onDisconnect To be called on the UI thread when a client gets disconnected. 289 * @param postMessageHandler The handler to be used for postMessage related operations. 290 * @return true for success. 291 */ newSession(CustomTabsSessionToken session, int uid, DisconnectCallback onDisconnect, @NonNull PostMessageHandler postMessageHandler, @NonNull PostMessageServiceConnection serviceConnection)292 public synchronized boolean newSession(CustomTabsSessionToken session, int uid, 293 DisconnectCallback onDisconnect, @NonNull PostMessageHandler postMessageHandler, 294 @NonNull PostMessageServiceConnection serviceConnection) { 295 if (session == null || session.getCallback() == null) return false; 296 if (mSessionParams.containsKey(session)) { 297 mSessionParams.get(session).setCustomTabsCallback(session.getCallback()); 298 } else { 299 SessionParams params = new SessionParams(ContextUtils.getApplicationContext(), uid, 300 session.getCallback(), onDisconnect, postMessageHandler, serviceConnection); 301 mSessionParams.put(session, params); 302 } 303 304 return true; 305 } 306 postMessage(CustomTabsSessionToken session, String message)307 public synchronized int postMessage(CustomTabsSessionToken session, String message) { 308 SessionParams params = mSessionParams.get(session); 309 if (params == null) return CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR; 310 return params.postMessageHandler.postMessageFromClientApp(message); 311 } 312 313 /** 314 * Records that {@link CustomTabsConnection#warmup(long)} has been called from the given uid. 315 */ recordUidHasCalledWarmup(int uid)316 public synchronized void recordUidHasCalledWarmup(int uid) { 317 mWarmupHasBeenCalled = true; 318 mUidHasCalledWarmup.put(uid, true); 319 } 320 321 /** 322 * @return all the sessions originating from a given {@code uid}. 323 */ uidToSessions(int uid)324 public synchronized List<CustomTabsSessionToken> uidToSessions(int uid) { 325 List<CustomTabsSessionToken> sessions = new ArrayList<>(); 326 for (Map.Entry<CustomTabsSessionToken, SessionParams> entry : mSessionParams.entrySet()) { 327 if (entry.getValue().uid == uid) sessions.add(entry.getKey()); 328 } 329 return sessions; 330 } 331 332 /** Updates the client behavior stats and returns whether speculation is allowed. 333 * 334 * The first call to the "low priority" mode is not throttled. Subsequent ones are. 335 * 336 * @param session Client session. 337 * @param uid As returned by Binder.getCallingUid(). 338 * @param url Predicted URL. 339 * @param lowConfidence whether the request contains some "low confidence" URLs. 340 * @return true if speculation is allowed. 341 */ updateStatsAndReturnWhetherAllowed( CustomTabsSessionToken session, int uid, String url, boolean lowConfidence)342 public synchronized boolean updateStatsAndReturnWhetherAllowed( 343 CustomTabsSessionToken session, int uid, String url, boolean lowConfidence) { 344 SessionParams params = mSessionParams.get(session); 345 if (params == null || params.uid != uid) return false; 346 boolean firstLowConfidencePrediction = 347 TextUtils.isEmpty(url) && lowConfidence && !params.lowConfidencePrediction; 348 params.setPredictionMetrics(url, SystemClock.elapsedRealtime(), lowConfidence); 349 if (firstLowConfidencePrediction) return true; 350 RequestThrottler throttler = RequestThrottler.getForUid(uid); 351 return throttler.updateStatsAndReturnWhetherAllowed(); 352 } 353 354 @VisibleForTesting getWarmupState(CustomTabsSessionToken session)355 synchronized @CalledWarmup int getWarmupState(CustomTabsSessionToken session) { 356 SessionParams params = mSessionParams.get(session); 357 boolean hasValidSession = params != null; 358 boolean hasUidCalledWarmup = hasValidSession && mUidHasCalledWarmup.get(params.uid); 359 int result = mWarmupHasBeenCalled ? CalledWarmup.NO_SESSION_WARMUP 360 : CalledWarmup.NO_SESSION_NO_WARMUP; 361 if (hasValidSession) { 362 if (hasUidCalledWarmup) { 363 result = CalledWarmup.SESSION_WARMUP; 364 } else { 365 result = mWarmupHasBeenCalled ? CalledWarmup.SESSION_NO_WARMUP_ALREADY_CALLED 366 : CalledWarmup.SESSION_NO_WARMUP_NOT_CALLED; 367 } 368 } 369 return result; 370 } 371 372 /** 373 * @return the prediction outcome. PredictionStatus.NONE if mSessionParams.get(session) returns 374 * null. 375 */ 376 @VisibleForTesting getPredictionOutcome( CustomTabsSessionToken session, String url)377 synchronized @PredictionStatus int getPredictionOutcome( 378 CustomTabsSessionToken session, String url) { 379 SessionParams params = mSessionParams.get(session); 380 if (params == null) return PredictionStatus.NONE; 381 382 String predictedUrl = params.getPredictedUrl(); 383 if (predictedUrl == null) return PredictionStatus.NONE; 384 385 boolean urlsMatch = TextUtils.equals(predictedUrl, url) 386 || (params.mIgnoreFragments 387 && UrlUtilities.urlsMatchIgnoringFragments(predictedUrl, url)); 388 return urlsMatch ? PredictionStatus.GOOD : PredictionStatus.BAD; 389 } 390 391 /** 392 * Registers that a client has launched a URL inside a Custom Tab. 393 */ registerLaunch(CustomTabsSessionToken session, String url)394 public synchronized void registerLaunch(CustomTabsSessionToken session, String url) { 395 @PredictionStatus 396 int outcome = getPredictionOutcome(session, url); 397 RecordHistogram.recordEnumeratedHistogram( 398 "CustomTabs.PredictionStatus", outcome, PredictionStatus.NUM_ENTRIES); 399 400 SessionParams params = mSessionParams.get(session); 401 if (outcome == PredictionStatus.GOOD) { 402 long elapsedTimeMs = SystemClock.elapsedRealtime() 403 - params.getLastMayLaunchUrlTimestamp(); 404 RequestThrottler.getForUid(params.uid).registerSuccess(params.mPredictedUrl); 405 RecordHistogram.recordCustomTimesHistogram("CustomTabs.PredictionToLaunch", 406 elapsedTimeMs, 1, DateUtils.MINUTE_IN_MILLIS * 3, 100); 407 } 408 RecordHistogram.recordEnumeratedHistogram("CustomTabs.WarmupStateOnLaunch", 409 getWarmupState(session), CalledWarmup.NUM_ENTRIES); 410 411 if (params == null) return; 412 413 @MayLaunchUrlType 414 int value = (params.lowConfidencePrediction ? MayLaunchUrlType.LOW_CONFIDENCE : 0) 415 + (params.highConfidencePrediction ? MayLaunchUrlType.HIGH_CONFIDENCE : 0); 416 RecordHistogram.recordEnumeratedHistogram( 417 "CustomTabs.MayLaunchUrlType", value, MayLaunchUrlType.NUM_ENTRIES); 418 params.resetPredictionMetrics(); 419 } 420 421 /** 422 * See {@link PostMessageServiceConnection#bindSessionToPostMessageService(Context, String)}. 423 */ bindToPostMessageServiceForSession(CustomTabsSessionToken session)424 public synchronized boolean bindToPostMessageServiceForSession(CustomTabsSessionToken session) { 425 SessionParams params = mSessionParams.get(session); 426 if (params == null) return false; 427 return params.serviceConnection.bindSessionToPostMessageService( 428 ContextUtils.getApplicationContext()); 429 } 430 431 /** 432 * See {@link PostMessageHandler#initializeWithPostMessageUri(Uri)}. 433 */ initializeWithPostMessageOriginForSession( CustomTabsSessionToken session, Uri origin)434 public synchronized void initializeWithPostMessageOriginForSession( 435 CustomTabsSessionToken session, Uri origin) { 436 SessionParams params = mSessionParams.get(session); 437 if (params == null) return; 438 params.postMessageHandler.initializeWithPostMessageUri(origin); 439 } 440 validateRelationship( CustomTabsSessionToken session, int relation, Origin origin, Bundle extras)441 public synchronized boolean validateRelationship( 442 CustomTabsSessionToken session, int relation, Origin origin, Bundle extras) { 443 return validateRelationshipInternal(session, relation, origin, false); 444 } 445 446 /** 447 * Validates the link between the client and the origin. 448 */ verifyAndInitializeWithPostMessageOriginForSession( CustomTabsSessionToken session, Origin origin, @Relation int relation)449 public synchronized void verifyAndInitializeWithPostMessageOriginForSession( 450 CustomTabsSessionToken session, Origin origin, @Relation int relation) { 451 validateRelationshipInternal(session, relation, origin, true); 452 } 453 454 /** 455 * Can't be called on UI Thread. 456 */ validateRelationshipInternal(CustomTabsSessionToken session, int relation, Origin origin, boolean initializePostMessageChannel)457 private synchronized boolean validateRelationshipInternal(CustomTabsSessionToken session, 458 int relation, Origin origin, boolean initializePostMessageChannel) { 459 SessionParams params = mSessionParams.get(session); 460 if (params == null || TextUtils.isEmpty(params.getPackageName())) return false; 461 462 OriginVerificationListener listener = (packageName, verifiedOrigin, verified, online) -> { 463 assert origin.equals(verifiedOrigin); 464 465 CustomTabsCallback callback = getCallbackForSession(session); 466 if (callback != null) { 467 Bundle extras = null; 468 if (verified && online != null) { 469 extras = new Bundle(); 470 extras.putBoolean(CustomTabsCallback.ONLINE_EXTRAS_KEY, online); 471 } 472 callback.onRelationshipValidationResult(relation, origin.uri(), verified, extras); 473 } 474 if (initializePostMessageChannel) { 475 params.postMessageHandler 476 .onOriginVerified(packageName, verifiedOrigin, verified, online); 477 } 478 }; 479 480 params.originVerifier = new OriginVerifier(params.getPackageName(), relation, 481 /* webContents= */ null, /* externalAuthUtils= */ null); 482 PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, 483 () -> { params.originVerifier.start(listener, origin); }); 484 if (relation == CustomTabsService.RELATION_HANDLE_ALL_URLS 485 && InstalledAppProviderImpl.isAppInstalledAndAssociatedWithOrigin( 486 params.getPackageName(), URI.create(origin.toString()), 487 new PackageManagerDelegate())) { 488 params.mLinkedOrigins.add(origin); 489 } 490 return true; 491 } 492 493 /** 494 * @return The postMessage origin for the given session. 495 */ 496 @VisibleForTesting getPostMessageOriginForSessionForTesting(CustomTabsSessionToken session)497 synchronized Uri getPostMessageOriginForSessionForTesting(CustomTabsSessionToken session) { 498 SessionParams params = mSessionParams.get(session); 499 if (params == null) return null; 500 return params.postMessageHandler.getPostMessageUriForTesting(); 501 } 502 503 /** 504 * See {@link PostMessageHandler#reset(WebContents)}. 505 */ resetPostMessageHandlerForSession( CustomTabsSessionToken session, WebContents webContents)506 public synchronized void resetPostMessageHandlerForSession( 507 CustomTabsSessionToken session, WebContents webContents) { 508 SessionParams params = mSessionParams.get(session); 509 if (params == null) return; 510 params.postMessageHandler.reset(webContents); 511 } 512 513 /** 514 * @return The referrer that is associated with the client owning given session. 515 */ getDefaultReferrerForSession(CustomTabsSessionToken session)516 public synchronized Referrer getDefaultReferrerForSession(CustomTabsSessionToken session) { 517 return IntentHandler.constructValidReferrerForAuthority( 518 getClientPackageNameForSession(session)); 519 } 520 521 /** 522 * @return The package name associated with the client owning the given session. 523 */ getClientPackageNameForSession(CustomTabsSessionToken session)524 public synchronized String getClientPackageNameForSession(CustomTabsSessionToken session) { 525 SessionParams params = mSessionParams.get(session); 526 return params == null ? null : params.getPackageName(); 527 } 528 529 /** 530 * Overrides the package name for the given session to be the given package name. To be used 531 * for testing only. 532 */ overridePackageNameForSession( CustomTabsSessionToken session, String packageName)533 public synchronized void overridePackageNameForSession( 534 CustomTabsSessionToken session, String packageName) { 535 SessionParams params = mSessionParams.get(session); 536 if (params != null) params.overridePackageNameForTesting(packageName); 537 } 538 539 /** 540 * @return The callback {@link CustomTabsSessionToken} for the given session. 541 */ getCallbackForSession(CustomTabsSessionToken session)542 public synchronized CustomTabsCallback getCallbackForSession(CustomTabsSessionToken session) { 543 if (session != null && mSessionParams.containsKey(session)) { 544 return mSessionParams.get(session).getCustomTabsCallback(); 545 } 546 return null; 547 } 548 549 /** 550 * @return Whether the urlbar should be hidden for the session on first page load. Urls are 551 * foced to show up after the user navigates away. 552 */ shouldHideDomainForSession(CustomTabsSessionToken session)553 public synchronized boolean shouldHideDomainForSession(CustomTabsSessionToken session) { 554 SessionParams params = mSessionParams.get(session); 555 return params != null ? params.mShouldHideDomain : false; 556 } 557 558 /** 559 * Sets whether the urlbar should be hidden for a given session. 560 */ setHideDomainForSession(CustomTabsSessionToken session, boolean hide)561 public synchronized void setHideDomainForSession(CustomTabsSessionToken session, boolean hide) { 562 SessionParams params = mSessionParams.get(session); 563 if (params != null) params.mShouldHideDomain = hide; 564 } 565 566 /** 567 * @return Whether bottom bar scrolling state should be recorded and shared for the session. 568 */ shouldSendBottomBarScrollStateForSession( CustomTabsSessionToken session)569 public synchronized boolean shouldSendBottomBarScrollStateForSession( 570 CustomTabsSessionToken session) { 571 SessionParams params = mSessionParams.get(session); 572 return params != null ? params.mShouldSendBottomBarScrollState : false; 573 } 574 575 /** 576 * Sets whether bottom bar scrolling state should be recorded and shared for the session. 577 */ setSendBottomBarScrollingStateForSessionn( CustomTabsSessionToken session, boolean send)578 public synchronized void setSendBottomBarScrollingStateForSessionn( 579 CustomTabsSessionToken session, boolean send) { 580 SessionParams params = mSessionParams.get(session); 581 if (params != null) params.mShouldSendBottomBarScrollState = send; 582 } 583 584 /** 585 * @return Whether navigation info should be recorded and shared for the session. 586 */ shouldSendNavigationInfoForSession(CustomTabsSessionToken session)587 public synchronized boolean shouldSendNavigationInfoForSession(CustomTabsSessionToken session) { 588 SessionParams params = mSessionParams.get(session); 589 return params != null ? params.mShouldSendNavigationInfo : false; 590 } 591 592 /** 593 * Sets whether navigation info should be recorded and shared for the current navigation in this 594 * session. 595 */ setSendNavigationInfoForSession( CustomTabsSessionToken session, boolean send)596 public synchronized void setSendNavigationInfoForSession( 597 CustomTabsSessionToken session, boolean send) { 598 SessionParams params = mSessionParams.get(session); 599 if (params != null) params.mShouldSendNavigationInfo = send; 600 } 601 602 /** 603 * @return Whether the fragment should be ignored for speculation matching. 604 */ getIgnoreFragmentsForSession(CustomTabsSessionToken session)605 public synchronized boolean getIgnoreFragmentsForSession(CustomTabsSessionToken session) { 606 SessionParams params = mSessionParams.get(session); 607 return params == null ? false : params.mIgnoreFragments; 608 } 609 610 /** Sets whether the fragment should be ignored for speculation matching. */ setIgnoreFragmentsForSession( CustomTabsSessionToken session, boolean value)611 public synchronized void setIgnoreFragmentsForSession( 612 CustomTabsSessionToken session, boolean value) { 613 SessionParams params = mSessionParams.get(session); 614 if (params != null) params.mIgnoreFragments = value; 615 } 616 617 /** 618 * @return Whether load speculation should be turned on for cellular networks for given session. 619 */ shouldSpeculateLoadOnCellularForSession( CustomTabsSessionToken session)620 public synchronized boolean shouldSpeculateLoadOnCellularForSession( 621 CustomTabsSessionToken session) { 622 SessionParams params = mSessionParams.get(session); 623 return params != null ? params.mShouldSpeculateLoadOnCellular : false; 624 } 625 626 /** 627 * @return Whether the session is using the default parameters (that is, don't ignore 628 * fragments and don't speculate loads on cellular connections). 629 */ usesDefaultSessionParameters(CustomTabsSessionToken session)630 public synchronized boolean usesDefaultSessionParameters(CustomTabsSessionToken session) { 631 SessionParams params = mSessionParams.get(session); 632 return params != null ? params.isDefault() : true; 633 } 634 635 /** 636 * Sets whether speculation should be turned on for mobile networks for given session. 637 * If it is turned on, hidden tab speculation is turned on as well. 638 */ setSpeculateLoadOnCellularForSession( CustomTabsSessionToken session, boolean shouldSpeculate)639 public synchronized void setSpeculateLoadOnCellularForSession( 640 CustomTabsSessionToken session, boolean shouldSpeculate) { 641 SessionParams params = mSessionParams.get(session); 642 if (params != null) { 643 params.mShouldSpeculateLoadOnCellular = shouldSpeculate; 644 params.mCanUseHiddenTab = shouldSpeculate; 645 } 646 } 647 648 /** 649 * Sets whether hidden tab speculation can be used. 650 */ setCanUseHiddenTab( CustomTabsSessionToken session, boolean canUseHiddenTab)651 public synchronized void setCanUseHiddenTab( 652 CustomTabsSessionToken session, boolean canUseHiddenTab) { 653 SessionParams params = mSessionParams.get(session); 654 if (params != null) { 655 params.mCanUseHiddenTab = canUseHiddenTab; 656 } 657 } 658 659 /** 660 * Get whether hidden tab speculation can be used. The default is false. 661 */ getCanUseHiddenTab(CustomTabsSessionToken session)662 public synchronized boolean getCanUseHiddenTab(CustomTabsSessionToken session) { 663 SessionParams params = mSessionParams.get(session); 664 return params == null ? false : params.mCanUseHiddenTab; 665 } 666 setAllowParallelRequestForSession( CustomTabsSessionToken session, boolean allowed)667 public synchronized void setAllowParallelRequestForSession( 668 CustomTabsSessionToken session, boolean allowed) { 669 SessionParams params = mSessionParams.get(session); 670 if (params != null) params.mAllowParallelRequest = allowed; 671 } 672 getAllowParallelRequestForSession(CustomTabsSessionToken session)673 public synchronized boolean getAllowParallelRequestForSession(CustomTabsSessionToken session) { 674 SessionParams params = mSessionParams.get(session); 675 return params != null ? params.mAllowParallelRequest : false; 676 } 677 setAllowResourcePrefetchForSession( CustomTabsSessionToken session, boolean allowed)678 public synchronized void setAllowResourcePrefetchForSession( 679 CustomTabsSessionToken session, boolean allowed) { 680 SessionParams params = mSessionParams.get(session); 681 if (params != null) params.mAllowResourcePrefetch = allowed; 682 } 683 getAllowResourcePrefetchForSession(CustomTabsSessionToken session)684 public synchronized boolean getAllowResourcePrefetchForSession(CustomTabsSessionToken session) { 685 SessionParams params = mSessionParams.get(session); 686 return params != null ? params.mAllowResourcePrefetch : false; 687 } 688 setShouldGetPageLoadMetricsForSession( CustomTabsSessionToken session, boolean allowed)689 public synchronized void setShouldGetPageLoadMetricsForSession( 690 CustomTabsSessionToken session, boolean allowed) { 691 SessionParams params = mSessionParams.get(session); 692 if (params != null) params.mShouldGetPageLoadMetrics = allowed; 693 } 694 shouldGetPageLoadMetrics(CustomTabsSessionToken session)695 public synchronized boolean shouldGetPageLoadMetrics(CustomTabsSessionToken session) { 696 SessionParams params = mSessionParams.get(session); 697 return params != null ? params.mShouldGetPageLoadMetrics : false; 698 } 699 700 /** 701 * Returns the uid associated with the session, {@code -1} if there is no matching session. 702 */ getUidForSession(CustomTabsSessionToken session)703 public synchronized int getUidForSession(CustomTabsSessionToken session) { 704 SessionParams params = mSessionParams.get(session); 705 return params != null ? params.uid : -1; 706 } 707 708 /** 709 * Returns whether an origin is first-party with respect to a session, that is if the 710 * application linked to the session has a relation with the provided origin. This does not 711 * calls OriginVerifier, but only checks the cached relations. 712 * 713 * @param session The session. 714 * @param origin Origin to verify 715 */ isFirstPartyOriginForSession( CustomTabsSessionToken session, Origin origin)716 public synchronized boolean isFirstPartyOriginForSession( 717 CustomTabsSessionToken session, Origin origin) { 718 return OriginVerifier.wasPreviouslyVerified(getClientPackageNameForSession(session), origin, 719 CustomTabsService.RELATION_USE_AS_ORIGIN); 720 } 721 722 /** Tries to bind to a client to keep it alive, and returns true for success. */ keepAliveForSession(CustomTabsSessionToken session, Intent intent)723 public synchronized boolean keepAliveForSession(CustomTabsSessionToken session, Intent intent) { 724 // When an application is bound to a service, its priority is raised to 725 // be at least equal to the application's one. This binds to a dummy 726 // service (no calls to this service are made). 727 if (intent == null || intent.getComponent() == null) return false; 728 SessionParams params = mSessionParams.get(session); 729 if (params == null) return false; 730 731 KeepAliveServiceConnection connection = params.getKeepAliveConnection(); 732 733 if (connection == null) { 734 String packageName = intent.getComponent().getPackageName(); 735 PackageManager pm = ContextUtils.getApplicationContext().getPackageManager(); 736 // Only binds to the application associated to this session. 737 if (!Arrays.asList(pm.getPackagesForUid(params.uid)).contains(packageName)) { 738 return false; 739 } 740 Intent serviceIntent = new Intent().setComponent(intent.getComponent()); 741 connection = new KeepAliveServiceConnection( 742 ContextUtils.getApplicationContext(), serviceIntent); 743 } 744 745 boolean ok = connection.connect(); 746 if (ok) params.setKeepAliveConnection(connection); 747 return ok; 748 } 749 750 /** Unbind from the KeepAlive service for a client. */ dontKeepAliveForSession(CustomTabsSessionToken session)751 public synchronized void dontKeepAliveForSession(CustomTabsSessionToken session) { 752 SessionParams params = mSessionParams.get(session); 753 if (params == null || params.getKeepAliveConnection() == null) return; 754 KeepAliveServiceConnection connection = params.getKeepAliveConnection(); 755 connection.disconnect(); 756 } 757 758 /** See {@link RequestThrottler#isPrerenderingAllowed()} */ isPrerenderingAllowed(int uid)759 public synchronized boolean isPrerenderingAllowed(int uid) { 760 return RequestThrottler.getForUid(uid).isPrerenderingAllowed(); 761 } 762 763 /** See {@link RequestThrottler#registerPrerenderRequest(String)} */ registerPrerenderRequest(int uid, String url)764 public synchronized void registerPrerenderRequest(int uid, String url) { 765 RequestThrottler.getForUid(uid).registerPrerenderRequest(url); 766 } 767 768 /** See {@link RequestThrottler#reset()} */ resetThrottling(int uid)769 public synchronized void resetThrottling(int uid) { 770 RequestThrottler.getForUid(uid).reset(); 771 } 772 773 /** See {@link RequestThrottler#ban()} */ ban(int uid)774 public synchronized void ban(int uid) { 775 RequestThrottler.getForUid(uid).ban(); 776 } 777 778 /** 779 * Cleans up all data associated with all sessions. 780 */ cleanupAll()781 public synchronized void cleanupAll() { 782 // cleanupSessionInternal modifies mSessionParams therefore we need a copy 783 List<CustomTabsSessionToken> sessions = new ArrayList<>(mSessionParams.keySet()); 784 for (CustomTabsSessionToken session : sessions) cleanupSession(session); 785 } 786 787 /** 788 * Handle any clean up left after a session is destroyed. 789 * @param session The session that has been destroyed. 790 */ cleanupSessionInternal(CustomTabsSessionToken session)791 private synchronized void cleanupSessionInternal(CustomTabsSessionToken session) { 792 SessionParams params = mSessionParams.get(session); 793 if (params == null) return; 794 mSessionParams.remove(session); 795 if (params.serviceConnection != null) { 796 params.serviceConnection.cleanup(ContextUtils.getApplicationContext()); 797 } 798 if (params.originVerifier != null) params.originVerifier.cleanUp(); 799 if (params.disconnectCallback != null) params.disconnectCallback.run(session); 800 mUidHasCalledWarmup.delete(params.uid); 801 } 802 803 /** 804 * Destroys session when its callback become invalid if the callback is used as identifier. 805 * 806 * @param session The session with invalid callback. 807 */ cleanupSession(CustomTabsSessionToken session)808 public synchronized void cleanupSession(CustomTabsSessionToken session) { 809 if (session.hasId()) { 810 // Leave session parameters, so client might update callback later. 811 // The session will be completely removed when system runs low on memory. 812 // {@see #cleanupUnusedSessions} 813 mSessionParams.get(session).setCustomTabsCallback(null); 814 } else { 815 cleanupSessionInternal(session); 816 } 817 } 818 819 /** 820 * Clean up all sessions which are not currently used. 821 */ cleanupUnusedSessions()822 public synchronized void cleanupUnusedSessions() { 823 // cleanupSessionInternal modifies mSessionParams therefore we need a copy 824 List<CustomTabsSessionToken> sessions = new ArrayList<>(mSessionParams.keySet()); 825 for (CustomTabsSessionToken session : sessions) { 826 if (mSessionParams.get(session).getCustomTabsCallback() == null) { 827 cleanupSessionInternal(session); 828 } 829 } 830 } 831 } 832