ImageAnalysisNonBlockingAnalyzer.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.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.impl.ImageReaderProxy;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;

import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;

/**
 * OnImageAvailableListener with non-blocking behavior. Analyzes images in a non-blocking way by
 * dropping images when analyzer is busy.
 *
 * <p> Used with {@link ImageAnalysis}.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
final class ImageAnalysisNonBlockingAnalyzer extends ImageAnalysisAbstractAnalyzer {

    // The executor for managing cached image.
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Executor mBackgroundExecutor;

    private final Object mLock = new Object();

    // The cached image when analyzer is busy. Image removed from cache must be closed by 1) closing
    // it directly or 2) re-posting it to close it eventually.
    @GuardedBy("mLock")
    @Nullable
    @VisibleForTesting
    ImageProxy mCachedImage;

    // The latest unclosed image sent to the app.
    @GuardedBy("mLock")
    @Nullable
    private CacheAnalyzingImageProxy mPostedImage;

    ImageAnalysisNonBlockingAnalyzer(Executor executor) {
        mBackgroundExecutor = executor;
    }

    @Nullable
    @Override
    ImageProxy acquireImage(@NonNull ImageReaderProxy imageReaderProxy) {
        // Use acquireLatestImage() so older images should be released.
        return imageReaderProxy.acquireLatestImage();
    }

    /**
     * This method guarantees closing the image by either 1) closing the image in the current
     * thread, 2) caching it for later or 3) posting it to user Thread to close it.
     *
     * @param imageProxy the incoming image frame.
     */
    @Override
    void onValidImageAvailable(@NonNull ImageProxy imageProxy) {
        synchronized (mLock) {
            if (!mIsAttached) {
                imageProxy.close();
                return;
            }
            if (mPostedImage != null) {
                // There is unclosed image held by the app. The incoming image has to wait.

                if (imageProxy.getImageInfo().getTimestamp()
                        <= mPostedImage.getImageInfo().getTimestamp()) {
                    // Discard the incoming image that is in the wrong order. Cached image can be
                    // in this state.
                    imageProxy.close();
                } else {
                    // Otherwise cache the incoming image and repost it later.
                    if (mCachedImage != null) {
                        mCachedImage.close();
                    }
                    mCachedImage = imageProxy;
                }
                return;
            }

            // Post the incoming image to app.
            final CacheAnalyzingImageProxy newPostedImage = new CacheAnalyzingImageProxy(imageProxy,
                    this);
            mPostedImage = newPostedImage;
            Futures.addCallback(analyzeImage(newPostedImage), new FutureCallback<Void>() {
                @Override
                public void onSuccess(Void result) {
                    // No-op. If the post is successful, app should close it.
                }

                @Override
                public void onFailure(Throwable t) {
                    // Close the image if we didn't post it to user.
                    newPostedImage.close();
                }
            }, CameraXExecutors.directExecutor());
        }
    }

    @Override
    void clearCache() {
        synchronized (mLock) {
            if (mCachedImage != null) {
                mCachedImage.close();
                mCachedImage = null;
            }
        }
    }

    /**
     * Removes cached image from cache and analyze it.
     */
    void analyzeCachedImage() {
        synchronized (mLock) {
            mPostedImage = null;
            if (mCachedImage != null) {
                ImageProxy cachedImage = mCachedImage;
                mCachedImage = null;
                onValidImageAvailable(cachedImage);
            }
        }
    }

    /**
     * An {@link ImageProxy} that analyze cached image on close.
     */
    static class CacheAnalyzingImageProxy extends ForwardingImageProxy {

        // WeakReference so that if the app holds onto the ImageProxy instance the analyzer can
        // still be GCed.
        final WeakReference<ImageAnalysisNonBlockingAnalyzer> mNonBlockingAnalyzerWeakReference;

        /**
         * Creates a new instance which wraps the given image.
         *
         * @param image  image proxy to wrap.
         * @param nonBlockingAnalyzer instance of the nonblocking analyzer.
         */
        CacheAnalyzingImageProxy(@NonNull ImageProxy image,
                @NonNull ImageAnalysisNonBlockingAnalyzer nonBlockingAnalyzer) {
            super(image);
            mNonBlockingAnalyzerWeakReference = new WeakReference<>(nonBlockingAnalyzer);

            addOnImageCloseListener((imageProxy) -> {
                ImageAnalysisNonBlockingAnalyzer analyzer = mNonBlockingAnalyzerWeakReference.get();
                if (analyzer != null) {
                    analyzer.mBackgroundExecutor.execute(
                            () -> analyzer.analyzeCachedImage());
                }
            });
        }
    }
}