ConnectionHolder.java

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.browser.trusted;

import android.content.ComponentName;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.customtabs.trusted.ITrustedWebActivityService;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;

import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.List;

/**
 * Holds a connection to a TrustedWebActivityService.
 * It should only be used on the UI Thread.
 */
class ConnectionHolder implements ServiceConnection {
    private static final int STATE_AWAITING_CONNECTION = 0;
    private static final int STATE_CONNECTED = 1;
    private static final int STATE_DISCONNECTED = 2;
    private static final int STATE_CANCELLED = 3;

    @NonNull private final Runnable mCloseRunnable;
    @NonNull private final WrapperFactory mWrapperFactory;

    private int mState = STATE_AWAITING_CONNECTION;
    @Nullable private TrustedWebActivityServiceConnection mService;
    @NonNull private List<Completer<TrustedWebActivityServiceConnection>> mCompleters =
            new ArrayList<>();
    @Nullable private Exception mCancellationException;

    /** A class that creates the TrustedWebActivityServiceConnection. Allows mocking in tests. */
    static class WrapperFactory {
        @NonNull
        TrustedWebActivityServiceConnection create(ComponentName name, IBinder iBinder) {
            return new TrustedWebActivityServiceConnection(
                    ITrustedWebActivityService.Stub.asInterface(iBinder), name);
        }
    }

    /**
     * Constructor for production use. Takes in a {@link Runnable} that will be called either when
     * the Service disconnects or {@link #cancel} is called.
     */
    @MainThread
    ConnectionHolder(@NonNull Runnable closeRunnable) {
        this(closeRunnable, new WrapperFactory());
    }

    /** Constructor for testing use. */
    @MainThread
    ConnectionHolder(@NonNull Runnable closeRunnable, @NonNull WrapperFactory factory) {
        mCloseRunnable = closeRunnable;
        mWrapperFactory = factory;
    }

    /** This method will be called on the UI Thread by the Android Framework. */
    @Override
    @MainThread
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        mService = mWrapperFactory.create(componentName, iBinder);
        for (Completer<TrustedWebActivityServiceConnection> completer : mCompleters) {
            completer.set(mService);
        }
        mCompleters.clear();

        mState = STATE_CONNECTED;
    }

    /** This method will be called on the UI Thread by the Android Framework. */
    @Override
    @MainThread
    public void onServiceDisconnected(ComponentName componentName) {
        mService = null;
        mCloseRunnable.run();
        mState = STATE_DISCONNECTED;
    }

    /**
     * Called to signal that the connection attempt failed and that neither
     * {@link #onServiceConnected(ComponentName, IBinder)} or
     * {@link #onServiceDisconnected(ComponentName)} will be called.
     */
    @MainThread
    public void cancel(@NonNull Exception exception) {
        for (Completer<TrustedWebActivityServiceConnection> completer : mCompleters) {
            completer.setException(exception);
        }
        mCompleters.clear();
        mCloseRunnable.run();
        mState = STATE_CANCELLED;
        mCancellationException = exception;
    }

    /**
     * Returns a future that will:
     * - be unset if a connection is still pending and set once open.
     * - be set to a {@link TrustedWebActivityServiceConnection} if a connection is open.
     * - be set to an exception if the connection failed or has been closed.
     */
    @MainThread
    @NonNull
    public ListenableFuture<TrustedWebActivityServiceConnection> getServiceWrapper() {
        // Using CallbackToFutureAdapter and storing the completers gives us some additional safety
        // checks over using Futures ourselves (such as failing the Future if the completer is
        // garbage collected).
        return CallbackToFutureAdapter.getFuture(completer -> {
            switch (mState) {
                case STATE_AWAITING_CONNECTION:
                    mCompleters.add(completer);
                    break;
                case STATE_CONNECTED:
                    if (mService == null) {
                        throw new IllegalStateException("ConnectionHolder state is incorrect.");
                    }
                    completer.set(mService);
                    break;
                case STATE_DISCONNECTED:
                    throw new IllegalStateException("Service has been disconnected.");
                case STATE_CANCELLED:
                    throw mCancellationException;
                default:
                    throw new IllegalStateException("Connection state is invalid");
            }

            return "ConnectionHolder, state = " + mState;
        });
    }
}