CheckedSurfaceTexture.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.graphics.SurfaceTexture;
import android.os.Looper;
import android.util.Size;
import android.view.Surface;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.concurrent.futures.CallbackToFutureAdapter;

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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;

/**
 * A {@link DeferrableSurface} which verifies the {@link SurfaceTexture} that backs the {@link
 * Surface} is unreleased before returning the Surface.
 */
final class CheckedSurfaceTexture extends DeferrableSurface {
    private final OnTextureChangedListener mOutputChangedListener;
    final List<Surface> mSurfaceToReleaseList = new ArrayList<>();
    @Nullable
    FixedSizeSurfaceTexture mSurfaceTexture;
    @Nullable
    Surface mSurface;
    @Nullable
    private Size mResolution;

    Object mLock = new Object();

    @VisibleForTesting
    @GuardedBy("mLock")
    final Map<SurfaceTexture, Resource> mResourceMap = new HashMap<>();

    CheckedSurfaceTexture(
            OnTextureChangedListener outputChangedListener) {
        mOutputChangedListener = outputChangedListener;
    }

    private FixedSizeSurfaceTexture createDetachedSurfaceTexture(Size resolution) {
        Resource resource = new Resource();
        FixedSizeSurfaceTexture surfaceTexture = new FixedSizeSurfaceTexture(0,
                resolution, resource);
        surfaceTexture.detachFromGLContext();
        resource.setSurfaceTexture(surfaceTexture);

        synchronized (mLock) {
            mResourceMap.put(surfaceTexture, resource);
        }

        return surfaceTexture;
    }

    @UiThread
    void setResolution(Size resolution) {
        mResolution = resolution;
    }

    @UiThread
    void resetSurfaceTexture() {
        if (mResolution == null) {
            throw new IllegalStateException(
                    "setResolution() must be called before resetSurfaceTexture()");
        }

        release();

        mSurfaceTexture = createDetachedSurfaceTexture(mResolution);
        mOutputChangedListener.onTextureChanged(mSurfaceTexture, mResolution);
    }


    @UiThread
    boolean isSurfaceTextureReleasing(FixedSizeSurfaceTexture surfaceTexture) {
        synchronized (mLock) {
            Resource resource = mResourceMap.get(surfaceTexture);
            if (resource == null) {
                return true;
            }

            return resource.isReleasing();
        }
    }

    /**
     * Returns the {@link Surface} that is backed by a {@link SurfaceTexture}.
     *
     * <p>If the {@link SurfaceTexture} has already been released then the surface will be reset
     * using a new {@link SurfaceTexture}.
     */
    @Override
    public ListenableFuture<Surface> getSurface() {
        return CallbackToFutureAdapter.getFuture(
                new CallbackToFutureAdapter.Resolver<Surface>() {
                    @Override
                    public Object attachCompleter(
                            @NonNull final CallbackToFutureAdapter.Completer<Surface> completer) {
                        Runnable checkAndSetRunnable =
                                new Runnable() {
                                    @Override
                                    public void run() {
                                        if (isSurfaceTextureReleasing(mSurfaceTexture)) {
                                            // Reset the surface texture and notify the listener
                                            CheckedSurfaceTexture.this.resetSurfaceTexture();
                                        }

                                        if (mSurface == null) {
                                            mSurface = createSurfaceFrom(mSurfaceTexture);
                                        }
                                        completer.set(mSurface);
                                    }
                                };
                        runOnMainThread(checkAndSetRunnable);
                        return "CheckSurfaceTexture";
                    }
                });
    }

    @UiThread
    Surface createSurfaceFrom(FixedSizeSurfaceTexture surfaceTexture) {
        Surface surface = new Surface(surfaceTexture);

        synchronized (mLock) {
            Resource resource = mResourceMap.get(surfaceTexture);
            if (resource == null) {
                resource = new Resource();
                resource.setSurfaceTexture(surfaceTexture);
                mResourceMap.put(surfaceTexture, resource);
            }

            resource.setSurface(surface);
        }
        return surface;
    }

