summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/process')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja19
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java927
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java40
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java213
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java63
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java74
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java613
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java141
8 files changed, 2090 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja
new file mode 100644
index 0000000000..fa2f336566
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoChildProcessServices.jinja
@@ -0,0 +1,19 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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;
+
+public class GeckoChildProcessServices {
+ /* package */ static final int MAX_NUM_ISOLATED_CONTENT_SERVICES = {{MOZ_ANDROID_CONTENT_SERVICE_COUNT}};
+ public static final class gmplugin extends GeckoServiceChildProcess {}
+ public static final class socket extends GeckoServiceChildProcess {}
+ public static final class gpu extends GeckoServiceGpuProcess {}
+ public static final class utility extends GeckoServiceChildProcess {}
+ public static final class ipdlunittest extends GeckoServiceChildProcess {}
+
+{% for id in range(0, MOZ_ANDROID_CONTENT_SERVICE_COUNT | int) %}
+ public static final class tab{{ id }} extends GeckoServiceChildProcess {}
+{% endfor %}
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
new file mode 100644
index 0000000000..039396f9e8
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
@@ -0,0 +1,927 @@
+/* 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.os.DeadObjectException;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+import androidx.collection.SimpleArrayMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoNetworkManager;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoThread.FileDescriptors;
+import org.mozilla.gecko.GeckoThread.ParcelFileDescriptors;
+import org.mozilla.gecko.IGeckoEditableChild;
+import org.mozilla.gecko.IGeckoEditableParent;
+import org.mozilla.gecko.TelemetryUtils;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.CompositorSurfaceManager;
+import org.mozilla.gecko.gfx.ISurfaceAllocator;
+import org.mozilla.gecko.gfx.RemoteSurfaceAllocator;
+import org.mozilla.gecko.mozglue.JNIObject;
+import org.mozilla.gecko.process.ServiceAllocator.PriorityLevel;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.XPCOMEventTarget;
+import org.mozilla.geckoview.GeckoResult;
+
+public final class GeckoProcessManager extends IProcessManager.Stub {
+ private static final String LOGTAG = "GeckoProcessManager";
+ private static final GeckoProcessManager INSTANCE = new GeckoProcessManager();
+ private static final int INVALID_PID = 0;
+
+ // This id univocally identifies the current process manager instance
+ private final String mInstanceId;
+
+ public static GeckoProcessManager getInstance() {
+ return INSTANCE;
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void setEditableChildParent(
+ final IGeckoEditableChild child, final IGeckoEditableParent parent) {
+ try {
+ child.transferParent(parent);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Cannot set parent", e);
+ }
+ }
+
+ @WrapForJNI(stubName = "GetEditableParent", dispatchTo = "gecko")
+ private static native void nativeGetEditableParent(
+ IGeckoEditableChild child, long contentId, long tabId);
+
+ @Override // IProcessManager
+ public void getEditableParent(
+ final IGeckoEditableChild child, final long contentId, final long tabId) {
+ nativeGetEditableParent(child, contentId, tabId);
+ }
+
+ /**
+ * Returns the surface allocator interface to be used by child processes to allocate Surfaces. The
+ * service bound to the returned interface may live in either the GPU process or parent process.
+ */
+ @Override // IProcessManager
+ public ISurfaceAllocator getSurfaceAllocator() {
+ final GeckoResult<Boolean> gpuEnabled = GeckoAppShell.isGpuProcessEnabled();
+
+ try {
+ final GeckoResult<ISurfaceAllocator> allocator = new GeckoResult<>();
+ if (gpuEnabled.poll(1000)) {
+ // The GPU process is enabled, so look it up and ask it for its surface allocator.
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ final Selector selector = new Selector(GeckoProcessType.GPU);
+ final GpuProcessConnection conn =
+ (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn != null) {
+ allocator.complete(conn.getSurfaceAllocator());
+ } else {
+ // If we cannot find a GPU process, it has probably been killed and not yet
+ // restarted. Return null here, and allow the caller to try again later.
+ // We definitely do *not* want to return the parent process allocator instead, as
+ // that will result in surfaces being allocated in the parent process, which
+ // therefore won't be usable when the GPU process is eventually launched.
+ allocator.complete(null);
+ }
+ });
+ } else {
+ // The GPU process is disabled, so return the parent process allocator instance.
+ allocator.complete(RemoteSurfaceAllocator.getInstance(0));
+ }
+ return allocator.poll(100);
+ } catch (final Throwable e) {
+ Log.e(LOGTAG, "Error in getSurfaceAllocator", e);
+ return null;
+ }
+ }
+
+ @WrapForJNI
+ public static CompositorSurfaceManager getCompositorSurfaceManager() {
+ final Selector selector = new Selector(GeckoProcessType.GPU);
+ final GpuProcessConnection conn =
+ (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return null;
+ }
+ return conn.getCompositorSurfaceManager();
+ }
+
+ /** Gecko uses this class to uniquely identify a process managed by GeckoProcessManager. */
+ public static final class Selector {
+ private final GeckoProcessType mType;
+ private final int mPid;
+
+ @WrapForJNI
+ private Selector(@NonNull final GeckoProcessType type, final int pid) {
+ if (pid == INVALID_PID) {
+ throw new RuntimeException("Invalid PID");
+ }
+
+ mType = type;
+ mPid = pid;
+ }
+
+ @WrapForJNI
+ private Selector(@NonNull final GeckoProcessType type) {
+ mType = type;
+ mPid = INVALID_PID;
+ }
+
+ public GeckoProcessType getType() {
+ return mType;
+ }
+
+ public int getPid() {
+ return mPid;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (obj == ((Object) this)) {
+ return true;
+ }
+
+ final Selector other = (Selector) obj;
+ return mType == other.mType && mPid == other.mPid;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[] {mType, mPid});
+ }
+ }
+
+ private static final class IncompleteChildConnectionException extends RuntimeException {
+ public IncompleteChildConnectionException(@NonNull final String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Maintains state pertaining to an individual child process. Inheriting from
+ * ServiceAllocator.InstanceInfo enables this class to work with ServiceAllocator.
+ */
+ private static class ChildConnection extends ServiceAllocator.InstanceInfo {
+ private IChildProcess mChild;
+ private GeckoResult<IChildProcess> mPendingBind;
+ private int mPid;
+
+ protected ChildConnection(
+ @NonNull final ServiceAllocator allocator,
+ @NonNull final GeckoProcessType type,
+ @NonNull final PriorityLevel initialPriority) {
+ super(allocator, type, initialPriority);
+ mPid = INVALID_PID;
+ }
+
+ public int getPid() {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (mChild == null) {
+ throw new IncompleteChildConnectionException(
+ "Calling ChildConnection.getPid() on an incomplete connection");
+ }
+
+ return mPid;
+ }
+
+ private GeckoResult<IChildProcess> completeFailedBind(
+ @NonNull final ServiceAllocator.BindException e) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ Log.e(LOGTAG, "Failed bind", e);
+
+ if (mPendingBind == null) {
+ throw new IllegalStateException("Bind failed with null mPendingBind");
+ }
+
+ final GeckoResult<IChildProcess> bindResult = mPendingBind;
+ mPendingBind = null;
+ unbind().accept(v -> bindResult.completeExceptionally(e));
+ return bindResult;
+ }
+
+ public GeckoResult<IChildProcess> bind() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (mChild != null) {
+ // Already bound
+ return GeckoResult.fromValue(mChild);
+ }
+
+ if (mPendingBind != null) {
+ // Bind in progress
+ return mPendingBind;
+ }
+
+ mPendingBind = new GeckoResult<>();
+ try {
+ if (!bindService()) {
+ throw new ServiceAllocator.BindException("Cannot connect to process");
+ }
+ } catch (final ServiceAllocator.BindException e) {
+ return completeFailedBind(e);
+ }
+
+ return mPendingBind;
+ }
+
+ public GeckoResult<Void> unbind() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (mPendingBind != null) {
+ // We called unbind() while bind() was still pending completion
+ return mPendingBind.then(child -> unbind());
+ }
+
+ if (mChild == null) {
+ // Not bound in the first place
+ return GeckoResult.fromValue(null);
+ }
+
+ unbindService();
+
+ return GeckoResult.fromValue(null);
+ }
+
+ @Override
+ protected void onBinderConnected(final IBinder service) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final IChildProcess child = IChildProcess.Stub.asInterface(service);
+ try {
+ mPid = child.getPid();
+ onBinderConnected(child);
+ } catch (final DeadObjectException e) {
+ unbindService();
+
+ // mPendingBind might be null if a bind was initiated by the system (eg Service Restart)
+ if (mPendingBind != null) {
+ mPendingBind.completeExceptionally(e);
+ mPendingBind = null;
+ }
+
+ return;
+ } catch (final RemoteException e) {
+ throw new RuntimeException(e);
+ }
+
+ mChild = child;
+ GeckoProcessManager.INSTANCE.mConnections.onBindComplete(this);
+
+ // mPendingBind might be null if a bind was initiated by the system (eg Service Restart)
+ if (mPendingBind != null) {
+ mPendingBind.complete(mChild);
+ mPendingBind = null;
+ }
+ }
+
+ // Subclasses of ChildConnection can override this method to make any IChildProcess calls
+ // specific to their process type immediately after connection.
+ protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException {}
+
+ @Override
+ protected void onReleaseResources() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ // NB: This must happen *before* resetting mPid!
+ GeckoProcessManager.INSTANCE.mConnections.removeConnection(this);
+
+ mChild = null;
+ mPid = INVALID_PID;
+ }
+ }
+
+ private static class NonContentConnection extends ChildConnection {
+ public NonContentConnection(
+ @NonNull final ServiceAllocator allocator, @NonNull final GeckoProcessType type) {
+ super(allocator, type, PriorityLevel.FOREGROUND);
+ if (type == GeckoProcessType.CONTENT) {
+ throw new AssertionError("Attempt to create a NonContentConnection as CONTENT");
+ }
+ }
+
+ protected void onAppForeground() {
+ setPriorityLevel(PriorityLevel.FOREGROUND);
+ }
+
+ protected void onAppBackground() {
+ setPriorityLevel(PriorityLevel.IDLE);
+ }
+ }
+
+ private static final class GpuProcessConnection extends NonContentConnection {
+ private CompositorSurfaceManager mCompositorSurfaceManager;
+ private ISurfaceAllocator mSurfaceAllocator;
+
+ // Unique ID used to identify each GPU process instance. Will always be non-zero,
+ // and unlike the process' pid cannot be the same value for successive instances.
+ private int mUniqueGpuProcessId;
+ // Static counter used to initialize each instance's mUniqueGpuProcessId
+ private static int sUniqueGpuProcessIdCounter = 0;
+
+ public GpuProcessConnection(@NonNull final ServiceAllocator allocator) {
+ super(allocator, GeckoProcessType.GPU);
+
+ // Initialize the unique ID ensuring we skip 0 (as that is reserved for parent process
+ // allocators).
+ if (sUniqueGpuProcessIdCounter == 0) {
+ sUniqueGpuProcessIdCounter++;
+ }
+ mUniqueGpuProcessId = sUniqueGpuProcessIdCounter++;
+ }
+
+ @Override
+ protected void onBinderConnected(@NonNull final IChildProcess child) throws RemoteException {
+ mCompositorSurfaceManager = new CompositorSurfaceManager(child.getCompositorSurfaceManager());
+ mSurfaceAllocator = child.getSurfaceAllocator(mUniqueGpuProcessId);
+ }
+
+ public CompositorSurfaceManager getCompositorSurfaceManager() {
+ return mCompositorSurfaceManager;
+ }
+
+ public ISurfaceAllocator getSurfaceAllocator() {
+ return mSurfaceAllocator;
+ }
+ }
+
+ private static final class SocketProcessConnection extends NonContentConnection {
+ private boolean mIsForeground = true;
+ private boolean mIsNetworkUp = true;
+
+ public SocketProcessConnection(@NonNull final ServiceAllocator allocator) {
+ super(allocator, GeckoProcessType.SOCKET);
+ GeckoProcessManager.INSTANCE.mConnections.enableNetworkNotifications();
+ }
+
+ public void onNetworkStateChange(final boolean isNetworkUp) {
+ mIsNetworkUp = isNetworkUp;
+ prioritize();
+ }
+
+ @Override
+ protected void onAppForeground() {
+ mIsForeground = true;
+ prioritize();
+ }
+
+ @Override
+ protected void onAppBackground() {
+ mIsForeground = false;
+ prioritize();
+ }
+
+ private static final PriorityLevel[][] sPriorityStates = initPriorityStates();
+
+ private static PriorityLevel[][] initPriorityStates() {
+ final PriorityLevel[][] states = new PriorityLevel[2][2];
+ // Background, no network
+ states[0][0] = PriorityLevel.IDLE;
+ // Background, network
+ states[0][1] = PriorityLevel.BACKGROUND;
+ // Foreground, no network
+ states[1][0] = PriorityLevel.IDLE;
+ // Foreground, network
+ states[1][1] = PriorityLevel.FOREGROUND;
+ return states;
+ }
+
+ private void prioritize() {
+ final PriorityLevel nextPriority =
+ sPriorityStates[mIsForeground ? 1 : 0][mIsNetworkUp ? 1 : 0];
+ setPriorityLevel(nextPriority);
+ }
+ }
+
+ private static final class ContentConnection extends ChildConnection {
+ private static final String TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME =
+ "GV_CONTENT_PROCESS_LIFETIME_MS";
+
+ private TelemetryUtils.UptimeTimer mLifetimeTimer = null;
+
+ public ContentConnection(
+ @NonNull final ServiceAllocator allocator, @NonNull final PriorityLevel initialPriority) {
+ super(allocator, GeckoProcessType.CONTENT, initialPriority);
+ }
+
+ @Override
+ protected void onBinderConnected(final IBinder service) {
+ mLifetimeTimer = new TelemetryUtils.UptimeTimer(TELEMETRY_PROCESS_LIFETIME_HISTOGRAM_NAME);
+ super.onBinderConnected(service);
+ }
+
+ @Override
+ protected void onReleaseResources() {
+ if (mLifetimeTimer != null) {
+ mLifetimeTimer.stop();
+ mLifetimeTimer = null;
+ }
+
+ super.onReleaseResources();
+ }
+ }
+
+ /** This class manages the state surrounding existing connections and their priorities. */
+ private static final class ConnectionManager extends JNIObject {
+ // Connections to non-content processes
+ private final ArrayMap<GeckoProcessType, NonContentConnection> mNonContentConnections;
+ // Mapping of pid to content process
+ private final SimpleArrayMap<Integer, ContentConnection> mContentPids;
+ // Set of initialized content process connections
+ private final ArraySet<ContentConnection> mContentConnections;
+ // Set of bound but uninitialized content connections
+ private final ArraySet<ContentConnection> mNonStartedContentConnections;
+ // Allocator for service IDs
+ private final ServiceAllocator mServiceAllocator;
+ private boolean mIsObservingNetwork = false;
+
+ public ConnectionManager() {
+ mNonContentConnections = new ArrayMap<GeckoProcessType, NonContentConnection>();
+ mContentPids = new SimpleArrayMap<Integer, ContentConnection>();
+ mContentConnections = new ArraySet<ContentConnection>();
+ mNonStartedContentConnections = new ArraySet<ContentConnection>();
+ mServiceAllocator = new ServiceAllocator();
+
+ // Attach to native once JNI is ready.
+ if (GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) {
+ attachTo(this);
+ } else {
+ GeckoThread.queueNativeCallUntil(
+ GeckoThread.State.JNI_READY, ConnectionManager.class, "attachTo", this);
+ }
+ }
+
+ private void enableNetworkNotifications() {
+ if (mIsObservingNetwork) {
+ return;
+ }
+
+ mIsObservingNetwork = true;
+
+ // Ensure that GeckoNetworkManager is monitoring network events so that we can
+ // prioritize the socket process.
+ ThreadUtils.runOnUiThread(
+ () -> {
+ GeckoNetworkManager.getInstance().enableNotifications();
+ });
+
+ observeNetworkNotifications();
+ }
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private static native void attachTo(ConnectionManager instance);
+
+ @WrapForJNI(dispatchTo = "gecko")
+ private native void observeNetworkNotifications();
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onBackground() {
+ XPCOMEventTarget.runOnLauncherThread(() -> onAppBackgroundInternal());
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onForeground() {
+ XPCOMEventTarget.runOnLauncherThread(() -> onAppForegroundInternal());
+ }
+
+ @WrapForJNI(calledFrom = "gecko")
+ private void onNetworkStateChange(final boolean isUp) {
+ XPCOMEventTarget.runOnLauncherThread(() -> onNetworkStateChangeInternal(isUp));
+ }
+
+ @Override
+ protected native void disposeNative();
+
+ private void onAppBackgroundInternal() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ for (final NonContentConnection conn : mNonContentConnections.values()) {
+ conn.onAppBackground();
+ }
+ }
+
+ private void onAppForegroundInternal() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ for (final NonContentConnection conn : mNonContentConnections.values()) {
+ conn.onAppForeground();
+ }
+ }
+
+ private void onNetworkStateChangeInternal(final boolean isUp) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final SocketProcessConnection conn =
+ (SocketProcessConnection) mNonContentConnections.get(GeckoProcessType.SOCKET);
+ if (conn == null) {
+ return;
+ }
+
+ conn.onNetworkStateChange(isUp);
+ }
+
+ private void removeContentConnection(@NonNull final ChildConnection conn) {
+ if (!mContentConnections.remove(conn)) {
+ throw new RuntimeException("Attempt to remove non-registered connection");
+ }
+ mNonStartedContentConnections.remove(conn);
+
+ final int pid;
+
+ try {
+ pid = conn.getPid();
+ } catch (final IncompleteChildConnectionException e) {
+ // conn lost its binding before it was able to retrieve its pid. It follows that
+ // mContentPids does not have an entry for this connection, so we can just return.
+ return;
+ }
+
+ if (pid == INVALID_PID) {
+ return;
+ }
+
+ final ChildConnection removed = mContentPids.remove(Integer.valueOf(pid));
+ if (removed != null && removed != conn) {
+ throw new RuntimeException(
+ "Integrity error - connection mismatch for pid " + Integer.toString(pid));
+ }
+ }
+
+ public void removeConnection(@NonNull final ChildConnection conn) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (conn.getType() == GeckoProcessType.CONTENT) {
+ removeContentConnection(conn);
+ return;
+ }
+
+ final ChildConnection removed = mNonContentConnections.remove(conn.getType());
+ if (removed != conn) {
+ throw new RuntimeException(
+ "Integrity error - connection mismatch for process type " + conn.getType().toString());
+ }
+ }
+
+ /** Saves any state information that was acquired upon start completion. */
+ public void onBindComplete(@NonNull final ChildConnection conn) {
+ if (conn.getType() == GeckoProcessType.CONTENT) {
+ final int pid = conn.getPid();
+ if (pid == INVALID_PID) {
+ throw new AssertionError(
+ "PID is invalid even though our caller just successfully retrieved it after binding");
+ }
+
+ mContentPids.put(pid, (ContentConnection) conn);
+ }
+ }
+
+ /** Retrieve the ChildConnection for an already running content process. */
+ private ContentConnection getExistingContentConnection(@NonNull final Selector selector) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (selector.getType() != GeckoProcessType.CONTENT) {
+ throw new IllegalArgumentException("Selector is not for content!");
+ }
+
+ return mContentPids.get(selector.getPid());
+ }
+
+ /** Unconditionally create a new content connection for the specified priority. */
+ private ContentConnection getNewContentConnection(@NonNull final PriorityLevel newPriority) {
+ final ContentConnection result = new ContentConnection(mServiceAllocator, newPriority);
+ mContentConnections.add(result);
+
+ return result;
+ }
+
+ /** Retrieve the ChildConnection for an already running child process of any type. */
+ public ChildConnection getExistingConnection(@NonNull final Selector selector) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final GeckoProcessType type = selector.getType();
+
+ if (type == GeckoProcessType.CONTENT) {
+ return getExistingContentConnection(selector);
+ }
+
+ return mNonContentConnections.get(type);
+ }
+
+ /**
+ * Retrieve a ChildConnection for a content process for the purposes of starting. If there are
+ * any preloaded content processes already running, we will use one of those. Otherwise we will
+ * allocate a new ChildConnection.
+ */
+ private ChildConnection getContentConnectionForStart() {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ if (mNonStartedContentConnections.isEmpty()) {
+ return getNewContentConnection(PriorityLevel.FOREGROUND);
+ }
+
+ final ChildConnection conn =
+ mNonStartedContentConnections.removeAt(mNonStartedContentConnections.size() - 1);
+ conn.setPriorityLevel(PriorityLevel.FOREGROUND);
+ return conn;
+ }
+
+ /** Retrieve or create a new child process for the specified non-content process. */
+ private ChildConnection getNonContentConnection(@NonNull final GeckoProcessType type) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ if (type == GeckoProcessType.CONTENT) {
+ throw new IllegalArgumentException("Content processes not supported by this method");
+ }
+
+ NonContentConnection connection = mNonContentConnections.get(type);
+ if (connection == null) {
+ if (type == GeckoProcessType.SOCKET) {
+ connection = new SocketProcessConnection(mServiceAllocator);
+ } else if (type == GeckoProcessType.GPU) {
+ connection = new GpuProcessConnection(mServiceAllocator);
+ } else {
+ connection = new NonContentConnection(mServiceAllocator, type);
+ }
+
+ mNonContentConnections.put(type, connection);
+ }
+
+ return connection;
+ }
+
+ /** Retrieve a ChildConnection for the purposes of starting a new child process. */
+ public ChildConnection getConnectionForStart(@NonNull final GeckoProcessType type) {
+ if (type == GeckoProcessType.CONTENT) {
+ return getContentConnectionForStart();
+ }
+
+ return getNonContentConnection(type);
+ }
+
+ /** Retrieve a ChildConnection for the purposes of preloading a new child process. */
+ public ChildConnection getConnectionForPreload(@NonNull final GeckoProcessType type) {
+ if (type == GeckoProcessType.CONTENT) {
+ final ContentConnection conn = getNewContentConnection(PriorityLevel.BACKGROUND);
+ mNonStartedContentConnections.add(conn);
+ return conn;
+ }
+
+ return getNonContentConnection(type);
+ }
+ }
+
+ private final ConnectionManager mConnections;
+
+ private GeckoProcessManager() {
+ mConnections = new ConnectionManager();
+ mInstanceId = UUID.randomUUID().toString();
+ }
+
+ public void preload(final GeckoProcessType... types) {
+ XPCOMEventTarget.launcherThread()
+ .execute(
+ () -> {
+ for (final GeckoProcessType type : types) {
+ final ChildConnection connection = mConnections.getConnectionForPreload(type);
+ connection.bind();
+ }
+ });
+ }
+
+ public void crashChild(@NonNull final Selector selector) {
+ XPCOMEventTarget.launcherThread()
+ .execute(
+ () -> {
+ final ChildConnection conn = mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return;
+ }
+
+ conn.bind()
+ .accept(
+ proc -> {
+ try {
+ proc.crash();
+ } catch (final RemoteException e) {
+ }
+ });
+ });
+ }
+
+ @WrapForJNI
+ private static void shutdownProcess(final Selector selector) {
+ XPCOMEventTarget.assertOnLauncherThread();
+ final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return;
+ }
+
+ conn.unbind();
+ }
+
+ @WrapForJNI
+ private static void setProcessPriority(
+ @NonNull final Selector selector,
+ @NonNull final PriorityLevel priorityLevel,
+ final int relativeImportance) {
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ final ChildConnection conn = INSTANCE.mConnections.getExistingConnection(selector);
+ if (conn == null) {
+ return;
+ }
+
+ conn.setPriorityLevel(priorityLevel, relativeImportance);
+ });
+ }
+
+ @WrapForJNI
+ private static GeckoResult<Integer> start(
+ final GeckoProcessType type,
+ final String[] args,
+ final int prefsFd,
+ final int prefMapFd,
+ final int ipcFd,
+ final int crashFd,
+ final int crashAnnotationFd) {
+ final GeckoResult<Integer> result = new GeckoResult<>();
+ final StartInfo info =
+ new StartInfo(
+ type,
+ GeckoThread.InitInfo.builder()
+ .args(args)
+ .userSerialNumber(System.getenv("MOZ_ANDROID_USER_SERIAL_NUMBER"))
+ .extras(GeckoThread.getActiveExtras())
+ .flags(filterFlagsForChild(GeckoThread.getActiveFlags()))
+ .fds(
+ FileDescriptors.builder()
+ .prefs(prefsFd)
+ .prefMap(prefMapFd)
+ .ipc(ipcFd)
+ .crashReporter(crashFd)
+ .crashAnnotation(crashAnnotationFd)
+ .build())
+ .build());
+
+ XPCOMEventTarget.runOnLauncherThread(
+ () -> {
+ INSTANCE
+ .start(info)
+ .accept(result::complete, result::completeExceptionally)
+ .finally_(info.pfds::close);
+ });
+
+ return result;
+ }
+
+ private static int filterFlagsForChild(final int flags) {
+ return flags & GeckoThread.FLAG_ENABLE_NATIVE_CRASHREPORTER;
+ }
+
+ private static class StartInfo {
+ final GeckoProcessType type;
+ final String crashHandler;
+ final GeckoThread.InitInfo init;
+
+ final ParcelFileDescriptors pfds;
+
+ private StartInfo(final GeckoProcessType type, final GeckoThread.InitInfo initInfo) {
+ this.type = type;
+ this.init = initInfo;
+ crashHandler =
+ GeckoAppShell.getCrashHandlerService() != null
+ ? GeckoAppShell.getCrashHandlerService().getName()
+ : null;
+ // The native side owns the File Descriptors so we cannot call adopt here.
+ pfds = ParcelFileDescriptors.from(initInfo.fds);
+ }
+ }
+
+ private static final int MAX_RETRIES = 3;
+
+ private GeckoResult<Integer> start(final StartInfo info) {
+ return start(info, new ArrayList<>());
+ }
+
+ private GeckoResult<Integer> retry(
+ final StartInfo info, final List<Throwable> retryLog, final Throwable error) {
+ retryLog.add(error);
+
+ if (error instanceof StartException) {
+ final StartException startError = (StartException) error;
+ if (startError.errorCode == IChildProcess.STARTED_BUSY) {
+ // This process is owned by a different runtime, so we can't use
+ // it. We will keep retrying indefinitely until we find a non-busy process.
+ // Note: this strategy is pretty bad, we go through each process in
+ // sequence until one works, the multiple runtime case is test-only
+ // for now, so that's ok. We can improve on this if we eventually
+ // end up needing something fancier.
+ return start(info, retryLog);
+ }
+ }
+
+ // If we couldn't unbind there's something very wrong going on and we bail
+ // immediately.
+ if (retryLog.size() >= MAX_RETRIES || error instanceof UnbindException) {
+ return GeckoResult.fromException(fromRetryLog(retryLog));
+ }
+
+ return start(info, retryLog);
+ }
+
+ private String serializeLog(final List<Throwable> retryLog) {
+ if (retryLog == null || retryLog.size() == 0) {
+ return "Empty log.";
+ }
+
+ final StringBuilder message = new StringBuilder();
+
+ for (final Throwable error : retryLog) {
+ if (error instanceof UnbindException) {
+ message.append("Could not unbind: ");
+ } else if (error instanceof StartException) {
+ message.append("Cannot restart child: ");
+ } else {
+ message.append("Error while binding: ");
+ }
+ message.append(error);
+ message.append(";");
+ }
+
+ return message.toString();
+ }
+
+ private RuntimeException fromRetryLog(final List<Throwable> retryLog) {
+ return new RuntimeException(serializeLog(retryLog), retryLog.get(retryLog.size() - 1));
+ }
+
+ private GeckoResult<Integer> start(final StartInfo info, final List<Throwable> retryLog) {
+ return startInternal(info).then(GeckoResult::fromValue, error -> retry(info, retryLog, error));
+ }
+
+ private static class StartException extends RuntimeException {
+ public final int errorCode;
+
+ public StartException(final int errorCode, final int pid) {
+ super("Could not start process, errorCode: " + errorCode + " PID: " + pid);
+ this.errorCode = errorCode;
+ }
+ }
+
+ private GeckoResult<Integer> startInternal(final StartInfo info) {
+ XPCOMEventTarget.assertOnLauncherThread();
+
+ final ChildConnection connection = mConnections.getConnectionForStart(info.type);
+ return connection
+ .bind()
+ .map(
+ child -> {
+ final int result =
+ child.start(
+ this,
+ mInstanceId,
+ info.init.args,
+ info.init.extras,
+ info.init.flags,
+ info.init.userSerialNumber,
+ info.crashHandler,
+ info.pfds.prefs,
+ info.pfds.prefMap,
+ info.pfds.ipc,
+ info.pfds.crashReporter,
+ info.pfds.crashAnnotation);
+ if (result == IChildProcess.STARTED_OK) {
+ return connection.getPid();
+ } else {
+ throw new StartException(result, connection.getPid());
+ }
+ })
+ .then(GeckoResult::fromValue, error -> handleBindError(connection, error));
+ }
+
+ private GeckoResult<Integer> handleBindError(
+ final ChildConnection connection, final Throwable error) {
+ return connection
+ .unbind()
+ .then(
+ unused -> GeckoResult.fromException(error),
+ unbindError -> GeckoResult.fromException(new UnbindException(unbindError)));
+ }
+
+ private static class UnbindException extends RuntimeException {
+ public UnbindException(final Throwable cause) {
+ super(cause);
+ }
+ }
+} // GeckoProcessManager
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java
new file mode 100644
index 0000000000..812a27614c
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessType.java
@@ -0,0 +1,40 @@
+/* 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 org.mozilla.gecko.annotation.WrapForJNI;
+
+@WrapForJNI
+public enum GeckoProcessType {
+ // These need to match the stringified names from the GeckoProcessType enum
+ PARENT("default"),
+ PLUGIN("plugin"),
+ CONTENT("tab"),
+ IPDLUNITTEST("ipdlunittest"),
+ GMPLUGIN("gmplugin"),
+ GPU("gpu"),
+ VR("vr"),
+ RDD("rdd"),
+ SOCKET("socket"),
+ REMOTESANDBOXBROKER("sandboxbroker"),
+ FORKSERVER("forkserver"),
+ UTILITY("utility");
+
+ private final String mGeckoName;
+
+ private GeckoProcessType(final String geckoName) {
+ mGeckoName = geckoName;
+ }
+
+ @Override
+ public String toString() {
+ return mGeckoName;
+ }
+
+ @WrapForJNI
+ private static final GeckoProcessType fromInt(final int type) {
+ return values()[type];
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
new file mode 100644
index 0000000000..e030a47c74
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
@@ -0,0 +1,213 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoThread.FileDescriptors;
+import org.mozilla.gecko.GeckoThread.ParcelFileDescriptors;
+import org.mozilla.gecko.IGeckoEditableChild;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.ICompositorSurfaceManager;
+import org.mozilla.gecko.gfx.ISurfaceAllocator;
+import org.mozilla.gecko.util.ThreadUtils;
+
+public class GeckoServiceChildProcess extends Service {
+ private static final String LOGTAG = "ServiceChildProcess";
+
+ private static IProcessManager sProcessManager;
+ private static String sOwnerProcessId;
+ private final MemoryController mMemoryController = new MemoryController();
+
+ // Makes sure we don't reuse this process
+ private static boolean sCreateCalled;
+
+ @WrapForJNI(calledFrom = "gecko")
+ private static void getEditableParent(
+ final IGeckoEditableChild child, final long contentId, final long tabId) {
+ try {
+ sProcessManager.getEditableParent(child, contentId, tabId);
+ } catch (final RemoteException e) {
+ Log.e(LOGTAG, "Cannot get editable", e);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.i(LOGTAG, "onCreate");
+
+ if (sCreateCalled) {
+ // We don't support reusing processes, and this could get us in a really weird state,
+ // so let's throw here.
+ throw new RuntimeException("Cannot reuse process.");
+ }
+ sCreateCalled = true;
+
+ GeckoAppShell.setApplicationContext(getApplicationContext());
+ GeckoThread.launch(); // Preload Gecko.
+ }
+
+ protected static class ChildProcessBinder extends IChildProcess.Stub {
+ @Override
+ public int getPid() {
+ return Process.myPid();
+ }
+
+ @Override
+ public int start(
+ final IProcessManager procMan,
+ final String mainProcessId,
+ final String[] args,
+ final Bundle extras,
+ final int flags,
+ final String userSerialNumber,
+ final String crashHandlerService,
+ final ParcelFileDescriptor prefsPfd,
+ final ParcelFileDescriptor prefMapPfd,
+ final ParcelFileDescriptor ipcPfd,
+ final ParcelFileDescriptor crashReporterPfd,
+ final ParcelFileDescriptor crashAnnotationPfd) {
+
+ final ParcelFileDescriptors pfds =
+ ParcelFileDescriptors.builder()
+ .prefs(prefsPfd)
+ .prefMap(prefMapPfd)
+ .ipc(ipcPfd)
+ .crashReporter(crashReporterPfd)
+ .crashAnnotation(crashAnnotationPfd)
+ .build();
+
+ synchronized (GeckoServiceChildProcess.class) {
+ if (sOwnerProcessId != null && !sOwnerProcessId.equals(mainProcessId)) {
+ Log.w(
+ LOGTAG,
+ "This process belongs to a different GeckoRuntime owner: "
+ + sOwnerProcessId
+ + " process: "
+ + mainProcessId);
+ // We need to close the File Descriptors here otherwise we will leak them causing a
+ // shutdown hang.
+ pfds.close();
+ return IChildProcess.STARTED_BUSY;
+ }
+ if (sProcessManager != null) {
+ Log.e(LOGTAG, "Child process already started");
+ pfds.close();
+ return IChildProcess.STARTED_FAIL;
+ }
+ sProcessManager = procMan;
+ sOwnerProcessId = mainProcessId;
+ }
+
+ final FileDescriptors fds = pfds.detach();
+ ThreadUtils.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (crashHandlerService != null) {
+ try {
+ @SuppressWarnings("unchecked")
+ final Class<? extends Service> crashHandler =
+ (Class<? extends Service>) Class.forName(crashHandlerService);
+
+ // Native crashes are reported through pipes, so we don't have to
+ // do anything special for that.
+ GeckoAppShell.setCrashHandlerService(crashHandler);
+ GeckoAppShell.ensureCrashHandling(crashHandler);
+ } catch (final ClassNotFoundException e) {
+ Log.w(LOGTAG, "Couldn't find crash handler service " + crashHandlerService);
+ }
+ }
+
+ final GeckoThread.InitInfo info =
+ GeckoThread.InitInfo.builder()
+ .args(args)
+ .extras(extras)
+ .flags(flags)
+ .userSerialNumber(userSerialNumber)
+ .fds(fds)
+ .build();
+
+ if (GeckoThread.init(info)) {
+ GeckoThread.launch();
+ }
+ }
+ });
+ return IChildProcess.STARTED_OK;
+ }
+
+ @Override
+ public void crash() {
+ GeckoThread.crash();
+ }
+
+ @Override
+ public ICompositorSurfaceManager getCompositorSurfaceManager() {
+ Log.e(
+ LOGTAG, "Invalid call to IChildProcess.getCompositorSurfaceManager for non-GPU process");
+ throw new AssertionError(
+ "Invalid call to IChildProcess.getCompositorSurfaceManager for non-GPU process.");
+ }
+
+ @Override
+ public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) {
+ Log.e(LOGTAG, "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process");
+ throw new AssertionError(
+ "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process.");
+ }
+ }
+
+ protected Binder createBinder() {
+ return new ChildProcessBinder();
+ }
+
+ private final Binder mBinder = createBinder();
+
+ @Override
+ public void onDestroy() {
+ Log.i(LOGTAG, "Destroying GeckoServiceChildProcess");
+ System.exit(0);
+ }
+
+ @Override
+ public IBinder onBind(final Intent intent) {
+ // Calling stopSelf ensures that whenever the client unbinds the process dies immediately.
+ stopSelf();
+ return mBinder;
+ }
+
+ @Override
+ public void onTrimMemory(final int level) {
+ mMemoryController.onTrimMemory(level);
+
+ // This is currently a no-op in Service, but let's future-proof.
+ super.onTrimMemory(level);
+ }
+
+ @Override
+ public void onLowMemory() {
+ mMemoryController.onLowMemory();
+ super.onLowMemory();
+ }
+
+ /**
+ * Returns the surface allocator interface that should be used by this process to allocate
+ * Surfaces, for consumption in either the GPU process or parent process.
+ */
+ public static ISurfaceAllocator getSurfaceAllocator() throws RemoteException {
+ return sProcessManager.getSurfaceAllocator();
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java
new file mode 100644
index 0000000000..e4312c7e67
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java
@@ -0,0 +1,63 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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.os.Binder;
+import android.util.SparseArray;
+import android.view.Surface;
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.gfx.ICompositorSurfaceManager;
+import org.mozilla.gecko.gfx.ISurfaceAllocator;
+import org.mozilla.gecko.gfx.RemoteSurfaceAllocator;
+
+public class GeckoServiceGpuProcess extends GeckoServiceChildProcess {
+ private static final String LOGTAG = "ServiceGpuProcess";
+
+ private static final class GpuProcessBinder extends GeckoServiceChildProcess.ChildProcessBinder {
+ @Override
+ public ICompositorSurfaceManager getCompositorSurfaceManager() {
+ return RemoteCompositorSurfaceManager.getInstance();
+ }
+
+ @Override
+ public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) {
+ return RemoteSurfaceAllocator.getInstance(allocatorId);
+ }
+ }
+
+ @Override
+ protected Binder createBinder() {
+ return new GpuProcessBinder();
+ }
+
+ public static final class RemoteCompositorSurfaceManager extends ICompositorSurfaceManager.Stub {
+ private static RemoteCompositorSurfaceManager mInstance;
+
+ @WrapForJNI
+ private static synchronized RemoteCompositorSurfaceManager getInstance() {
+ if (mInstance == null) {
+ mInstance = new RemoteCompositorSurfaceManager();
+ }
+ return mInstance;
+ }
+
+ private final SparseArray<Surface> mSurfaces = new SparseArray<Surface>();
+
+ @Override
+ public synchronized void onSurfaceChanged(final int widgetId, final Surface surface) {
+ if (surface != null) {
+ mSurfaces.put(widgetId, surface);
+ } else {
+ mSurfaces.remove(widgetId);
+ }
+ }
+
+ @WrapForJNI
+ public synchronized Surface getCompositorSurface(final int widgetId) {
+ return mSurfaces.get(widgetId);
+ }
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java
new file mode 100644
index 0000000000..f2dcb7a52b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/MemoryController.java
@@ -0,0 +1,74 @@
+/* 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.content.ComponentCallbacks2;
+import android.content.res.Configuration;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import org.mozilla.gecko.GeckoAppShell;
+
+public class MemoryController implements ComponentCallbacks2 {
+ private static final String LOGTAG = "MemoryController";
+ private long mLastLowMemoryNotificationTime = 0;
+
+ // Allowed elapsed time between full GCs while under constant memory pressure
+ private static final long LOW_MEMORY_ONGOING_RESET_TIME_MS = 10000;
+
+ private static final int LOW = 0;
+ private static final int MODERATE = 1;
+ private static final int CRITICAL = 2;
+
+ private int memoryLevelFromTrim(final int level) {
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE
+ || level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
+ return CRITICAL;
+ } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
+ return MODERATE;
+ }
+ return LOW;
+ }
+
+ public void onTrimMemory(final int level) {
+ Log.i(LOGTAG, "onTrimMemory(" + level + ")");
+ onMemoryNotification(memoryLevelFromTrim(level));
+ }
+
+ @Override
+ public void onConfigurationChanged(final @NonNull Configuration newConfig) {}
+
+ public void onLowMemory() {
+ Log.i(LOGTAG, "onLowMemory");
+ onMemoryNotification(CRITICAL);
+ }
+
+ private void onMemoryNotification(final int level) {
+ if (level == LOW) {
+ // The trim level is too low to be actionable
+ return;
+ }
+
+ // See nsIMemory.idl for descriptions of the various arguments to the "memory-pressure"
+ // observer.
+ final String observerArg;
+
+ final long currentNotificationTime = System.currentTimeMillis();
+ if (level == CRITICAL
+ || (currentNotificationTime - mLastLowMemoryNotificationTime)
+ >= LOW_MEMORY_ONGOING_RESET_TIME_MS) {
+ // We do a full "low-memory" notification for both new and last-ditch onTrimMemory requests.
+ observerArg = "low-memory";
+ mLastLowMemoryNotificationTime = currentNotificationTime;
+ } else {
+ // If it has been less than ten seconds since the last time we sent a "low-memory"
+ // notification, we send a "low-memory-ongoing" notification instead.
+ // This prevents Gecko from re-doing full GC's repeatedly over and over in succession,
+ // as they are expensive and quickly result in diminishing returns.
+ observerArg = "low-memory-ongoing";
+ }
+
+ GeckoAppShell.notifyObservers("memory-pressure", observerArg);
+ }
+}
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);
+ }
+}
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java
new file mode 100644
index 0000000000..695c69666b
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java
@@ -0,0 +1,141 @@
+/* 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.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import androidx.annotation.NonNull;
+
+/* package */ final class ServiceUtils {
+ private static final String DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX = "0";
+
+ private ServiceUtils() {}
+
+ /**
+ * @return StringBuilder containing the name of a service class but not qualifed with any unique
+ * identifiers.
+ */
+ private static StringBuilder startSvcName(@NonNull final GeckoProcessType type) {
+ final StringBuilder builder = new StringBuilder(GeckoChildProcessServices.class.getName());
+ builder.append("$").append(type);
+ return builder;
+ }
+
+ /**
+ * Given a service's GeckoProcessType, obtain the name of its class, including any qualifiers that
+ * are needed to uniquely identify its manifest definition.
+ */
+ public static String buildSvcName(
+ @NonNull final GeckoProcessType type, final String... suffixes) {
+ final StringBuilder builder = startSvcName(type);
+
+ for (final String suffix : suffixes) {
+ builder.append(suffix);
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Given a service's GeckoProcessType, obtain the name of its class to be used for the purpose of
+ * binding as an isolated service.
+ *
+ * <p>Content services are defined in the manifest as "tab0" through "tabN" for some value of N.
+ * For the purposes of binding to an isolated content service, we simply need to repeatedly re-use
+ * the definition of "tab0", the "0" being stored as the
+ * DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX constant.
+ */
+ public static String buildIsolatedSvcName(@NonNull final GeckoProcessType type) {
+ if (type == GeckoProcessType.CONTENT) {
+ return buildSvcName(type, DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX);
+ }
+
+ // Non-content services do not require any unique IDs
+ return buildSvcName(type);
+ }
+
+ /**
+ * Given a service's GeckoProcessType, obtain the unqualified name of its class.
+ *
+ * @return The name of the class that hosts the implementation of the service corresponding to
+ * type, but without any unique identifiers that may be required to actually instantiate it.
+ */
+ private static String buildSvcNamePrefix(@NonNull final GeckoProcessType type) {
+ return startSvcName(type).toString();
+ }
+
+ /**
+ * Extracts flags from the manifest definition of a service.
+ *
+ * @param context Context to use for extraction
+ * @param type Service type
+ * @return flags that are specified in the service's definition in our manifest.
+ * @see android.content.pm.ServiceInfo for explanation of the various flags.
+ */
+ public static int getServiceFlags(
+ @NonNull final Context context, @NonNull final GeckoProcessType type) {
+ final ComponentName component = new ComponentName(context, buildIsolatedSvcName(type));
+ final PackageManager pkgMgr = context.getPackageManager();
+
+ try {
+ final ServiceInfo svcInfo = pkgMgr.getServiceInfo(component, 0);
+ // svcInfo is never null
+ return svcInfo.flags;
+ } catch (final PackageManager.NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Obtain the list of all services defined for |context|. */
+ private static ServiceInfo[] getServiceList(@NonNull final Context context) {
+ final PackageInfo packageInfo;
+ try {
+ packageInfo =
+ context
+ .getPackageManager()
+ .getPackageInfo(context.getPackageName(), PackageManager.GET_SERVICES);
+ } catch (final PackageManager.NameNotFoundException e) {
+ throw new AssertionError("Should not happen: Can't get package info of own package");
+ }
+ return packageInfo.services;
+ }
+
+ /**
+ * Count the number of service definitions in our manifest that satisfy bindings for a particular
+ * service type.
+ *
+ * @param context Context object to use for extracting the service definitions
+ * @param type The type of service to count
+ * @return The number of available service definitions.
+ */
+ public static int getServiceCount(
+ @NonNull final Context context, @NonNull final GeckoProcessType type) {
+ final ServiceInfo[] svcList = getServiceList(context);
+ final String serviceNamePrefix = buildSvcNamePrefix(type);
+
+ int result = 0;
+ for (final ServiceInfo svc : svcList) {
+ final String svcName = svc.name;
+ // If svcName starts with serviceNamePrefix, then both strings must either be equal
+ // or else the first subsequent character in svcName must be a digit.
+ // This guards against any future GeckoProcessType whose string representation shares
+ // a common prefix with another GeckoProcessType value.
+ if (svcName.startsWith(serviceNamePrefix)
+ && (svcName.length() == serviceNamePrefix.length()
+ || Character.isDigit(svcName.codePointAt(serviceNamePrefix.length())))) {
+ ++result;
+ }
+ }
+
+ if (result <= 0) {
+ throw new RuntimeException("Could not count " + serviceNamePrefix + " services in manifest");
+ }
+
+ return result;
+ }
+}