ForceCloseCaptureSession.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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.internal.SynchronizedCaptureSession;
import androidx.camera.camera2.internal.compat.quirk.CaptureSessionOnClosedNotCalledQuirk;
import androidx.camera.core.impl.Quirks;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * The workaround is used to set the {@link androidx.camera.camera2.internal.CaptureSession} to
 * closed state since the
 * {@link android.hardware.camera2.CameraCaptureSession.StateCallback#onClosed} may not be called
 * after the {@link android.hardware.camera2.CameraCaptureSession} is closed.
 *
 * @see CaptureSessionOnClosedNotCalledQuirk
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ForceCloseCaptureSession {

    @Nullable
    private final CaptureSessionOnClosedNotCalledQuirk mCaptureSessionOnClosedNotCalledQuirk;

    /** Constructor of the ForceCloseCaptureSession workaround */
    public ForceCloseCaptureSession(@NonNull Quirks deviceQuirks) {
        mCaptureSessionOnClosedNotCalledQuirk =
                deviceQuirks.get(CaptureSessionOnClosedNotCalledQuirk.class);
    }

    /** Return true if the obsolete non-closed capture sessions should be forced closed. */
    public boolean shouldForceClose() {
        return mCaptureSessionOnClosedNotCalledQuirk != null;
    }

    /**
     * For b/144817309, the onClosed() callback on
     * {@link android.hardware.camera2.CameraCaptureSession.StateCallback}
     * might not be invoked if the capture session is not the latest one. To align the fixed
     * framework behavior, we manually call the onClosed() when a new CameraCaptureSession is
     * created.
     */
    public void onSessionConfigured(@NonNull SynchronizedCaptureSession session,
            @NonNull List<SynchronizedCaptureSession> creatingSessions,
            @NonNull List<SynchronizedCaptureSession> sessions,
            @NonNull OnConfigured onConfigured) {
        if (shouldForceClose()) {
            Set<SynchronizedCaptureSession> staleCreatingSessions = new LinkedHashSet<>();
            for (SynchronizedCaptureSession s : creatingSessions) {
                // Collect the sessions that started configuring before the current session. The
                // current session and the session that starts configure after the current session
                // are not included since they don't need to be closed.
                if (s == session) {
                    break;
                }
                staleCreatingSessions.add(s);
            }
            // Once the CaptureSession is configured, the stale CaptureSessions should not have
            // chance to complete the configuration flow. Force change to configure fail since
            // the configureFail will treat the CaptureSession is closed. More detail please see
            // b/158540776.
            forceOnConfigureFailed(staleCreatingSessions);
        }

        onConfigured.run(session);

        // Once the new CameraCaptureSession is created, all the previous opened
        // CameraCaptureSession can be treated as closed (more detail in b/144817309),
        // trigger its associated StateCallback#onClosed callback to finish the
        // session close flow.
        if (shouldForceClose()) {
            Set<SynchronizedCaptureSession> openedSessions = new LinkedHashSet<>();
            for (SynchronizedCaptureSession s : sessions) {

                // The entrySet keys of the LinkedHashMap should be insertion-ordered, so we
                // get the previous capture sessions by iterate it from the beginning.
                if (s == session) {
                    break;
                }
                openedSessions.add(s);
            }

            forceOnClosed(openedSessions);
        }
    }

    private void forceOnConfigureFailed(@NonNull Set<SynchronizedCaptureSession> sessions) {
        for (SynchronizedCaptureSession session : sessions) {
            session.getStateCallback().onConfigureFailed(session);
        }
    }

    private void forceOnClosed(@NonNull Set<SynchronizedCaptureSession> sessions) {
        for (SynchronizedCaptureSession session : sessions) {
            session.getStateCallback().onClosed(session);
        }
    }

    /** Interface to forward call of the onConfigured() method. */
    @FunctionalInterface
    public interface OnConfigured {
        /** Run the onConfigured() method. */
        void run(@NonNull SynchronizedCaptureSession session);
    }
}