RequestWithCallback.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.core.imagecapture;

import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.core.util.Preconditions.checkState;

import android.os.Build;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.concurrent.futures.CallbackToFutureAdapter;

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

/**
 * A wrapper of a {@link TakePictureRequest} and its {@link TakePictureCallback}.
 *
 * <p>This is the connection between the internal callback and the app callback. This
 * connection allows us to manipulate the propagation of the callback. For example, failures
 * might be retried before sent to the app.
 */
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
class RequestWithCallback implements TakePictureCallback {

    private final TakePictureRequest mTakePictureRequest;
    private final ListenableFuture<Void> mCaptureFuture;
    private CallbackToFutureAdapter.Completer<Void> mCaptureCompleter;
    // Tombstone flag that indicates that this callback should not be invoked anymore.
    private boolean mIsComplete = false;
    // Flag tracks if the request has been aborted by the UseCase. Once aborted, this class stops
    // propagating callbacks to the app.
    private boolean mIsAborted = false;

    RequestWithCallback(@NonNull TakePictureRequest takePictureRequest) {
        mTakePictureRequest = takePictureRequest;
        mCaptureFuture = CallbackToFutureAdapter.getFuture(
                completer -> {
                    mCaptureCompleter = completer;
                    return "CaptureCompleteFuture";
                });
    }

    @MainThread
    @Override
    public void onImageCaptured() {
        checkMainThread();
        if (mIsAborted) {
            // Ignore. mCaptureFuture should have been completed by the #abort() call.
            return;
        }
        mCaptureCompleter.set(null);
        // TODO: send early callback to app.
    }

    @MainThread
    @Override
    public void onFinalResult(@NonNull ImageCapture.OutputFileResults outputFileResults) {
        checkMainThread();
        if (mIsAborted) {
            // Do not deliver result if the request has been aborted.
            // TODO: delete the saved file when the request is aborted.
            return;
        }
        checkOnImageCaptured();
        markComplete();
        mTakePictureRequest.onResult(outputFileResults);
    }

    @MainThread
    @Override
    public void onFinalResult(@NonNull ImageProxy imageProxy) {
        checkMainThread();
        if (mIsAborted) {
            // Do not deliver result if the request has been aborted.
            return;
        }
        checkOnImageCaptured();
        markComplete();
        mTakePictureRequest.onResult(imageProxy);
    }


    @MainThread
    @Override
    public void onProcessFailure(@NonNull ImageCaptureException imageCaptureException) {
        checkMainThread();
        if (mIsAborted) {
            // Fail silently if the request has been aborted.
            return;
        }
        checkOnImageCaptured();
        markComplete();
        onFailure(imageCaptureException);
    }

    @Override
    public boolean isAborted() {
        return mIsAborted;
    }

    @MainThread
    @Override
    public void onCaptureFailure(@NonNull ImageCaptureException imageCaptureException) {
        checkMainThread();
        if (mIsAborted) {
            // Fail silently if the request has been aborted.
            return;
        }
        markComplete();
        mCaptureCompleter.set(null);

        // TODO(b/242683221): Add retry logic.
        onFailure(imageCaptureException);
    }

    @MainThread
    void abort(@NonNull ImageCaptureException imageCaptureException) {
        checkMainThread();
        mIsAborted = true;
        mCaptureCompleter.set(null);
        onFailure(imageCaptureException);
    }

    /**
     * Gets a {@link ListenableFuture} that finishes when the capture is completed by camera.
     *
     * <p>Send the next request after this one completes.
     */
    @MainThread
    @NonNull
    ListenableFuture<Void> getCaptureFuture() {
        checkMainThread();
        return mCaptureFuture;
    }

    private void checkOnImageCaptured() {
        checkState(mCaptureFuture.isDone(),
                "onImageCaptured() must be called before onFinalResult()");
    }

    private void markComplete() {
        checkState(!mIsComplete, "The callback can only complete once.");
        mIsComplete = true;
    }

    @MainThread
    private void onFailure(@NonNull ImageCaptureException imageCaptureException) {
        checkMainThread();
        mTakePictureRequest.onError(imageCaptureException);
    }
}