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