    @Override
    public void refresh() {
        runOnMainThread(new Runnable() {
            @Override
            public void run() {
                if (isSurfaceTextureReleasing(mSurfaceTexture)) {
                    // Reset the surface texture and notify the listener
                    CheckedSurfaceTexture.this.resetSurfaceTexture();
                }
                // To fix the incorrect preview orientation for devices running on legacy camera,
                // it needs to attach a new Surface instance to the newly created camera capture
                // session.
                if (mSurface != null) {
                    mSurfaceToReleaseList.add(mSurface);
                }
                mSurface = createSurfaceFrom(mSurfaceTexture);
            }
        });
    }

    @UiThread
    void release() {
        if (mSurface == null && mSurfaceTexture == null) {
            return;
        }

        Resource resource;
        synchronized (mLock) {
            resource = mResourceMap.get(mSurfaceTexture);
        }

        if (resource != null) {
            releaseResourceWhenDetached(resource);
        }
        mSurfaceTexture = null;
        mSurface = null;

        for (Surface surface : mSurfaceToReleaseList) {
            surface.release();
        }
        mSurfaceToReleaseList.clear();
    }

    void releaseResourceWhenDetached(final Resource resource) {
        synchronized (mLock) {
            resource.setReleasing(true);
        }

        setOnSurfaceDetachedListener(CameraXExecutors.mainThreadExecutor(),
                new OnSurfaceDetachedListener() {
                    @Override
                    public void onSurfaceDetached() {
                        List<Resource> resourcesToRelease = new ArrayList<>();

                        synchronized (mLock) {
                            for (Resource resource : mResourceMap.values()) {
                                if (resource.isReleasing()) {
                                    resourcesToRelease.add(resource);
                                }
                            }

                            // Removes the resource from the map since it is of no use.
                            for (Resource resourceToRemove : resourcesToRelease) {
                                mResourceMap.remove(resourceToRemove.mSurfaceTexture);
                            }
                        }

                        for (Resource resource : resourcesToRelease) {
                            resource.release();
                        }
                    }
                });
    }

    void runOnMainThread(Runnable runnable) {
        Executor executor =
                (Looper.myLooper() == Looper.getMainLooper())
                        ? CameraXExecutors.directExecutor()
                        : CameraXExecutors.mainThreadExecutor();
        executor.execute(runnable);
    }

    interface OnTextureChangedListener {
        void onTextureChanged(SurfaceTexture newOutput, Size newResolution);
    }

    /**
     * Contains a pair of SurfaceTexture and Surface and also implements
     * FixedSizeSurfaceTexture.Owner interface to control the release timing of
     * FixedSizeSurfaceTexture.
     */
    class Resource implements FixedSizeSurfaceTexture.Owner {
        FixedSizeSurfaceTexture mSurfaceTexture;
        Surface mSurface;
        boolean mIsReleasing = false;
        boolean mIsReadyToRelease = false;

        @UiThread
        public void setSurfaceTexture(FixedSizeSurfaceTexture surfaceTexture) {
            mSurfaceTexture = surfaceTexture;
        }

        @UiThread
        public void setSurface(Surface surface) {
            mSurface = surface;
        }

        public synchronized boolean isReleasing() {
            return mIsReleasing;
        }

        public synchronized void setReleasing(boolean releasing) {
            mIsReleasing = releasing;
        }

        @Override
        public synchronized boolean requestRelease() {
            if (mIsReadyToRelease) {
                return true;
            }

            releaseResourceWhenDetached(this);
            return false;
        }

        @UiThread
        public synchronized void release() {
            mIsReadyToRelease = true;

            if (mSurfaceTexture != null) {
                mSurfaceTexture.release();
                mSurfaceTexture = null;
            }

            if (mSurface != null) {
                mSurface.release();
                mSurface = null;
            }
        }
    }
}