chromium/content/public/android/java/src/org/chromium/content/browser/SpareChildConnection.java

// Copyright 2017 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.content.browser;

import android.content.Context;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.process_launcher.ChildConnectionAllocator;
import org.chromium.base.process_launcher.ChildProcessConnection;

/**
 * This class is used to create a single spare ChildProcessConnection (usually early on during
 * start-up) that can then later be retrieved when a connection to a service is needed.
 */
public class SpareChildConnection {
    private static final String TAG = "SpareChildConn";

    // The allocator used to create the connection.
    private final ChildConnectionAllocator mConnectionAllocator;

    // The actual spare connection.
    private ChildProcessConnection mConnection;

    // True when there is a spare connection and it is bound.
    private boolean mConnectionReady;

    // The callback that should be called when the connection becomes bound. Set when the connection
    // is retrieved.
    private ChildProcessConnection.ServiceCallback mConnectionServiceCallback;

    /** Creates and binds a ChildProcessConnection using the specified parameters. */
    public SpareChildConnection(
            Context context, ChildConnectionAllocator connectionAllocator, Bundle serviceBundle) {
        assert LauncherThread.runningOnLauncherThread();

        mConnectionAllocator = connectionAllocator;

        ChildProcessConnection.ServiceCallback serviceCallback =
                new ChildProcessConnection.ServiceCallback() {
                    @Override
                    public void onChildStarted() {
                        assert LauncherThread.runningOnLauncherThread();
                        mConnectionReady = true;
                        if (mConnectionServiceCallback != null) {
                            mConnectionServiceCallback.onChildStarted();
                            clearConnection();
                        }
                        // If there is no chained callback, that means the spare connection has not
                        // been used yet. It will be cleared when used.
                    }

                    @Override
                    public void onChildStartFailed(ChildProcessConnection connection) {
                        assert LauncherThread.runningOnLauncherThread();
                        Log.w(TAG, "Failed to warm up the spare sandbox service");
                        if (mConnectionServiceCallback != null) {
                            mConnectionServiceCallback.onChildStartFailed(connection);
                        }
                        clearConnection();
                    }

                    @Override
                    public void onChildProcessDied(ChildProcessConnection connection) {
                        if (mConnectionServiceCallback != null) {
                            mConnectionServiceCallback.onChildProcessDied(connection);
                        }
                        if (mConnection != null) {
                            assert connection == mConnection;
                            clearConnection();
                        }
                    }
                };

        mConnection = mConnectionAllocator.allocate(context, serviceBundle, serviceCallback);
    }

    /**
     * @return a connection that has been bound or is being bound if one was created with the same
     * allocator as the one provided, null otherwise.
     */
    public ChildProcessConnection getConnection(
            ChildConnectionAllocator allocator,
            @NonNull final ChildProcessConnection.ServiceCallback serviceCallback) {
        assert LauncherThread.runningOnLauncherThread();
        if (isEmpty() || mConnectionAllocator != allocator || mConnectionServiceCallback != null) {
            return null;
        }

        mConnectionServiceCallback = serviceCallback;

        ChildProcessConnection connection = mConnection;
        if (mConnectionReady) {
            // onChildStarted was already run. Call it explicitly on the passed serviceCallback.
            if (serviceCallback != null) {
                // Post a task so the callback happens after the caller has retrieved the
                // connection.
                LauncherThread.post(
                        new Runnable() {
                            @Override
                            public void run() {
                                serviceCallback.onChildStarted();
                            }
                        });
            }
            clearConnection();
        }
        return connection;
    }

    /** Returns true if no connection is available (so getConnection will always return null), */
    public boolean isEmpty() {
        // Note that if the connection was retrieved but was not yet ready mConnection is non null
        // but that connection is already used and will be cleared when it becomes ready. In that
        // case mConnectionServiceCallback is non null.
        return mConnection == null || mConnectionServiceCallback != null;
    }

    private void clearConnection() {
        assert LauncherThread.runningOnLauncherThread();
        mConnection = null;
        mConnectionReady = false;
    }

    @VisibleForTesting
    public ChildProcessConnection getConnection() {
        return mConnection;
    }
}