summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java613
1 files changed, 613 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java
new file mode 100644
index 0000000000..8058d71601
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java
@@ -0,0 +1,613 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.process;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import java.security.SecureRandom;
+import java.util.BitSet;
+import java.util.EnumMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.UUID;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.util.XPCOMEventTarget;
+
+/* package */ final class ServiceAllocator {
+ private static final String LOGTAG = "ServiceAllocator";
+ private static final int MAX_NUM_ISOLATED_CONTENT_SERVICES =
+ GeckoChildProcessServices.MAX_NUM_ISOLATED_CONTENT_SERVICES;
+
+ private static boolean hasQApis() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+ }
+
+ /**
+ * Possible priority levels that are available to child services. Each one maps to a flag that is
+ * passed into Context.bindService().
+ */
+ @WrapForJNI
+ public static enum PriorityLevel {
+ FOREGROUND(Context.BIND_IMPORTANT),
+ BACKGROUND(0),
+ IDLE(Context.BIND_WAIVE_PRIORITY);
+
+ private final int mAndroidFlag;
+
+ private PriorityLevel(final int androidFlag) {
+ mAndroidFlag = androidFlag;
+ }
+
+ public int getAndroidFlag() {
+ return mAndroidFlag;
+ }
+ }
+
+ public static final class BindException extends RuntimeException {
+ public BindException(@NonNull final String msg) {
+ super(msg);
+ }
+ }
+
+ private interface BindServiceDelegate {
+ boolean bindService(ServiceConnection binding, PriorityLevel priority);
+
+ String getServiceName();
+ }
+
+ /**
+ * Abstract class that holds the essential per-service data that is required to work with
+ * ServiceAllocator. ServiceAllocator clients should extend this class when implementing their
+ * per-service connection objects.
+ */
+ public abstract static class InstanceInfo {
+ private class Binding implements ServiceConnection {
+ /**
+ * This implementation of ServiceConnection.onServiceConnected simply bounces the connection
+ * notification over to the launcher thread (if it is not already on it).
+ */
+ @Override
+ public final void onServiceConnected(final ComponentName name, final IBinder service) {
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ onBinderConnectedInternal(service);
+ });
+ }
+
+ /**
+ * This implementation of ServiceConnection.onServiceDisconnected simply bounces the
+ * disconnection notification over to the launcher thread (if it is not already on it).
+ */
+ @Override
+ public final void onServiceDisconnected(final ComponentName name) {
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ onBinderConnectionLostInternal();
+ });
+ }
+ }
+
+ private class DefaultBindDelegate implements BindServiceDelegate {
+ @Override
+ public boolean bindService(
+ @NonNull final ServiceConnection binding, @NonNull final PriorityLevel priority) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final Intent intent = new Intent();
+ intent.setClassName(context, getServiceName());
+ return bindServiceDefault(context, intent, binding, getAndroidFlags(priority));
+ }
+
+ @Override
+ public String getServiceName() {
+ return getSvcClassNameDefault(InstanceInfo.this);
+ }
+ }
+
+ private class IsolatedBindDelegate implements BindServiceDelegate {
+ @Override
+ public boolean bindService(
+ @NonNull final ServiceConnection binding, @NonNull final PriorityLevel priority) {
+ final Context context = GeckoAppShell.getApplicationContext();
+ final Intent intent = new Intent();
+ intent.setClassName(context, getServiceName());
+ return bindServiceIsolated(
+ context, intent, getAndroidFlags(priority), getIdInternal(), binding);
+ }
+
+ @Override
+ public String getServiceName() {
+ return ServiceUtils.buildIsolatedSvcName(getType());
+ }
+ }
+
+ private final ServiceAllocator mAllocator;
+ private final GeckoProcessType mType;
+ private final String mId;
+ private final EnumMap<PriorityLevel, Binding> mBindings;
+ private final BindServiceDelegate mBindDelegate;
+
+ private boolean mCalledConnected = false;
+ private boolean mCalledConnectionLost = false;
+ private boolean mIsDefunct = false;
+
+ private PriorityLevel mCurrentPriority;
+ private int mRelativeImportance = 0;
+
+ protected InstanceInfo(
+ @NonNull final ServiceAllocator allocator,
+ @NonNull final GeckoProcessType type,
+ @NonNull final PriorityLevel initialPriority) {
+ mAllocator = allocator;
+ mType = type;
+ mId = mAllocator.allocate(type);
+ mBindings = new EnumMap<PriorityLevel, Binding>(PriorityLevel.class);
+ mBindDelegate = getBindServiceDelegate();
+
+ mCurrentPriority = initialPriority;
+ }
+
+ private BindServiceDelegate getBindServiceDelegate() {
+ if (mType != GeckoProcessType.CONTENT) {
+ // Non-content services just use default binding
+ return this.new DefaultBindDelegate();
+ }
+
+ // Content services defer to the alloc policy
+ return mAllocator.mContentAllocPolicy.getBindServiceDelegate(this);
+ }
+
+ public PriorityLevel getPriorityLevel() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ return mCurrentPriority;
+ }
+
+ public boolean setPriorityLevel(@NonNull final PriorityLevel newPriority) {
+ return setPriorityLevel(newPriority, 0);
+ }
+
+ public boolean setPriorityLevel(
+ @NonNull final PriorityLevel newPriority, final int relativeImportance) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ mCurrentPriority = newPriority;
+ mRelativeImportance = relativeImportance;
+
+ // If we haven't bound yet then we can just return
+ if (mBindings.size() == 0) {
+ return true;
+ }
+
+ // Otherwise we need to update our bindings
+ return updateBindings();
+ }
+
+ /**
+ * Only content services have unique IDs. This method throws if called for a non-content service
+ * type.
+ */
+ public String getId() {
+ if (mId == null) {
+ throw new RuntimeException("This service does not have a unique id");
+ }
+
+ return mId;
+ }
+
+ /** This method is infallible and returns an empty string for non-content services. */
+ private String getIdInternal() {
+ return mId == null ? "" : mId;
+ }
+
+ public boolean isContent() {
+ return mType == GeckoProcessType.CONTENT;
+ }
+
+ public GeckoProcessType getType() {
+ return mType;
+ }
+
+ protected boolean bindService() {
+ if (mIsDefunct) {
+ final String errorMsg =
+ "Attempt to bind a defunct InstanceInfo for " + mType + " child process";
+ throw new BindException(errorMsg);
+ }
+
+ return updateBindings();
+ }
+
+ /**
+ * Unbinds the service described by |this| and releases our unique ID. This method may safely be
+ * called multiple times even if we are already defunct.
+ */
+ protected void unbindService() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ // This could happen if a service death races with our attempt to shut it down.
+ if (mIsDefunct) {
+ return;
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+
+ // Make a clone of mBindings to iterate over since we're going to mutate the original
+ final EnumMap<PriorityLevel, Binding> cloned = mBindings.clone();
+ for (final Entry<PriorityLevel, Binding> entry : cloned.entrySet()) {
+ try {
+ context.unbindService(entry.getValue());
+ } catch (final IllegalArgumentException e) {
+ // The binding was already dead. That's okay.
+ }
+
+ mBindings.remove(entry.getKey());
+ }
+
+ if (mBindings.size() != 0) {
+ throw new IllegalStateException("Unable to release all bindings");
+ }
+
+ mIsDefunct = true;
+ mAllocator.release(this);
+ onReleaseResources();
+ }
+
+ private void onBinderConnectedInternal(@NonNull final IBinder service) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ // We only care about the first time this is called; subsequent bindings can be ignored.
+ if (mCalledConnected) {
+ return;
+ }
+
+ mCalledConnected = true;
+
+ onBinderConnected(service);
+ }
+
+ private void onBinderConnectionLostInternal() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ // We only care about the first time this is called; subsequent connection errors can be
+ // ignored.
+ if (mCalledConnectionLost) {
+ return;
+ }
+
+ mCalledConnectionLost = true;
+
+ onBinderConnectionLost();
+ }
+
+ protected abstract void onBinderConnected(@NonNull final IBinder service);
+
+ protected abstract void onReleaseResources();
+
+ // Optionally overridable by subclasses, but this is a sane default
+ protected void onBinderConnectionLost() {
+ // The binding has lost its connection, but the binding itself might still be active.
+ // Gecko itself will request a process restart, so here we attempt to unbind so that
+ // Android does not try to automatically restart and reconnect the service.
+ unbindService();
+ }
+
+ /**
+ * This function relies on the fact that the PriorityLevel enum is ordered from highest priority
+ * to lowest priority. We examine the ordinal of the current priority setting, and then iterate
+ * across all possible priority levels, adjusting as necessary. Any priority levels whose
+ * ordinals are less than then current priority level ordinal must be unbound, while all
+ * priority levels whose ordinals are greater than or equal to the current priority level
+ * ordinal must be bound.
+ */
+ @TargetApi(29)
+ private boolean updateBindings() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ int numBindSuccesses = 0;
+ int numBindFailures = 0;
+ int numUnbindSuccesses = 0;
+
+ final Context context = GeckoAppShell.getApplicationContext();
+
+ // This code assumes that the order of the PriorityLevel enum is highest to lowest
+ final int curPriorityOrdinal = mCurrentPriority.ordinal();
+ final PriorityLevel[] levels = PriorityLevel.values();
+
+ for (int curLevelIdx = 0; curLevelIdx < levels.length; ++curLevelIdx) {
+ final PriorityLevel curLevel = levels[curLevelIdx];
+ final Binding existingBinding = mBindings.get(curLevel);
+ final boolean hasExistingBinding = existingBinding != null;
+
+ if (curLevelIdx < curPriorityOrdinal) {
+ // Remove if present
+ if (hasExistingBinding) {
+ try {
+ context.unbindService(existingBinding);
+ ++numUnbindSuccesses;
+ mBindings.remove(curLevel);
+ } catch (final IllegalArgumentException e) {
+ // The binding was already dead. That's okay.
+ ++numUnbindSuccesses;
+ mBindings.remove(curLevel);
+ }
+ }
+ } else {
+ // Normally we only need to do a bind if we do not yet have an existing binding
+ // for this priority level.
+ boolean bindNeeded = !hasExistingBinding;
+
+ // We only update the service group when the binding for this level already
+ // exists and no binds have occurred yet during the current updateBindings call.
+ if (hasExistingBinding && hasQApis() && (numBindSuccesses + numBindFailures) == 0) {
+ // NB: Right now we're passing 0 as the |group| argument, indicating that
+ // the process is not grouped with any other processes. Once we support
+ // Fission we should re-evaluate this.
+ context.updateServiceGroup(existingBinding, 0, mRelativeImportance);
+ // Now we need to call bindService with the existing binding to make this
+ // change take effect.
+ bindNeeded = true;
+ }
+
+ if (bindNeeded) {
+ final Binding useBinding = hasExistingBinding ? existingBinding : this.new Binding();
+ if (mBindDelegate.bindService(useBinding, curLevel)) {
+ ++numBindSuccesses;
+ if (!hasExistingBinding) {
+ mBindings.put(curLevel, useBinding);
+ }
+ } else {
+ ++numBindFailures;
+ }
+ }
+ }
+ }
+
+ final String svcName = mBindDelegate.getServiceName();
+ final StringBuilder builder = new StringBuilder(svcName);
+ builder
+ .append(" updateBindings: ")
+ .append(mCurrentPriority)
+ .append(" priority, ")
+ .append(mRelativeImportance)
+ .append(" importance, ")
+ .append(numBindSuccesses)
+ .append(" successful binds, ")
+ .append(numBindFailures)
+ .append(" failed binds, ")
+ .append(numUnbindSuccesses)
+ .append(" successful unbinds");
+ Log.d(LOGTAG, builder.toString());
+
+ return numBindFailures == 0;
+ }
+ }
+
+ private interface ContentAllocationPolicy {
+ /**
+ * @return BindServiceDelegate that will be used for binding a new content service.
+ */
+ BindServiceDelegate getBindServiceDelegate(InstanceInfo info);
+
+ /**
+ * Allocate an unused service ID for use by the caller.
+ *
+ * @return The new service id.
+ */
+ String allocate();
+
+ /**
+ * Release a previously used service ID.
+ *
+ * @param id The service id being released.
+ */
+ void release(final String id);
+ }
+
+ /**
+ * This policy is intended for Android versions &lt; 10, as well as for content process services
+ * that are not defined as isolated processes. In this case, the number of possible content
+ * service IDs has a fixed upper bound, so we use a BitSet to manage their allocation.
+ */
+ private static final class DefaultContentPolicy implements ContentAllocationPolicy {
+ private final int mMaxNumSvcs;
+ private final BitSet mAllocator;
+ private final SecureRandom mRandom;
+
+ public DefaultContentPolicy() {
+ mMaxNumSvcs = getContentServiceCount();
+ mAllocator = new BitSet(mMaxNumSvcs);
+ mRandom = new SecureRandom();
+ }
+
+ @Override
+ public BindServiceDelegate getBindServiceDelegate(@NonNull final InstanceInfo info) {
+ return info.new DefaultBindDelegate();
+ }
+
+ @Override
+ public String allocate() {
+ final int[] available = new int[mMaxNumSvcs];
+ int size = 0;
+ for (int i = 0; i < mMaxNumSvcs; i++) {
+ if (!mAllocator.get(i)) {
+ available[size] = i;
+ size++;
+ }
+ }
+
+ if (size == 0) {
+ throw new RuntimeException("No more content services available");
+ }
+
+ final int next = available[mRandom.nextInt(size)];
+ mAllocator.set(next);
+ return Integer.toString(next);
+ }
+
+ @Override
+ public void release(final String stringId) {
+ final int id = Integer.valueOf(stringId);
+ if (!mAllocator.get(id)) {
+ throw new IllegalStateException("Releasing an unallocated id=" + id);
+ }
+
+ mAllocator.clear(id);
+ }
+
+ /**
+ * @return The number of content services defined in our manifest.
+ */
+ private static int getContentServiceCount() {
+ return ServiceUtils.getServiceCount(
+ GeckoAppShell.getApplicationContext(), GeckoProcessType.CONTENT);
+ }
+ }
+
+ /**
+ * This policy is intended for Android versions &gt;= 10 when our content process services are
+ * defined in our manifest as having isolated processes. Since isolated services share a single
+ * service definition, there is no longer an Android-induced hard limit on the number of content
+ * processes that may be started. We simply use a monotonically-increasing counter to generate
+ * unique instance IDs in this case.
+ */
+ private static final class IsolatedContentPolicy implements ContentAllocationPolicy {
+ private final Set<String> mRunningServiceIds = new HashSet<>();
+
+ @Override
+ public BindServiceDelegate getBindServiceDelegate(@NonNull final InstanceInfo info) {
+ return info.new IsolatedBindDelegate();
+ }
+
+ /**
+ * We generate a new instance ID simply by incrementing a counter. We do track how many content
+ * services are currently active for the purposes of maintaining the configured limit on number
+ * of simultaneous content processes.
+ */
+ @Override
+ public String allocate() {
+ if (mRunningServiceIds.size() >= MAX_NUM_ISOLATED_CONTENT_SERVICES) {
+ throw new RuntimeException("No more content services available");
+ }
+
+ final String newId = UUID.randomUUID().toString();
+ mRunningServiceIds.add(newId);
+ return newId;
+ }
+
+ /** Just drop the count of active services. */
+ @Override
+ public void release(final String id) {
+ if (!mRunningServiceIds.remove(id)) {
+ throw new IllegalStateException("Releasing an unallocated id");
+ }
+ }
+ }
+
+ /** The policy used for allocating content processes. */
+ private ContentAllocationPolicy mContentAllocPolicy = null;
+
+ /**
+ * Allocate a service ID.
+ *
+ * @param type The type of service.
+ * @return Integer encapsulating the service ID, or null if no ID is necessary.
+ */
+ private String allocate(@NonNull final GeckoProcessType type) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (type != GeckoProcessType.CONTENT) {
+ // No unique id necessary
+ return null;
+ }
+
+ // Lazy initialization of mContentAllocPolicy to ensure that it is constructed on the
+ // launcher thread.
+ if (mContentAllocPolicy == null) {
+ if (canBindIsolated(GeckoProcessType.CONTENT)) {
+ mContentAllocPolicy = new IsolatedContentPolicy();
+ } else {
+ mContentAllocPolicy = new DefaultContentPolicy();
+ }
+ }
+
+ return mContentAllocPolicy.allocate();
+ }
+
+ /**
+ * Free a defunct service's ID if necessary.
+ *
+ * @param info The InstanceInfo-derived object that contains essential information for tearing
+ * down the child service.
+ */
+ private void release(@NonNull final InstanceInfo info) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (!info.isContent()) {
+ return;
+ }
+
+ mContentAllocPolicy.release(info.getId());
+ }
+
+ /**
+ * Find out whether the desired service type is defined in our manifest as having an isolated
+ * process.
+ *
+ * @param type Service type to query
+ * @return true if this service type may use isolated binding, otherwise false.
+ */
+ private static boolean canBindIsolated(@NonNull final GeckoProcessType type) {
+ if (!hasQApis()) {
+ return false;
+ }
+
+ final Context context = GeckoAppShell.getApplicationContext();
+ final int svcFlags = ServiceUtils.getServiceFlags(context, type);
+ return (svcFlags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;
+ }
+
+ /** Convert PriorityLevel into the flags argument to Context.bindService() et al */
+ private static int getAndroidFlags(@NonNull final PriorityLevel priority) {
+ return Context.BIND_AUTO_CREATE | priority.getAndroidFlag();
+ }
+
+ /** Obtain the class name to use for service binding in the default (ie, non-isolated) case. */
+ private static String getSvcClassNameDefault(@NonNull final InstanceInfo info) {
+ return ServiceUtils.buildSvcName(info.getType(), info.getIdInternal());
+ }
+
+ /**
+ * Wrapper for bindService() that utilizes the Context.bindService() overload that accepts an
+ * Executor argument, when available. Otherwise it falls back to the legacy overload.
+ */
+ @TargetApi(29)
+ private static boolean bindServiceDefault(
+ @NonNull final Context context,
+ @NonNull final Intent intent,
+ @NonNull final ServiceConnection conn,
+ final int flags) {
+ if (hasQApis()) {
+ // We always specify the launcher thread as our Executor.
+ return context.bindService(intent, flags, XPCOMEventTarget.launcherThread(), conn);
+ }
+
+ return context.bindService(intent, conn, flags);
+ }
+
+ @TargetApi(29)
+ private static boolean bindServiceIsolated(
+ @NonNull final Context context,
+ @NonNull final Intent intent,
+ final int flags,
+ @NonNull final String instanceId,
+ @NonNull final ServiceConnection conn) {
+ // We always specify the launcher thread as our Executor.
+ return context.bindIsolatedService(
+ intent, flags, instanceId, XPCOMEventTarget.launcherThread(), conn);
+ }
+}