QueuedImageReaderProxy.java

/*
 * Copyright (C) 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 android.os.Handler;
import android.view.Surface;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * An {@link ImageReaderProxy} which maintains a queue of recently available images.
 *
 * <p>Like a conventional {@link android.media.ImageReader}, when the queue becomes full and the
 * user does not close older images quickly enough, newly available images will not be added to the
 * queue and become lost. The user is responsible for setting a listener for newly available images
 * and closing the acquired images quickly enough.
 */
final class QueuedImageReaderProxy
        implements ImageReaderProxy, ForwardingImageProxy.OnImageCloseListener {
    private final int mWidth;
    private final int mHeight;
    private final int mFormat;
    private final int mMaxImages;

    @GuardedBy("this")
    private final Surface mSurface;

    // mMaxImages is not expected to be large, because images consume a lot of memory and there
    // cannot
    // co-exist too many images simultaneously. So, just use a List to simplify the implementation.
    @GuardedBy("this")
    private final List<ImageProxy> mImages;

    @GuardedBy("this")
    private final Set<ImageProxy> mAcquiredImages = new HashSet<>();
    @GuardedBy("this")
    private final Set<OnReaderCloseListener> mOnReaderCloseListeners = new HashSet<>();
    // Current access position in the queue.
    @GuardedBy("this")
    private int mCurrentPosition;
    @GuardedBy("this")
    @Nullable
    private ImageReaderProxy.OnImageAvailableListener mOnImageAvailableListener;
    @GuardedBy("this")
    @Nullable
    private Executor mOnImageAvailableExecutor;
    @GuardedBy("this")
    private boolean mClosed;

    /**
     * Creates a new instance of a queued image reader proxy.
     *
     * @param width     of the images
     * @param height    of the images
     * @param format    of the images
     * @param maxImages capacity of the queue
     * @param surface   to which the reader is attached
     * @return new {@link QueuedImageReaderProxy} instance
     */
    QueuedImageReaderProxy(int width, int height, int format, int maxImages, Surface surface) {
        mWidth = width;
        mHeight = height;
        mFormat = format;
        mMaxImages = maxImages;
        mSurface = surface;
        mImages = new ArrayList<>(maxImages);
        mCurrentPosition = 0;
        mClosed = false;
    }

    @Override
    @Nullable
    public synchronized ImageProxy acquireLatestImage() {
        throwExceptionIfClosed();
        if (mImages.isEmpty()) {
            return null;
        }
        if (mCurrentPosition >= mImages.size()) {
            throw new IllegalStateException("Max images have already been acquired without close.");
        }

        // Close all images up to the tail of the list, except for already acquired images.
        List<ImageProxy> imagesToClose = new ArrayList<>();
        for (int i = 0; i < mImages.size() - 1; ++i) {
            if (!mAcquiredImages.contains(mImages.get(i))) {
                imagesToClose.add(mImages.get(i));
            }
        }
        for (ImageProxy image : imagesToClose) {
            // Calling image.close() will cause this.onImageClosed(image) to be called.
            image.close();
        }

        // Move the current position to the tail of the list.
        mCurrentPosition = mImages.size() - 1;
        ImageProxy acquiredImage = mImages.get(mCurrentPosition++);
        mAcquiredImages.add(acquiredImage);
        return acquiredImage;
    }

    @Override
    @Nullable
    public synchronized ImageProxy acquireNextImage() {
        throwExceptionIfClosed();
        if (mImages.isEmpty()) {
            return null;
        }
        if (mCurrentPosition >= mImages.size()) {
            throw new IllegalStateException("Max images have already been acquired without close.");
        }
        ImageProxy acquiredImage = mImages.get(mCurrentPosition++);
        mAcquiredImages.add(acquiredImage);
        return acquiredImage;
    }

    /**
     * Adds an image to the tail of the queue.
     *
     * <p>If the queue already contains the max number of images, the given image is not added to
     * the queue and is closed. This is consistent with the documented behavior of an {@link
     * android.media.ImageReader}, where new images may be lost if older images are not closed
     * quickly enough.
     *
     * <p>If the image is added to the queue and an on-image-available listener has been previously
     * set, the listener is notified that the new image is available.
     *
     * @param image to add
     */
    synchronized void enqueueImage(ForwardingImageProxy image) {
        throwExceptionIfClosed();
        if (mImages.size() < mMaxImages) {
            mImages.add(image);
            image.addOnImageCloseListener(this);
            if (mOnImageAvailableListener != null && mOnImageAvailableExecutor != null) {
                final OnImageAvailableListener listener = mOnImageAvailableListener;
                mOnImageAvailableExecutor.execute(
                        new Runnable() {
                            @Override
                            public void run() {
                                if (!QueuedImageReaderProxy.this.isClosed()) {
                                    listener.onImageAvailable(QueuedImageReaderProxy.this);
                                }
                            }
                        });
            }
        } else {
            image.close();
        }
    }

    @Override
    public synchronized void close() {
        if (!mClosed) {
            this.mOnImageAvailableExecutor = null;
            this.mOnImageAvailableListener = null;
            // We need to copy into a different list, because closing an image triggers the on-close
            // listener which in turn modifies the original list.
            List<ImageProxy> imagesToClose = new ArrayList<>(mImages);
            for (ImageProxy image : imagesToClose) {
                image.close();
            }
            mImages.clear();
            mClosed = true;
            notifyOnReaderCloseListeners();
        }
    }

    @Override
    public int getHeight() {
        throwExceptionIfClosed();
        return mHeight;
    }

    @Override
    public int getWidth() {
        throwExceptionIfClosed();
        return mWidth;
    }

    @Override
    public int getImageFormat() {
        throwExceptionIfClosed();
        return mFormat;
    }

    @Override
    public int getMaxImages() {
        throwExceptionIfClosed();
        return mMaxImages;
    }

    @Override
    public synchronized Surface getSurface() {
        throwExceptionIfClosed();
        return mSurface;
    }

    @Override
    public synchronized void setOnImageAvailableListener(
            @NonNull OnImageAvailableListener onImageAvailableListener,
            @Nullable Handler onImageAvailableHandler) {
        setOnImageAvailableListener(onImageAvailableListener,
                onImageAvailableHandler == null ? null :
                        CameraXExecutors.newHandlerExecutor(onImageAvailableHandler));
    }

    @Override
    public synchronized void setOnImageAvailableListener(
            @NonNull OnImageAvailableListener onImageAvailableListener,
            @NonNull Executor executor) {
        throwExceptionIfClosed();
        mOnImageAvailableListener = onImageAvailableListener;
        mOnImageAvailableExecutor = executor;
    }

    @Override
    public synchronized void onImageClose(ImageProxy image) {
        int index = mImages.indexOf(image);
        if (index >= 0) {
            mImages.remove(index);
            if (index <= mCurrentPosition) {
                mCurrentPosition--;
            }
        }
        mAcquiredImages.remove(image);
    }

    /** Returns the current number of images in the queue. */
    synchronized int getCurrentImages() {
        throwExceptionIfClosed();
        return mImages.size();
    }

    /** Returns true if the reader is already closed. */
    synchronized boolean isClosed() {
        return mClosed;
    }

    /**
     * Adds a listener for close calls on this reader.
     *
     * @param listener to add
     */
    synchronized void addOnReaderCloseListener(OnReaderCloseListener listener) {
        mOnReaderCloseListeners.add(listener);
    }

    private synchronized void throwExceptionIfClosed() {
        if (mClosed) {
            throw new IllegalStateException("This reader is already closed.");
        }
    }

    private synchronized void notifyOnReaderCloseListeners() {
        for (OnReaderCloseListener listener : mOnReaderCloseListeners) {
            listener.onReaderClose(this);
        }
    }

    /** Listener for the reader close event. */
    interface OnReaderCloseListener {
        /**
         * Callback for reader close.
         *
         * @param imageReader which is closed
         */
        void onReaderClose(ImageReaderProxy imageReader);
    }
}