ImageAnalysisAbstractAnalyzer.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.camera.core;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.os.OperationCanceledException;

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

import java.util.concurrent.Executor;

/**
 * Abstract Analyzer that wraps around {@link ImageAnalysis.Analyzer} and implements
 * {@link ImageReaderProxy.OnImageAvailableListener}.
 *
 * This is an extension of {@link ImageAnalysis}. It has the same lifecycle and share part of the
 * states.
 */
abstract class ImageAnalysisAbstractAnalyzer implements ImageReaderProxy.OnImageAvailableListener {

    private static final String TAG = "ImageAnalysisAnalyzer";

    // Member variables from ImageAnalysis.
    @GuardedBy("mAnalyzerLock")
    private ImageAnalysis.Analyzer mSubscribedAnalyzer;
    private volatile int mRelativeRotation;
    @GuardedBy("mAnalyzerLock")
    private Executor mUserExecutor;

    // Lock that synchronizes the access to mSubscribedAnalyzer/mUserExecutor to prevent mismatch.
    private final Object mAnalyzerLock = new Object();

    // Flag that reflects the attaching state of the holding ImageAnalysis object.
    protected boolean mIsAttached = true;

    @Override
    public void onImageAvailable(@NonNull ImageReaderProxy imageReaderProxy) {
        try {
            ImageProxy imageProxy = acquireImage(imageReaderProxy);
            if (imageProxy != null) {
                onValidImageAvailable(imageProxy);
            }
        } catch (IllegalStateException e) {
            // This happens if image is not closed in STRATEGY_BLOCK_PRODUCER mode. Catch the
            // exception and fail with an error log.
            // TODO(b/175851631): it may also happen when STRATEGY_KEEP_ONLY_LATEST not closing
            //  the cached image properly. We are unclear why it happens but catching the
            //  exception should improve the situation by not crashing.
            Logger.e(TAG, "Failed to acquire image.", e);
        }
    }

    /**
     * Implemented by children to acquireImage via {@link ImageReaderProxy#acquireLatestImage()} or
     * {@link ImageReaderProxy#acquireNextImage()}.
     */
    @Nullable
    abstract ImageProxy acquireImage(@NonNull ImageReaderProxy imageReaderProxy);

    /**
     * Called when a new valid {@link ImageProxy} becomes available via
     * {@link ImageReaderProxy.OnImageAvailableListener}.
     */
    abstract void onValidImageAvailable(@NonNull ImageProxy imageProxy);

    /**
     * Called by {@link ImageAnalysis} to release cached images.
     */
    abstract void clearCache();

    /**
     * Analyzes a {@link ImageProxy} using the wrapped {@link ImageAnalysis.Analyzer}.
     *
     * <p> The analysis will run on the executor provided by {@link #setAnalyzer(Executor,
     * ImageAnalysis.Analyzer)}. Once the analysis successfully finishes the returned
     * ListenableFuture will succeed. If the future fails then it means the {@link
     * androidx.camera.core.ImageAnalysis.Analyzer} was not called so the image needs to be closed.
     *
     * @return The future which will complete once analysis has finished or it failed.
     */
    ListenableFuture<Void> analyzeImage(ImageProxy imageProxy) {
        Executor executor;
        ImageAnalysis.Analyzer analyzer;
        synchronized (mAnalyzerLock) {
            executor = mUserExecutor;
            analyzer = mSubscribedAnalyzer;
        }

        ListenableFuture<Void> future;

        if (analyzer != null && executor != null) {
            // When the analyzer exists and ImageAnalysis is active.
            future = CallbackToFutureAdapter.getFuture(
                    completer -> {
                        executor.execute(() -> {
                            if (mIsAttached) {
                                ImageInfo imageInfo = ImmutableImageInfo.create(
                                        imageProxy.getImageInfo().getTagBundle(),
                                        imageProxy.getImageInfo().getTimestamp(),
                                        mRelativeRotation);

                                analyzer.analyze(new SettableImageProxy(imageProxy, imageInfo));
                                completer.set(null);
                            } else {
                                completer.setException(new OperationCanceledException(
                                        "ImageAnalysis is detached"));
                            }
                        });
                        return "analyzeImage";
                    });
        } else {
            future = Futures.immediateFailedFuture(new OperationCanceledException(
                    "No analyzer or executor currently set."));
        }

        return future;
    }

    void setRelativeRotation(int relativeRotation) {
        mRelativeRotation = relativeRotation;
    }

    void setAnalyzer(@Nullable Executor userExecutor,
            @Nullable ImageAnalysis.Analyzer subscribedAnalyzer) {
        synchronized (mAnalyzerLock) {
            if (subscribedAnalyzer == null) {
                clearCache();
            }
            mSubscribedAnalyzer = subscribedAnalyzer;
            mUserExecutor = userExecutor;
        }
    }

    /**
     * Initialize the callback.
     */
    void attach() {
        mIsAttached = true;
    }

    /**
     * Closes the callback so that it will stop posting to analyzer.
     */
    void detach() {
        mIsAttached = false;
        clearCache();
    }
}