blob: 553ed6ac44f45fc5090109ff8e6a99f6c0e9b52d [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.process_launcher;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.BuildInfo;
import org.chromium.base.ChildBindingState;
import org.chromium.base.Log;
import org.chromium.base.MemoryPressureLevel;
import org.chromium.base.MemoryPressureListener;
import org.chromium.base.PackageUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.memory.MemoryPressureCallback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.build.BuildConfig;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import javax.annotation.concurrent.GuardedBy;
/** Manages a connection between the browser activity and a child service. */
public class ChildProcessConnection {
private static final String TAG = "ChildProcessConn";
private static final int FALLBACK_TIMEOUT_IN_SECONDS = 10;
private static final boolean SUPPORT_NOT_PERCEPTIBLE_BINDING =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
/**
* Used to notify the consumer about the process start. These callbacks will be invoked before
* the ConnectionCallbacks.
*/
public interface ServiceCallback {
/**
* Called when the child process has successfully started and is ready for connection
* setup.
*/
void onChildStarted();
/**
* Called when the child process failed to start. This can happen if the process is already
* in use by another client. The client will not receive any other callbacks after this one.
*/
void onChildStartFailed(ChildProcessConnection connection);
/**
* Called when the service has been disconnected. whether it was stopped by the client or
* if it stopped unexpectedly (process crash).
* This is the last callback from this interface that a client will receive for a specific
* connection.
*/
void onChildProcessDied(ChildProcessConnection connection);
}
/** Used to notify the consumer about the connection being established. */
public interface ConnectionCallback {
/**
* Called when the connection to the service is established.
* @param connection the connection object to the child process
*/
void onConnected(ChildProcessConnection connection);
}
/**
* Used to notify the client about the new shared relocations (RELRO) arriving from the app
* zygote.
*/
public interface ZygoteInfoCallback {
/**
* Called after the connection has been established, only once per process being connected
* to.
* @param connection the connection object to the child process. Must hold the zygote PID
* that the child process was spawned from
* @param relroBundle the bundle potentially containing the information making it possible
* to replace the current RELRO address range to memory shared with the
* zpp zygote. Needs to be passed to the library loader in order to take
* effect. Can be done before or after the LibraryLoader loads the
* library.
*/
void onReceivedZygoteInfo(ChildProcessConnection connection, Bundle relroBundle);
}
private static class ChildProcessMismatchException extends RuntimeException {
ChildProcessMismatchException(String msg) {
super(msg);
}
}
/** Run time check if variable number of connections is supported. */
public static boolean supportVariableConnections() {
return BindService.supportVariableConnections();
}
/** Run time check if not perceptible binding is supported. */
public static boolean supportNotPerceptibleBinding() {
return SUPPORT_NOT_PERCEPTIBLE_BINDING;
}
/** The string passed to bindToCaller to identify this class loader. */
@VisibleForTesting
public static String getBindToCallerClazz() {
// TODO(crbug.com/1057102): Have embedder explicitly set separate different strings since
// this could still collide in theory.
ClassLoader cl = ChildProcessConnection.class.getClassLoader();
return cl.toString() + cl.hashCode();
}
// The last zygote PID for which the zygote startup metrics were recorded. Lives on the
// launcher thread.
private static int sLastRecordedZygotePid;
// Only accessed on launcher thread.
// Set when a launching a service with zygote times out, in which case
// it's assumed the zygote code path is not functional. Make all future
// launches directly fallback without timeout to minimize user impact.
private static boolean sAlwaysFallback;
// Lock to protect all the fields that can be accessed outside launcher thread.
private final Object mBindingStateLock = new Object();
private final Handler mLauncherHandler;
private final Executor mLauncherExecutor;
private ComponentName mServiceName;
private final ComponentName mFallbackServiceName;
// Parameters passed to the child process through the service binding intent.
// If the service gets recreated by the framework the intent will be reused, so these parameters
// should be common to all processes of that type.
private final Bundle mServiceBundle;
// Whether bindToCaller should be called on the service after setup to check that only one
// process is bound to the service.
private final boolean mBindToCaller;
private static class ConnectionParams {
final Bundle mConnectionBundle;
final List<IBinder> mClientInterfaces;
ConnectionParams(Bundle connectionBundle, List<IBinder> clientInterfaces) {
mConnectionBundle = connectionBundle;
mClientInterfaces = clientInterfaces;
}
}
// This is set in start() and is used in onServiceConnected().
private ServiceCallback mServiceCallback;
// This is set in setupConnection() and is later used in doConnectionSetup(), after which the
// variable is cleared. Therefore this is only valid while the connection is being set up.
private ConnectionParams mConnectionParams;
// Callback provided in setupConnection() that will communicate the result to the caller. This
// has to be called exactly once after setupConnection(), even if setup fails, so that the
// caller can free up resources associated with the setup attempt. This is set to null after the
// call.
private ConnectionCallback mConnectionCallback;
// Callback provided in setupConnection().
private ZygoteInfoCallback mZygoteInfoCallback;
private IChildProcessService mService;
// Set to true when the service connection callback runs. This differs from
// mServiceConnectComplete, which tracks that the connection completed successfully.
private boolean mDidOnServiceConnected;
// Set to true when the service connected successfully.
private boolean mServiceConnectComplete;
// Set to true when the service disconnects, as opposed to being properly closed. This happens
// when the process crashes or gets killed by the system out-of-memory killer.
private boolean mServiceDisconnected;
// Process ID of the corresponding child process.
private int mPid;
// The PID of the app zygote that the child process was spawned from. Zero if no useful
// information about the app zygote can be obtained.
private int mZygotePid;
/**
* @return true iff the child process notified that it has usable info from the App Zygote.
*/
public boolean hasUsableZygoteInfo() {
assert isRunningOnLauncherThread();
return mZygotePid != 0;
}
// Factory which tests can override to intercept ChildServiceConnection creation.
private final ChildServiceConnectionFactory mConnectionFactory;
// ChildServiceConnectionDelegate for this class which is responsible for posting callbacks to
// the launcher thread, if needed.
private final ChildServiceConnectionDelegate mConnectionDelegate;
// Instance named used on Android 10 and above to create separate instances from the same
// <service> manifest declaration.
private final String mInstanceName;
// Use Context.BIND_EXTERNAL_SERVICE flag for this service.
private final boolean mBindAsExternalService;
// Strong binding will make the service priority equal to the priority of the activity.
private ChildServiceConnection mStrongBinding;
// Visible binding will make the service priority equal to the priority of a visible process
// while the app is in the foreground.
// This is also used as the initial binding before any priorities are set.
private ChildServiceConnection mVisibleBinding;
// On Android Q+ a not perceptible binding will make the service priority below that of a
// perceptible process of a backgrounded app. Only created on Android Q+.
private ChildServiceConnection mNotPerceptibleBinding;
// Low priority binding maintained in the entire lifetime of the connection, i.e. between calls
// to start() and stop().
private ChildServiceConnection mWaivedBinding;
// Refcount of bindings.
private int mStrongBindingCount;
private int mVisibleBindingCount;
private int mNotPerceptibleBindingCount;
private int mGroup;
private int mImportanceInGroup;
// Set to true once unbind() was called.
private boolean mUnbound;
// Binding state of this connection.
@GuardedBy("mBindingStateLock")
private @ChildBindingState int mBindingState;
// Same as above except it no longer updates after |unbind()|.
@GuardedBy("mBindingStateLock")
private @ChildBindingState int mBindingStateCurrentOrWhenDied;
// Indicate |kill()| was called to intentionally kill this process.
@GuardedBy("mBindingStateLock")
private boolean mKilledByUs;
private MemoryPressureCallback mMemoryPressureCallback;
// If the process threw an exception before entering the main loop, the exception
// string is reported here.
@GuardedBy("mBindingStateLock")
private String mExceptionInServiceDuringInit;
// Whether the process exited cleanly or not.
@GuardedBy("mBindingStateLock")
private boolean mCleanExit;
public ChildProcessConnection(
Context context,
ComponentName serviceName,
ComponentName fallbackServiceName,
boolean bindToCaller,
boolean bindAsExternalService,
Bundle serviceBundle,
String instanceName) {
this(
context,
serviceName,
fallbackServiceName,
bindToCaller,
bindAsExternalService,
serviceBundle,
/* connectionFactory= */ null,
instanceName);
}
@VisibleForTesting
public ChildProcessConnection(
final Context context,
ComponentName serviceName,
ComponentName fallbackServiceName,
boolean bindToCaller,
boolean bindAsExternalService,
Bundle serviceBundle,
ChildServiceConnectionFactory connectionFactory,
String instanceName) {
mLauncherHandler = new Handler();
mLauncherExecutor =
(Runnable runnable) -> {
mLauncherHandler.post(runnable);
};
assert isRunningOnLauncherThread();
mServiceName = serviceName;
mFallbackServiceName = fallbackServiceName;
mServiceBundle = serviceBundle != null ? serviceBundle : new Bundle();
mServiceBundle.putBoolean(ChildProcessConstants.EXTRA_BIND_TO_CALLER, bindToCaller);
mServiceBundle.putString(
ChildProcessConstants.EXTRA_BROWSER_PACKAGE_NAME,
BuildInfo.getInstance().packageName);
mBindToCaller = bindToCaller;
mInstanceName = instanceName;
// Incremental install does not work with isolatedProcess, and externalService requires
// isolatedProcess, so both need to be turned off for incremental install.
mBindAsExternalService = bindAsExternalService && !BuildConfig.IS_INCREMENTAL_INSTALL;
if (connectionFactory == null) {
mConnectionFactory =
new ChildServiceConnectionFactory() {
@Override
public ChildServiceConnection createConnection(
Intent bindIntent,
int bindFlags,
ChildServiceConnectionDelegate delegate,
String instanceName) {
return new ChildServiceConnectionImpl(
context,
bindIntent,
bindFlags,
mLauncherHandler,
mLauncherExecutor,
delegate,
instanceName);
}
};
} else {
mConnectionFactory = connectionFactory;
}
// Methods on the delegate are can be called on launcher thread or UI thread, so need to
// handle both cases. See BindService for details.
mConnectionDelegate =
new ChildServiceConnectionDelegate() {
@Override
public void onServiceConnected(final IBinder service) {
if (mLauncherHandler.getLooper() == Looper.myLooper()) {
onServiceConnectedOnLauncherThread(service);
return;
}
mLauncherHandler.post(() -> onServiceConnectedOnLauncherThread(service));
}
@Override
public void onServiceDisconnected() {
if (mLauncherHandler.getLooper() == Looper.myLooper()) {
onServiceDisconnectedOnLauncherThread();
return;
}
mLauncherHandler.post(() -> onServiceDisconnectedOnLauncherThread());
}
};
createBindings(
sAlwaysFallback && mFallbackServiceName != null
? mFallbackServiceName
: mServiceName);
}
private void createBindings(ComponentName serviceName) {
Intent intent = new Intent();
intent.setComponent(serviceName);
if (mServiceBundle != null) {
intent.putExtras(mServiceBundle);
}
int defaultFlags =
Context.BIND_AUTO_CREATE
| (mBindAsExternalService ? Context.BIND_EXTERNAL_SERVICE : 0);
mVisibleBinding =
mConnectionFactory.createConnection(
intent, defaultFlags, mConnectionDelegate, mInstanceName);
if (supportNotPerceptibleBinding()) {
mNotPerceptibleBinding =
mConnectionFactory.createConnection(
intent,
defaultFlags | Context.BIND_NOT_PERCEPTIBLE,
mConnectionDelegate,
mInstanceName);
}
mStrongBinding =
mConnectionFactory.createConnection(
intent,
defaultFlags | Context.BIND_IMPORTANT,
mConnectionDelegate,
mInstanceName);
mWaivedBinding =
mConnectionFactory.createConnection(
intent,
defaultFlags | Context.BIND_WAIVE_PRIORITY,
mConnectionDelegate,
mInstanceName);
}
public final IChildProcessService getService() {
assert isRunningOnLauncherThread();
return mService;
}
public final ComponentName getServiceName() {
assert isRunningOnLauncherThread();
return mServiceName;
}
public boolean isConnected() {
return mService != null;
}
/**
* @return the connection pid, or 0 if not yet connected
*/
public int getPid() {
assert isRunningOnLauncherThread();
return mPid;
}
/**
* @return the app zygote PID known by the connection.
*/
public int getZygotePid() {
assert isRunningOnLauncherThread();
return mZygotePid;
}
/**
* Starts a connection to an IChildProcessService. This must be followed by a call to
* setupConnection() to setup the connection parameters. start() and setupConnection() are
* separate to allow to pass whatever parameters are available in start(), and complete the
* remainder addStrongBinding while reducing the connection setup latency.
* @param useStrongBinding whether a strong binding should be bound by default. If false, an
* initial moderate binding is used.
* @param serviceCallback (optional) callbacks invoked when the child process starts or fails to
* start and when the service stops.
*/
public void start(boolean useStrongBinding, ServiceCallback serviceCallback) {
try {
TraceEvent.begin("ChildProcessConnection.start");
assert isRunningOnLauncherThread();
assert mConnectionParams == null
: "setupConnection() called before start() in ChildProcessConnection.";
mServiceCallback = serviceCallback;
if (!bind(useStrongBinding)) {
Log.e(TAG, "Failed to establish the service connection.");
// We have to notify the caller so that they can free-up associated resources.
// TODO(ppi): Can we hard-fail here?
notifyChildProcessDied();
}
} finally {
TraceEvent.end("ChildProcessConnection.start");
}
}
// This is the same as start, but returns a boolean whether bind succeeded. Also on failure,
// no method is called on |serviceCallback| so the allocation can be tried again. This is
// package private and is meant to be used by Android10WorkaroundAllocatorImpl. See comment
// there for details.
boolean tryStart(boolean useStrongBinding, ServiceCallback serviceCallback) {
try {
TraceEvent.begin("ChildProcessConnection.tryStart");
assert isRunningOnLauncherThread();
assert mConnectionParams == null
: "setupConnection() called before start() in ChildProcessConnection.";
if (!bind(useStrongBinding)) {
return false;
}
mServiceCallback = serviceCallback;
} finally {
TraceEvent.end("ChildProcessConnection.tryStart");
}
return true;
}
/**
* Call bindService again on this connection. This must be called while connection is already
* bound. This is useful for controlling the recency of this connection, and also for updating
*/
public void rebind() {
assert isRunningOnLauncherThread();
if (!isConnected()) return;
assert mWaivedBinding.isBound();
mWaivedBinding.bindServiceConnection();
}
/**
* Sets-up the connection after it was started with start().
*
* @param connectionBundle a bundle passed to the service that can be used to pass various
* parameters to the service
* @param clientInterfaces optional client specified interfaces that the child can use to
* communicate with the parent process
* @param connectionCallback will be called exactly once after the connection is set up or the
* setup fails
* @param zygoteInfoCallback will be called exactly once after the connection is set up
*/
public void setupConnection(
Bundle connectionBundle,
@Nullable List<IBinder> clientInterfaces,
ConnectionCallback connectionCallback,
ZygoteInfoCallback zygoteInfoCallback) {
assert isRunningOnLauncherThread();
assert mConnectionParams == null;
if (mServiceDisconnected) {
Log.w(TAG, "Tried to setup a connection that already disconnected.");
connectionCallback.onConnected(null);
return;
}
try (TraceEvent te = TraceEvent.scoped("ChildProcessConnection.setupConnection")) {
mConnectionCallback = connectionCallback;
mZygoteInfoCallback = zygoteInfoCallback;
mConnectionParams = new ConnectionParams(connectionBundle, clientInterfaces);
// Run the setup if the service is already connected. If not, doConnectionSetup() will
// be called from onServiceConnected().
if (mServiceConnectComplete) {
doConnectionSetup();
}
}
}
/**
* Terminates the connection to IChildProcessService, closing all bindings. It is safe to call
* this multiple times.
*/
public void stop() {
assert isRunningOnLauncherThread();
unbind();
notifyChildProcessDied();
}
public void kill() {
assert isRunningOnLauncherThread();
IChildProcessService service = mService;
unbind();
try {
if (service != null) service.forceKill();
} catch (RemoteException e) {
// Intentionally ignore since we are killing it anyway.
}
synchronized (mBindingStateLock) {
mKilledByUs = true;
}
notifyChildProcessDied();
}
/** Dumps the stack of the child process without crashing it. */
public void dumpProcessStack() {
assert isRunningOnLauncherThread();
IChildProcessService service = mService;
try {
if (service != null) service.dumpProcessStack();
} catch (RemoteException e) {
Log.e(TAG, "Failed to dump process stack.", e);
}
}
@VisibleForTesting
protected void onServiceConnectedOnLauncherThread(IBinder service) {
assert isRunningOnLauncherThread();
// A flag from the parent class ensures we run the post-connection logic only once
// (instead of once per each ChildServiceConnection).
if (mDidOnServiceConnected) {
return;
}
try {
TraceEvent.begin("ChildProcessConnection.ChildServiceConnection.onServiceConnected");
mDidOnServiceConnected = true;
mService = IChildProcessService.Stub.asInterface(service);
if (mBindToCaller) {
try {
if (!mService.bindToCaller(getBindToCallerClazz())) {
if (mServiceCallback != null) {
mServiceCallback.onChildStartFailed(this);
}
unbind();
return;
}
} catch (RemoteException ex) {
// Do not trigger the StartCallback here, since the service is already
// dead and the onChildStopped callback will run from onServiceDisconnected().
Log.e(TAG, "Failed to bind service to connection.", ex);
return;
}
}
// Validate that the child process is running the same code as the parent process.
boolean childMatches;
try {
ApplicationInfo child = mService.getAppInfo();
ApplicationInfo parent = BuildInfo.getInstance().getBrowserApplicationInfo();
// Don't compare splitSourceDirs as isolatedSplits/dynamic feature modules/etc make
// this potentially complicated.
childMatches =
Objects.equals(parent.sourceDir, child.sourceDir)
&& Arrays.equals(
parent.sharedLibraryFiles, child.sharedLibraryFiles);
} catch (RemoteException ex) {
// If the child can't handle getAppInfo then it is old and doesn't match.
childMatches = false;
}
if (!childMatches) {
// Check if it looks like the browser's package version has been changed since the
// browser process launched (i.e. if the install somehow did not kill our process)
PackageInfo latestPackage = PackageUtils.getApplicationPackageInfo(0);
long latestVersionCode = BuildInfo.packageVersionCode(latestPackage);
long loadedVersionCode = BuildInfo.getInstance().versionCode;
if (latestVersionCode != loadedVersionCode) {
// Crashing the process is likely to improve the situation - when we are next
// launched, we should be running the new version and match new children.
throw new ChildProcessMismatchException(
"Child process's classpath doesn't match, and main process's package"
+ " has been updated since process launch; process needs"
+ " restarting!");
} else {
// Crashing the process is unlikely to improve the situation - our classpath
// will probably be the same on next launch and probably still won't match.
// Log an error but just carry on and hope.
Log.e(
TAG,
"Child process's classpath doesn't match, but main process's package"
+ " hasn't changed; the child is likely to be broken!");
}
}
if (mServiceCallback != null) {
mServiceCallback.onChildStarted();
}
mServiceConnectComplete = true;
if (mMemoryPressureCallback == null) {
final MemoryPressureCallback callback = this::onMemoryPressure;
ThreadUtils.postOnUiThread(() -> MemoryPressureListener.addCallback(callback));
mMemoryPressureCallback = callback;
}
// Run the setup if the connection parameters have already been provided. If
// not, doConnectionSetup() will be called from setupConnection().
if (mConnectionParams != null) {
doConnectionSetup();
}
} finally {
TraceEvent.end("ChildProcessConnection.ChildServiceConnection.onServiceConnected");
}
}
@VisibleForTesting
protected void onServiceDisconnectedOnLauncherThread() {
assert isRunningOnLauncherThread();
// Ensure that the disconnection logic runs only once (instead of once per each
// ChildServiceConnection).
if (mServiceDisconnected) {
return;
}
mServiceDisconnected = true;
Log.w(
TAG,
"onServiceDisconnected (crash or killed by oom): pid=%d %s",
mPid,
buildDebugStateString());
stop(); // We don't want to auto-restart on crash. Let the browser do that.
// If we have a pending connection callback, we need to communicate the failure to
// the caller.
if (mConnectionCallback != null) {
mConnectionCallback.onConnected(null);
mConnectionCallback = null;
}
}
private String buildDebugStateString() {
StringBuilder s = new StringBuilder();
s.append("bindings:");
s.append(mWaivedBinding.isBound() ? "W" : " ");
s.append(mVisibleBinding.isBound() ? "V" : " ");
s.append(supportNotPerceptibleBinding() && mNotPerceptibleBinding.isBound() ? "N" : " ");
s.append(mStrongBinding.isBound() ? "S" : " ");
return s.toString();
}
private void onSetupConnectionResultOnLauncherThread(
int pid, int zygotePid, long zygoteStartupTimeMillis, Bundle relroBundle) {
assert isRunningOnLauncherThread();
// The RELRO bundle should be accepted only when establishing the connection. This is to
// prevent untrusted code from controlling shared memory regions in other processes. Make
// further IPCs a noop.
if (mPid != 0) {
Log.e(TAG, "Pid was sent more than once: pid=%d", mPid);
return;
}
mPid = pid;
assert mPid != 0 : "Child service claims to be run by a process of pid=0.";
// Remember zygote pid to detect zygote restarts later.
mZygotePid = zygotePid;
// Newly arrived zygote info sometimes needs to be broadcast to a number of processes.
if (mZygoteInfoCallback != null) {
mZygoteInfoCallback.onReceivedZygoteInfo(this, relroBundle);
}
mZygoteInfoCallback = null;
// Only record the zygote startup time for a process the first time it is sent. The zygote
// may get killed and recreated, so keep track of the last PID recorded to avoid double
// counting. The app may reuse a zygote process if the app is stopped and started again
// quickly, so the startup time of that zygote may be recorded multiple times. There's not
// much we can do about that, and it shouldn't be a major issue.
if (sLastRecordedZygotePid != mZygotePid && hasUsableZygoteInfo()) {
sLastRecordedZygotePid = mZygotePid;
RecordHistogram.recordMediumTimesHistogram(
"Android.ChildProcessStartTimeV2.Zygote", zygoteStartupTimeMillis);
}
if (mConnectionCallback != null) {
mConnectionCallback.onConnected(this);
}
mConnectionCallback = null;
}
/** Passes the zygote bundle to the service. */
public void consumeZygoteBundle(Bundle zygoteBundle) {
if (mService == null) return;
try {
mService.consumeRelroBundle(zygoteBundle);
} catch (RemoteException e) {
// Ignore.
}
}
/**
* Called after the connection parameters have been set (in setupConnection()) *and* a
* connection has been established (as signaled by onServiceConnected()). These two events can
* happen in any order.
*/
private void doConnectionSetup() {
try {
TraceEvent.begin("ChildProcessConnection.doConnectionSetup");
assert mServiceConnectComplete && mService != null;
assert mConnectionParams != null;
IParentProcess parentProcess =
new IParentProcess.Stub() {
@Override
public void finishSetupConnection(
int pid,
int zygotePid,
long zygoteStartupTimeMillis,
Bundle relroBundle) {
mLauncherHandler.post(
() -> {
onSetupConnectionResultOnLauncherThread(
pid,
zygotePid,
zygoteStartupTimeMillis,
relroBundle);
});
}
@Override
public void reportExceptionInInit(String exception) {
synchronized (mBindingStateLock) {
mExceptionInServiceDuringInit = exception;
}
mLauncherHandler.post(createUnbindRunnable());
}
@Override
public void reportCleanExit() {
synchronized (mBindingStateLock) {
mCleanExit = true;
}
mLauncherHandler.post(createUnbindRunnable());
}
private Runnable createUnbindRunnable() {
return new Runnable() {
@Override
public void run() {
unbind();
}
};
}
};
try {
mService.setupConnection(
mConnectionParams.mConnectionBundle,
parentProcess,
mConnectionParams.mClientInterfaces);
} catch (RemoteException re) {
Log.e(TAG, "Failed to setup connection.", re);
}
mConnectionParams = null;
} finally {
TraceEvent.end("ChildProcessConnection.doConnectionSetup");
}
}
private boolean bind(boolean useStrongBinding) {
assert isRunningOnLauncherThread();
assert !mUnbound;
boolean success = bindUsingExistingBindings(useStrongBinding);
boolean usedFallback = sAlwaysFallback && mFallbackServiceName != null;
boolean canFallback = !sAlwaysFallback && mFallbackServiceName != null;
if (!success && !usedFallback && canFallback) {
// Note this error condition is generally transient so `sAlwaysFallback` is
// not set in this code path.
retireAndCreateFallbackBindings();
success = bindUsingExistingBindings(useStrongBinding);
usedFallback = true;
canFallback = false;
}
if (success && !usedFallback && canFallback) {
mLauncherHandler.postDelayed(
this::checkBindTimeOut, FALLBACK_TIMEOUT_IN_SECONDS * 1000);
}
return success;
}
private boolean bindUsingExistingBindings(boolean useStrongBinding) {
assert isRunningOnLauncherThread();
boolean success;
if (useStrongBinding) {
success = mStrongBinding.bindServiceConnection();
if (success) {
mStrongBindingCount++;
}
} else {
success = mVisibleBinding.bindServiceConnection();
if (success) {
mVisibleBindingCount++;
}
}
if (success) {
boolean result = mWaivedBinding.bindServiceConnection();
// One binding already succeeded. Waived binding should succeed too.
assert result;
updateBindingState();
}
return success;
}
private void checkBindTimeOut() {
assert isRunningOnLauncherThread();
assert mFallbackServiceName != null;
if (mDidOnServiceConnected || mServiceDisconnected) {
return;
}
if (mUnbound) {
return;
}
sAlwaysFallback = true;
retireBindingsAndBindFallback();
}
private boolean retireBindingsAndBindFallback() {
assert mFallbackServiceName != null;
boolean isStrongBindingBound = mStrongBinding.isBound();
boolean isVisibleBindingBound = mVisibleBinding.isBound();
boolean isNotPerceptibleBindingBound =
supportNotPerceptibleBinding() && mNotPerceptibleBinding.isBound();
boolean isWaivedBindingBound = mWaivedBinding.isBound();
retireAndCreateFallbackBindings();
// Expect all bindings to succeed or fail together. So early out as soon as
// one binding fails.
if (isStrongBindingBound) {
if (!mStrongBinding.bindServiceConnection()) {
return false;
}
}
if (isVisibleBindingBound) {
if (!mVisibleBinding.bindServiceConnection()) {
return false;
}
}
if (isNotPerceptibleBindingBound) {
if (!mNotPerceptibleBinding.bindServiceConnection()) {
return false;
}
}
if (isWaivedBindingBound) {
if (!mWaivedBinding.bindServiceConnection()) {
return false;
}
}
return true;
}
private void retireAndCreateFallbackBindings() {
assert mFallbackServiceName != null;
Log.w(TAG, "Fallback to %s", mFallbackServiceName);
mStrongBinding.retire();
mVisibleBinding.retire();
if (supportNotPerceptibleBinding()) {
mNotPerceptibleBinding.retire();
}
mWaivedBinding.retire();
createBindings(mFallbackServiceName);
}
@VisibleForTesting
protected void unbind() {
assert isRunningOnLauncherThread();
mService = null;
mConnectionParams = null;
mUnbound = true;
mStrongBinding.unbindServiceConnection();
mWaivedBinding.unbindServiceConnection();
if (supportNotPerceptibleBinding()) {
mNotPerceptibleBinding.unbindServiceConnection();
}
mVisibleBinding.unbindServiceConnection();
updateBindingState();
if (mMemoryPressureCallback != null) {
final MemoryPressureCallback callback = mMemoryPressureCallback;
ThreadUtils.postOnUiThread(() -> MemoryPressureListener.removeCallback(callback));
mMemoryPressureCallback = null;
}
}
public void updateGroupImportance(int group, int importanceInGroup) {
assert isRunningOnLauncherThread();
assert group != 0 || importanceInGroup == 0;
if (mGroup != group || mImportanceInGroup != importanceInGroup) {
mGroup = group;
mImportanceInGroup = importanceInGroup;
if (isConnected()) {
mWaivedBinding.updateGroupImportance(group, importanceInGroup);
}
}
}
public int getGroup() {
assert isRunningOnLauncherThread();
return mGroup;
}
public int getImportanceInGroup() {
assert isRunningOnLauncherThread();
return mImportanceInGroup;
}
public boolean isStrongBindingBound() {
assert isRunningOnLauncherThread();
return mStrongBinding.isBound();
}
public void addStrongBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
Log.w(TAG, "The connection is not bound for %d", getPid());
return;
}
if (mStrongBindingCount == 0) {
mStrongBinding.bindServiceConnection();
updateBindingState();
}
mStrongBindingCount++;
}
public void removeStrongBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
return;
}
assert mStrongBindingCount > 0;
mStrongBindingCount--;
if (mStrongBindingCount == 0) {
mStrongBinding.unbindServiceConnection();
updateBindingState();
}
}
public boolean isVisibleBindingBound() {
assert isRunningOnLauncherThread();
return mVisibleBinding.isBound();
}
public int getVisibleBindingCount() {
assert isRunningOnLauncherThread();
return mVisibleBindingCount;
}
public void addVisibleBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
Log.w(TAG, "The connection is not bound for %d", getPid());
return;
}
if (mVisibleBindingCount == 0) {
mVisibleBinding.bindServiceConnection();
updateBindingState();
}
mVisibleBindingCount++;
}
public void removeVisibleBinding() {
assert isRunningOnLauncherThread();
if (!isConnected()) {
return;
}
assert mVisibleBindingCount > 0;
mVisibleBindingCount--;
if (mVisibleBindingCount == 0) {
mVisibleBinding.unbindServiceConnection();
updateBindingState();
}
}
public boolean isNotPerceptibleBindingBound() {
assert isRunningOnLauncherThread();
return supportNotPerceptibleBinding() && mNotPerceptibleBinding.isBound();
}
public int getNotPerceptibleBindingCount() {
assert isRunningOnLauncherThread();
return mNotPerceptibleBindingCount;
}
public void addNotPerceptibleBinding() {
assert isRunningOnLauncherThread();
assert supportNotPerceptibleBinding();
if (!isConnected()) {
Log.w(TAG, "The connection is not bound for %d", getPid());
return;
}
if (mNotPerceptibleBindingCount == 0) {
mNotPerceptibleBinding.bindServiceConnection();
updateBindingState();
}
mNotPerceptibleBindingCount++;
}
public void removeNotPerceptibleBinding() {
assert isRunningOnLauncherThread();
assert supportNotPerceptibleBinding();
if (!isConnected()) {
return;
}
assert mNotPerceptibleBindingCount > 0;
mNotPerceptibleBindingCount--;
if (mNotPerceptibleBindingCount == 0) {
mNotPerceptibleBinding.unbindServiceConnection();
updateBindingState();
}
}
/**
* @return the current connection binding state.
*/
public @ChildBindingState int bindingStateCurrent() {
// WARNING: this method can be called from a thread other than the launcher thread.
// Note that it returns the current waived bound only state and is racy. This not really
// preventable without changing the caller's API, short of blocking.
synchronized (mBindingStateLock) {
return mBindingState;
}
}
/**
* @return true if the connection is bound and only bound with the waived binding or if the
* connection is unbound and was only bound with the waived binding when it disconnected.
*/
public @ChildBindingState int bindingStateCurrentOrWhenDied() {
// WARNING: this method can be called from a thread other than the launcher thread.
// Note that it returns the current waived bound only state and is racy. This not really
// preventable without changing the caller's API, short of blocking.
synchronized (mBindingStateLock) {
return mBindingStateCurrentOrWhenDied;
}
}
/**
* @return true if the connection is intentionally killed by calling kill().
*/
public boolean isKilledByUs() {
// WARNING: this method can be called from a thread other than the launcher thread.
// Note that it returns the current waived bound only state and is racy. This not really
// preventable without changing the caller's API, short of blocking.
synchronized (mBindingStateLock) {
return mKilledByUs;
}
}
/**
* @return true if the process exited cleanly.
*/
public boolean hasCleanExit() {
synchronized (mBindingStateLock) {
return mCleanExit;
}
}
/**
* @return the exception string if service threw an exception during init.
* null otherwise.
*/
public @Nullable String getExceptionDuringInit() {
synchronized (mBindingStateLock) {
return mExceptionInServiceDuringInit;
}
}
// Should be called any binding is bound or unbound.
private void updateBindingState() {
int newBindingState;
if (mUnbound) {
newBindingState = ChildBindingState.UNBOUND;
} else if (mStrongBinding.isBound()) {
newBindingState = ChildBindingState.STRONG;
} else if (mVisibleBinding.isBound()) {
newBindingState = ChildBindingState.VISIBLE;
} else if (supportNotPerceptibleBinding() && mNotPerceptibleBinding.isBound()) {
newBindingState = ChildBindingState.NOT_PERCEPTIBLE;
} else {
assert mWaivedBinding.isBound();
newBindingState = ChildBindingState.WAIVED;
}
synchronized (mBindingStateLock) {
mBindingState = newBindingState;
if (!mUnbound) {
mBindingStateCurrentOrWhenDied = mBindingState;
}
}
}
private void notifyChildProcessDied() {
if (mServiceCallback != null) {
// Guard against nested calls to this method.
ServiceCallback serviceCallback = mServiceCallback;
mServiceCallback = null;
serviceCallback.onChildProcessDied(this);
}
}
private boolean isRunningOnLauncherThread() {
return mLauncherHandler.getLooper() == Looper.myLooper();
}
public void crashServiceForTesting() {
try {
mService.forceKill();
} catch (RemoteException e) {
// Expected. Ignore.
}
}
public boolean didOnServiceConnectedForTesting() {
return mDidOnServiceConnected;
}
@VisibleForTesting
protected Handler getLauncherHandler() {
return mLauncherHandler;
}
private void onMemoryPressure(@MemoryPressureLevel int pressure) {
mLauncherHandler.post(() -> onMemoryPressureOnLauncherThread(pressure));
}
private void onMemoryPressureOnLauncherThread(@MemoryPressureLevel int pressure) {
if (mService == null) return;
try {
mService.onMemoryPressure(pressure);
} catch (RemoteException ex) {
// Ignore
}
}
}