blob: 5a699410f2187ede2e9e12442517c95c553c0032 [file] [log] [blame]
// 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 android.util.SparseBooleanArray;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
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. */
@SuppressFBWarnings("CHROMIUM_SYNCHRONIZED_METHOD")
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;
// Values for the "CustomTabs.CalledWarmup" UMA histogram. Append-only.
@VisibleForTesting static final int NO_SESSION_NO_WARMUP = 0;
@VisibleForTesting static final int NO_SESSION_WARMUP = 1;
@VisibleForTesting static final int SESSION_NO_WARMUP_ALREADY_CALLED = 2;
@VisibleForTesting static final int SESSION_NO_WARMUP_NOT_CALLED = 3;
@VisibleForTesting static final int SESSION_WARMUP = 4;
@VisibleForTesting static final int SESSION_WARMUP_COUNT = 5;
/** Per-session values. */
private static class SessionParams {
public final int uid;
public final String packageName;
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;
packageName = getPackageName(context, uid);
this.callback = callback;
this.deathRecipient = deathRecipient;
}
private static String getPackageName(Context context, int uid) {
PackageManager packageManager = context.getPackageManager();
String[] packageList = packageManager.getPackagesForUid(uid);
if (packageList.length != 1 || TextUtils.isEmpty(packageList[0])) return null;
return 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<>();
private final SparseBooleanArray mUidHasCalledWarmup = new SparseBooleanArray();
private boolean mWarmupHasBeenCalled = false;
public ClientManager(Context context) {
mContext = context.getApplicationContext();
RequestThrottler.loadInBackground(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;
}
/**
* Records that {@link CustomTabsConnection#warmup(long)} has been called from the given uid.
*/
public synchronized void recordUidHasCalledWarmup(int uid) {
mWarmupHasBeenCalled = true;
mUidHasCalledWarmup.put(uid, 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.
*/
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();
}
@VisibleForTesting
synchronized int getWarmupState(IBinder session) {
SessionParams params = mSessionParams.get(session);
boolean hasValidSession = params != null;
boolean hasUidCalledWarmup = hasValidSession && mUidHasCalledWarmup.get(params.uid);
int result = mWarmupHasBeenCalled ? NO_SESSION_WARMUP : NO_SESSION_NO_WARMUP;
if (hasValidSession) {
if (hasUidCalledWarmup) {
result = SESSION_WARMUP;
} else {
result = mWarmupHasBeenCalled ? SESSION_NO_WARMUP_ALREADY_CALLED
: SESSION_NO_WARMUP_NOT_CALLED;
}
}
return result;
}
/**
* Registers that a client has launched a URL inside a Custom Tab.
*/
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);
}
RecordHistogram.recordEnumeratedHistogram(
"CustomTabs.WarmupStateOnLaunch", getWarmupState(session), SESSION_WARMUP_COUNT);
}
/**
* @return The referrer that is associated with the client owning given session.
*/
public synchronized Referrer getReferrerForSession(IBinder session) {
SessionParams params = mSessionParams.get(session);
if (params == null) return null;
final String packageName = params.packageName;
return IntentHandler.constructValidReferrerForAuthority(packageName);
}
/**
* @return The package name associated with the client owning the given session.
*/
public synchronized String getClientPackageNameForSession(IBinder session) {
SessionParams params = mSessionParams.get(session);
return params == null ? null : params.packageName;
}
/**
* @return The callback {@link IBinder} for the given session.
*/
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. */
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. */
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);
}
/** See {@link RequestThrottler#isPrerenderingAllowed()} */
public synchronized boolean isPrerenderingAllowed(int uid) {
return RequestThrottler.getForUid(mContext, uid).isPrerenderingAllowed();
}
/** See {@link RequestThrottler#registerPrerenderRequest(String)} */
public synchronized void registerPrerenderRequest(int uid, String url) {
RequestThrottler.getForUid(mContext, uid).registerPrerenderRequest(url);
}
/** See {@link RequestThrottler#reset()} */
public synchronized void resetThrottling(int uid) {
RequestThrottler.getForUid(mContext, uid).reset();
}
/**
* Cleans up all data associated with all sessions.
*/
public synchronized void cleanupAll() {
List<IBinder> sessions = new ArrayList<>(mSessionParams.keySet());
for (IBinder session : sessions) cleanupSession(session);
}
private synchronized void cleanupSession(IBinder session) {
SessionParams params = mSessionParams.get(session);
if (params == null) return;
mSessionParams.remove(session);
mUidHasCalledWarmup.delete(params.uid);
IBinder binder = params.callback.asBinder();
binder.unlinkToDeath(params.deathRecipient, 0);
}
}