WaitForRepeatingRequestStart.java

/*
 * Copyright 2022 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.camera.camera2.internal.compat.workaround;

import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.Camera2CaptureCallbacks;
import androidx.camera.camera2.internal.SynchronizedCaptureSession;
import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
import androidx.camera.camera2.internal.compat.quirk.CaptureSessionStuckQuirk;
import androidx.camera.core.impl.DeferrableSurface;
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureChain;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;

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

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

/**
 * The workaround is used to wait for the other CameraCaptureSessions to complete their in-flight
 * capture sequences before opening the current session.
 * <p>If it tries to open the CameraCaptureSession before the others to complete their in-flight
 * capture sequences, the current session may fail to be configured.
 *
 * @see CaptureSessionStuckQuirk
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class WaitForRepeatingRequestStart {
    private final boolean mHasCaptureSessionStuckQuirk;
    private final Object mLock = new Object();

    @NonNull
    private final ListenableFuture<Void> mStartStreamingFuture;
    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    CallbackToFutureAdapter.Completer<Void> mStartStreamingCompleter;
    /** Whether the capture session has submitted the repeating request. */
    private boolean mHasSubmittedRepeating;

    /** Constructor of the WaitForRepeatingRequestStart workaround */
    public WaitForRepeatingRequestStart(@NonNull Quirks cameraQuirks) {
        mHasCaptureSessionStuckQuirk = cameraQuirks.contains(CaptureSessionStuckQuirk.class);

        if (shouldWaitRepeatingSubmit()) {
            mStartStreamingFuture = CallbackToFutureAdapter.getFuture(completer -> {
                mStartStreamingCompleter = completer;
                return "WaitForRepeatingRequestStart[" + this + "]";
            });
        } else {
            mStartStreamingFuture = Futures.immediateFuture(null);
        }
    }

    /**
     * Return true if the opening of the session should wait for the other CameraCaptureSessions
     * to complete their in-flight capture sequences before opening the current session.
     */
    public boolean shouldWaitRepeatingSubmit() {
        return mHasCaptureSessionStuckQuirk;
    }

    /** Returns a ListenableFuture to indicate whether the start repeating request is done. */
    @NonNull
    public ListenableFuture<Void> getStartStreamFuture() {
        return Futures.nonCancellationPropagating(mStartStreamingFuture);
    }

    /**
     * For b/146773463: It needs to check all the releasing capture sessions are ready for
     * opening next capture session.
     */
    @NonNull
    public ListenableFuture<Void> openCaptureSession(
            @NonNull CameraDevice cameraDevice,
            @NonNull SessionConfigurationCompat sessionConfigurationCompat,
            @NonNull List<DeferrableSurface> deferrableSurfaces,
            @NonNull List<SynchronizedCaptureSession> closingSessions,
            @NonNull OpenCaptureSession openCaptureSession) {
        List<ListenableFuture<Void>> futureList = new ArrayList<>();
        for (SynchronizedCaptureSession session : closingSessions) {
            futureList.add(session.getOpeningBlocker());
        }

        return FutureChain.from(Futures.successfulAsList(futureList)).transformAsync(
                v -> openCaptureSession.run(cameraDevice, sessionConfigurationCompat,
                        deferrableSurfaces), CameraXExecutors.directExecutor());
    }

    /** Hook the setSingleRepeatingRequest() to know if it has started a repeating request. */
    public int setSingleRepeatingRequest(
            @NonNull CaptureRequest request,
            @NonNull CameraCaptureSession.CaptureCallback listener,
            @NonNull SingleRepeatingRequest singleRepeatingRequest)
            throws CameraAccessException {
        synchronized (mLock) {
            if (shouldWaitRepeatingSubmit()) {
                listener = Camera2CaptureCallbacks.createComboCallback(mCaptureCallback, listener);
                mHasSubmittedRepeating = true;
            }
            return singleRepeatingRequest.run(request, listener);
        }
    }

    /** This should be called when a SynchronizedCaptureSession is stopped or closed. */
    public void onSessionEnd() {
        synchronized (mLock) {
            if (shouldWaitRepeatingSubmit() && !mHasSubmittedRepeating) {
                // If the session is closed before any repeating requests have been issued,
                // then the startStreamingFuture should be cancelled.
                mStartStreamingFuture.cancel(true);
            }
        }
    }

    private final CameraCaptureSession.CaptureCallback mCaptureCallback =
            new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureStarted(@NonNull CameraCaptureSession session,
                        @NonNull CaptureRequest request, long timestamp, long frameNumber) {
                    if (mStartStreamingCompleter != null) {
                        mStartStreamingCompleter.set(null);
                        mStartStreamingCompleter = null;
                    }
                }

                @Override
                public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session,
                        int sequenceId) {
                    if (mStartStreamingCompleter != null) {
                        mStartStreamingCompleter.setCancelled();
                        mStartStreamingCompleter = null;
                    }
                }
            };

    /** Interface to forward call of the setSingleRepeatingRequest() method. */
    @FunctionalInterface
    public interface SingleRepeatingRequest {
        /** Run the setSingleRepeatingRequest() method. */
        int run(@NonNull CaptureRequest request,
                @NonNull CameraCaptureSession.CaptureCallback listener)
                throws CameraAccessException;
    }

    /** Interface to forward call of the openCaptureSession() method. */
    @FunctionalInterface
    public interface OpenCaptureSession {
        /** Run the openCaptureSession() method. */
        @NonNull
        ListenableFuture<Void> run(@NonNull CameraDevice cameraDevice,
                @NonNull SessionConfigurationCompat sessionConfigurationCompat,
                @NonNull List<DeferrableSurface> deferrableSurfaces);
    }
}