TextureViewImplementation.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.view;

import static androidx.camera.view.ScaleTypeTransform.getFillScaleWithBufferAspectRatio;
import static androidx.camera.view.ScaleTypeTransform.getOriginOfCenteredView;
import static androidx.camera.view.ScaleTypeTransform.getRotationDegrees;

import android.graphics.Point;
import android.graphics.SurfaceTexture;
import android.util.Log;
import android.util.Pair;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.Preview;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.core.content.ContextCompat;

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

/**
 * The {@link TextureView} implementation for {@link PreviewView}
 */
public class TextureViewImplementation implements PreviewView.Implementation {

    private static final String TAG = "TextureViewImpl";

    private FrameLayout mParent;
    TextureView mTextureView;
    SurfaceTexture mSurfaceTexture;
    private Size mResolution;
    ListenableFuture<Void> mSurfaceReleaseFuture;
    SurfaceRequest mSurfaceRequest;

    @Override
    public void init(@NonNull FrameLayout parent) {
        mParent = parent;
    }

    @NonNull
    @Override
    public Preview.SurfaceProvider getSurfaceProvider() {
        return (surfaceRequest) -> {
            mResolution = surfaceRequest.getResolution();
            initInternal();
            if (mSurfaceRequest != null) {
                mSurfaceRequest.setWillNotComplete();
            }

            mSurfaceRequest = surfaceRequest;
            surfaceRequest.addRequestCancellationListener(
                    ContextCompat.getMainExecutor(mTextureView.getContext()), () -> {
                        if (mSurfaceRequest != null && mSurfaceRequest == surfaceRequest) {
                            mSurfaceRequest = null;
                            mSurfaceReleaseFuture = null;
                        }
                    });

            tryToProvidePreviewSurface();
        };
    }

    @Override
    public void onDisplayChanged() {
        if (mParent == null || mTextureView == null || mResolution == null) {
            return;
        }

        correctPreviewForCenterCrop(mParent, mTextureView, mResolution);
    }

    private void initInternal() {
        mTextureView = new TextureView(mParent.getContext());
        mTextureView.setLayoutParams(
                new FrameLayout.LayoutParams(mResolution.getWidth(), mResolution.getHeight()));
        mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(final SurfaceTexture surfaceTexture,
                    final int width, final int height) {
                mSurfaceTexture = surfaceTexture;
                tryToProvidePreviewSurface();
            }

            @Override
            public void onSurfaceTextureSizeChanged(final SurfaceTexture surfaceTexture,
                    final int width, final int height) {
                Log.d(TAG, "onSurfaceTextureSizeChanged(width:" + width + ", height: " + height
                        + " )");
            }

            /**
             * If a surface has been provided to the camera (meaning
             * {@link TextureViewImplementation#mSurfaceRequest} is null), but the camera
             * is still using it (meaning {@link TextureViewImplementation#mSurfaceReleaseFuture} is
             * not null), a listener must be added to
             * {@link TextureViewImplementation#mSurfaceReleaseFuture} to ensure the surface
             * is properly released after the camera is done using it.
             *
             * @param surfaceTexture The {@link SurfaceTexture} about to be destroyed.
             * @return false if the camera is not done with the surface, true otherwise.
             */
            @Override
            public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) {
                mSurfaceTexture = null;
                if (mSurfaceRequest == null && mSurfaceReleaseFuture != null) {
                    Futures.addCallback(mSurfaceReleaseFuture, new FutureCallback<Void>() {
                        @Override
                        public void onSuccess(@Nullable Void result) {
                            surfaceTexture.release();
                        }

                        @Override
                        public void onFailure(Throwable t) {
                            if (t instanceof SurfaceRequest.RequestCancelledException) {
                                surfaceTexture.release();
                            } else {
                                throw new IllegalStateException("SurfaceReleaseFuture did not "
                                        + "complete nicely.", t);
                            }
                        }
                    }, ContextCompat.getMainExecutor(mTextureView.getContext()));
                    return false;
                } else {
                    return true;
                }
            }

            @Override
            public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) {
            }
        });

        // Even though PreviewView calls `removeAllViews()` before calling init(), it should be
        // called again here in case `getPreviewSurfaceProvider()` is called more than once on
        // the same TextureViewImplementation instance.
        mParent.removeAllViews();
        mParent.addView(mTextureView);
    }

    @SuppressWarnings("WeakerAccess")
    void tryToProvidePreviewSurface() {
        /*
          Should only continue if:
          - The preview size has been specified.
          - The textureView's surfaceTexture is available (after TextureView
          .SurfaceTextureListener#onSurfaceTextureAvailable is invoked)
          - The surfaceCompleter has been set (after CallbackToFutureAdapter
          .Resolver#attachCompleter is invoked).
         */
        if (mResolution == null || mSurfaceTexture == null || mSurfaceRequest == null) {
            return;
        }

        mSurfaceTexture.setDefaultBufferSize(mResolution.getWidth(), mResolution.getHeight());

        final Surface surface = new Surface(mSurfaceTexture);
        final ListenableFuture<Void> surfaceReleaseFuture = mSurfaceRequest.setSurface(surface);
        mSurfaceReleaseFuture = surfaceReleaseFuture;
        mSurfaceReleaseFuture.addListener(() -> {
            surface.release();
            if (mSurfaceReleaseFuture == surfaceReleaseFuture) {
                mSurfaceReleaseFuture = null;
            }
        }, ContextCompat.getMainExecutor(mTextureView.getContext()));

        mSurfaceRequest = null;

        correctPreviewForCenterCrop(mParent, mTextureView, mResolution);
    }

    /**
     * Corrects the preview to match the UI orientation and completely fill the PreviewView.
     *
     * <p>
     * The camera produces a preview that depends on its sensor orientation and that has a
     * specific resolution. In order to display it correctly, this preview must be rotated to
     * match the UI orientation, and must be scaled up/down to fit inside the view that's
     * displaying it. This method takes care of doing so while keeping the preview centered.
     * </p>
     *
     * @param container   The {@link PreviewView}'s root layout, which wraps the preview.
     * @param textureView The {@link android.view.TextureView} that displays the preview, its size
     *                    must match the camera sensor output size.
     * @param bufferSize  The camera sensor output size.
     */
    private void correctPreviewForCenterCrop(@NonNull final View container,
            @NonNull final TextureView textureView, @NonNull final Size bufferSize) {
        // Scale TextureView to fill PreviewView while respecting sensor output size aspect ratio
        final Pair<Float, Float> scale = getFillScaleWithBufferAspectRatio(container, textureView,
                bufferSize);
        textureView.setScaleX(scale.first);
        textureView.setScaleY(scale.second);

        // Center TextureView inside PreviewView
        final Point newOrigin = getOriginOfCenteredView(container, textureView);
        textureView.setX(newOrigin.x);
        textureView.setY(newOrigin.y);

        // Rotate TextureView to correct preview orientation
        final int rotation = getRotationDegrees(textureView);
        textureView.setRotation(-rotation);
    }
}