Index: chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..ec2226c7de16dcee341d57d648126ce49d97d865 |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/ClientManager.java |
@@ -0,0 +1,267 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.customtabs; |
+ |
+import android.content.ComponentName; |
+import android.content.Context; |
+import android.content.Intent; |
+import android.content.ServiceConnection; |
+import android.content.pm.PackageManager; |
+import android.os.IBinder; |
+import android.os.RemoteException; |
+import android.os.SystemClock; |
+import android.support.customtabs.ICustomTabsCallback; |
+import android.text.TextUtils; |
+ |
+import org.chromium.base.ThreadUtils; |
+import org.chromium.base.annotations.SuppressFBWarnings; |
+import org.chromium.base.metrics.RecordHistogram; |
+import org.chromium.chrome.browser.IntentHandler; |
+import org.chromium.content_public.common.Referrer; |
+ |
+import java.util.ArrayList; |
+import java.util.Arrays; |
+import java.util.HashMap; |
+import java.util.List; |
+import java.util.Map; |
+import java.util.concurrent.TimeUnit; |
+ |
+/** Manages the clients' state for Custom Tabs. This class is threadsafe. */ |
+class ClientManager { |
+ // Values for the "CustomTabs.PredictionStatus" UMA histogram. Append-only. |
+ private static final int NO_PREDICTION = 0; |
+ private static final int GOOD_PREDICTION = 1; |
+ private static final int BAD_PREDICTION = 2; |
+ private static final int PREDICTION_STATUS_COUNT = 3; |
+ |
+ /** Per-session values. */ |
+ private static class SessionParams { |
+ public final int uid; |
+ public final Referrer referrer; |
+ public final ICustomTabsCallback callback; |
+ public final IBinder.DeathRecipient deathRecipient; |
+ private ServiceConnection mKeepAliveConnection = null; |
+ private String mPredictedUrl = null; |
+ private long mLastMayLaunchUrlTimestamp = 0; |
+ |
+ public SessionParams(Context context, int uid, ICustomTabsCallback callback, |
+ IBinder.DeathRecipient deathRecipient) { |
+ this.uid = uid; |
+ referrer = constructReferrer(context, uid); |
+ this.callback = callback; |
+ this.deathRecipient = deathRecipient; |
+ } |
+ |
+ private static Referrer constructReferrer(Context context, int uid) { |
+ PackageManager packageManager = context.getPackageManager(); |
+ String[] packageList = packageManager.getPackagesForUid(uid); |
+ if (packageList.length != 1 || TextUtils.isEmpty(packageList[0])) return null; |
+ return IntentHandler.constructValidReferrerForAuthority(packageList[0]); |
+ } |
+ |
+ public ServiceConnection getKeepAliveConnection() { |
+ return mKeepAliveConnection; |
+ } |
+ |
+ public void setKeepAliveConnection(ServiceConnection serviceConnection) { |
+ mKeepAliveConnection = serviceConnection; |
+ } |
+ |
+ public void setPredictionMetrics(String predictedUrl, long lastMayLaunchUrlTimestamp) { |
+ mPredictedUrl = predictedUrl; |
+ mLastMayLaunchUrlTimestamp = lastMayLaunchUrlTimestamp; |
+ } |
+ |
+ public String getPredictedUrl() { |
+ return mPredictedUrl; |
+ } |
+ |
+ public long getLastMayLaunchUrlTimestamp() { |
+ return mLastMayLaunchUrlTimestamp; |
+ } |
+ } |
+ |
+ /** To be called when a client gets disconnected. */ |
+ public interface DisconnectCallback { public void run(IBinder session); } |
+ |
+ private final Context mContext; |
+ private final Map<IBinder, SessionParams> mSessionParams = new HashMap<>(); |
+ |
+ public ClientManager(Context context) { |
+ mContext = context.getApplicationContext(); |
+ RequestThrottler.purgeOldEntries(mContext); |
+ } |
+ |
+ /** Creates a new session. |
+ * |
+ * @param cb Callback provided by the client. |
+ * @param uid Client UID, as returned by Binder.getCallingUid(), |
+ * @param onDisconnect To be called on the UI thread when a client gets disconnected. |
+ * @return true for success. |
+ */ |
+ public boolean newSession( |
+ ICustomTabsCallback cb, int uid, final DisconnectCallback onDisconnect) { |
+ if (cb == null) return false; |
+ final IBinder session = cb.asBinder(); |
+ IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { |
+ @Override |
+ public void binderDied() { |
+ ThreadUtils.postOnUiThread(new Runnable() { |
+ @Override |
+ public void run() { |
+ cleanupSession(session); |
+ onDisconnect.run(session); |
+ } |
+ }); |
+ } |
+ }; |
+ SessionParams params = new SessionParams(mContext, uid, cb, deathRecipient); |
+ synchronized (this) { |
+ if (mSessionParams.containsKey(session)) return false; |
+ try { |
+ session.linkToDeath(deathRecipient, 0); |
+ } catch (RemoteException e) { |
+ // The return code doesn't matter, because this executes when |
+ // the caller has died. |
+ return false; |
+ } |
+ mSessionParams.put(session, params); |
+ } |
+ return true; |
+ } |
+ |
+ /** Updates the client behavior stats and returns whether speculation is allowed. |
+ * |
+ * @param session Client session. |
+ * @param uid As returned by Binder.getCallingUid(). |
+ * @param url Predicted URL. |
+ * @return true if speculation is allowed. |
+ */ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized boolean updateStatsAndReturnWhetherAllowed( |
+ IBinder session, int uid, String url) { |
+ SessionParams params = mSessionParams.get(session); |
+ if (params == null || params.uid != uid) return false; |
+ params.setPredictionMetrics(url, SystemClock.elapsedRealtime()); |
+ RequestThrottler throttler = RequestThrottler.getForUid(mContext, uid); |
+ return throttler.updateStatsAndReturnWhetherAllowed(); |
+ } |
+ |
+ /** |
+ * Registers that a client has launched a URL inside a Custom Tab. |
+ */ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized void registerLaunch(IBinder session, String url) { |
+ int outcome = NO_PREDICTION; |
+ long elapsedTimeMs = -1; |
+ SessionParams params = mSessionParams.get(session); |
+ if (params != null) { |
+ String predictedUrl = params.getPredictedUrl(); |
+ outcome = predictedUrl == null ? NO_PREDICTION : predictedUrl.equals(url) |
+ ? GOOD_PREDICTION |
+ : BAD_PREDICTION; |
+ long now = SystemClock.elapsedRealtime(); |
+ elapsedTimeMs = now - params.getLastMayLaunchUrlTimestamp(); |
+ params.setPredictionMetrics(null, 0); |
+ if (outcome == GOOD_PREDICTION) { |
+ RequestThrottler.getForUid(mContext, params.uid).registerSuccess(url); |
+ } |
+ } |
+ RecordHistogram.recordEnumeratedHistogram( |
+ "CustomTabs.PredictionStatus", outcome, PREDICTION_STATUS_COUNT); |
+ if (outcome == GOOD_PREDICTION) { |
+ RecordHistogram.recordCustomTimesHistogram("CustomTabs.PredictionToLaunch", |
+ elapsedTimeMs, 1, TimeUnit.MINUTES.toMillis(3), TimeUnit.MILLISECONDS, 100); |
+ } |
+ } |
+ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized Referrer getReferrerForSession(IBinder session) { |
+ SessionParams params = mSessionParams.get(session); |
+ return params != null ? params.referrer : null; |
+ } |
+ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized ICustomTabsCallback getCallbackForSession(IBinder session) { |
+ SessionParams params = mSessionParams.get(session); |
+ return params != null ? params.callback : null; |
+ } |
+ |
+ /** Tries to bind to a client to keep it alive, and returns true for success. */ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized boolean keepAliveForSession(IBinder session, Intent intent) { |
+ // When an application is bound to a service, its priority is raised to |
+ // be at least equal to the application's one. This binds to a dummy |
+ // service (no calls to this service are made). |
+ if (intent == null || intent.getComponent() == null) return false; |
+ SessionParams params = mSessionParams.get(session); |
+ if (params == null) return false; |
+ |
+ String packageName = intent.getComponent().getPackageName(); |
+ PackageManager pm = mContext.getApplicationContext().getPackageManager(); |
+ // Only binds to the application associated to this session. |
+ if (!Arrays.asList(pm.getPackagesForUid(params.uid)).contains(packageName)) return false; |
+ Intent serviceIntent = new Intent().setComponent(intent.getComponent()); |
+ // This ServiceConnection doesn't handle disconnects. This is on |
+ // purpose, as it occurs when the remote process has died. Since the |
+ // only use of this connection is to keep the application alive, |
+ // re-connecting would just re-create the process, but the application |
+ // state has been lost at that point, the callbacks invalidated, etc. |
+ ServiceConnection connection = new ServiceConnection() { |
+ @Override |
+ public void onServiceConnected(ComponentName name, IBinder service) {} |
+ @Override |
+ public void onServiceDisconnected(ComponentName name) {} |
+ }; |
+ boolean ok; |
+ try { |
+ ok = mContext.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE); |
+ } catch (SecurityException e) { |
+ return false; |
+ } |
+ if (ok) params.setKeepAliveConnection(connection); |
+ return ok; |
+ } |
+ |
+ /** Unbind from the KeepAlive service for a client. */ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized void dontKeepAliveForSession(IBinder session) { |
+ SessionParams params = mSessionParams.get(session); |
+ if (params == null || params.getKeepAliveConnection() == null) return; |
+ ServiceConnection connection = params.getKeepAliveConnection(); |
+ params.setKeepAliveConnection(null); |
+ mContext.unbindService(connection); |
+ } |
+ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized boolean isPrerenderingAllowed(int uid) { |
+ return RequestThrottler.getForUid(mContext, uid).isPrerenderingAllowed(); |
+ } |
+ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized void registerPrerenderRequest(int uid, String url) { |
+ RequestThrottler.getForUid(mContext, uid).registerPrerenderRequest(url); |
+ } |
+ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized void resetThrottling(int uid) { |
+ RequestThrottler.getForUid(mContext, uid).reset(); |
+ } |
+ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ public synchronized void cleanupAll() { |
+ List<IBinder> sessions = new ArrayList<>(mSessionParams.keySet()); |
+ for (IBinder session : sessions) cleanupSession(session); |
+ } |
+ |
+ @SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD") |
+ private synchronized void cleanupSession(IBinder session) { |
+ SessionParams params = mSessionParams.get(session); |
+ if (params == null) return; |
+ mSessionParams.remove(session); |
+ IBinder binder = params.callback.asBinder(); |
+ binder.unlinkToDeath(params.deathRecipient, 0); |
+ } |
+} |