Camera2CameraControl.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.camera2.interop;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.Camera2CameraControlImpl;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.core.CameraControl;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.TagBundle;
import androidx.camera.core.impl.annotation.ExecutedBy;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.core.util.Preconditions;

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

import java.util.concurrent.Executor;

/**
 * An class that provides ability to interoperate with the {@link android.hardware.camera2} APIs.
 *
 * <p>Camera2 specific controls, like capture request options, can be applied through this class.
 * A Camera2CameraControl can be created from a general {@link CameraControl} which is associated
 * to a camera. Then the controls will affect all use cases that are using that camera.
 *
 * <p>If any option applied by Camera2CameraControl conflicts with the options required by
 * CameraX internally. The options from Camera2CameraControl will override, which may result in
 * unexpected behavior depends on the options being applied.
 */
@ExperimentalCamera2Interop
public final class Camera2CameraControl {

    /** @hide */
    @RestrictTo(Scope.LIBRARY)
    public static final String TAG_KEY = "Camera2CameraControl";

    private boolean mIsActive = false;
    private boolean mPendingUpdate = false;
    private final Camera2CameraControlImpl mCamera2CameraControlImpl;
    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    @CameraExecutor
    final Executor mExecutor;
    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    final Object mLock = new Object();
    @GuardedBy("mLock")
    private Camera2ImplConfig.Builder mBuilder = new Camera2ImplConfig.Builder();
    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
    CallbackToFutureAdapter.Completer<Void> mCompleter;
    private final Camera2CameraControlImpl.CaptureResultListener mCaptureResultListener =
            (captureResult) -> {
                CallbackToFutureAdapter.Completer<Void> completerToSet = null;
                if (mCompleter != null) {
                    Object tag = captureResult.getRequest().getTag();
                    if (tag instanceof TagBundle) {
                        TagBundle tagBundle = (TagBundle) tag;
                        Integer tagInteger = (Integer) tagBundle.getTag(TAG_KEY);
                        if (tagInteger != null && tagInteger.equals(mCompleter.hashCode())) {
                            completerToSet = mCompleter;
                            mCompleter = null;
                        }
                    }
                }
                if (completerToSet != null) {
                    completerToSet.set(null);
                }
                // Return false to keep getting captureResult.
                return false;
            };

    /**
     * Creates a new camera control with Camera2 implementation.
     *
     * @param camera2CameraControlImpl the camera control this Camera2CameraControl belongs.
     * @param executor                 the camera executor used to run camera task.
     * @hide
     */
    @RestrictTo(Scope.LIBRARY)
    public Camera2CameraControl(@NonNull Camera2CameraControlImpl camera2CameraControlImpl,
            @NonNull @CameraExecutor Executor executor) {
        mCamera2CameraControlImpl = camera2CameraControlImpl;
        mExecutor = executor;
    }

    /** @hide */
    @RestrictTo(Scope.LIBRARY)
    @NonNull
    public Camera2CameraControlImpl.CaptureResultListener getCaptureRequestListener() {
        return mCaptureResultListener;
    }

    /**
     * Gets the {@link Camera2CameraControl} from a {@link CameraControl}.
     *
     * <p>The {@link CameraControl} is still usable after a {@link Camera2CameraControl} is
     * obtained from it. Note that the {@link Camera2CameraControl} has higher priority than the
     * {@link CameraControl}. For example, if
     * {@link android.hardware.camera2.CaptureRequest#FLASH_MODE} is set through the
     * {@link Camera2CameraControl}. All {@link CameraControl} features that required
     * {@link android.hardware.camera2.CaptureRequest#FLASH_MODE} internally like torch may not
     * work properly.
     *
     * @param cameraControl The {@link CameraControl} to get from.
     * @return The camera control with Camera2 implementation.
     * @throws IllegalArgumentException if the camera control does not contain the camera2
     *                                  information (e.g., if CameraX was not initialized with a
     *                                  {@link androidx.camera.camera2.Camera2Config}).
     */
    @NonNull
    public static Camera2CameraControl from(@NonNull CameraControl cameraControl) {
        Preconditions.checkArgument(cameraControl instanceof Camera2CameraControlImpl,
                "CameraControl doesn't contain Camera2 implementation.");
        return ((Camera2CameraControlImpl) cameraControl).getCamera2CameraControl();
    }

    /**
     * Sets a {@link CaptureRequestOptions} and updates the session with the options it
     * contains.
     *
     * <p>This will first clear all options that have already been set, then apply the new options.
     *
     * <p>Any values which are in conflict with values already set by CameraX, such as by
     * {@link androidx.camera.core.CameraControl}, will overwrite the existing values. The
     * values will be submitted with every repeating and single capture requests issued by
     * CameraX, which may result in unexpected behavior depending on the values being applied.
     *
     * @param bundle The {@link CaptureRequestOptions} which will be set.
     * @return a {@link ListenableFuture} which completes when the repeating
     * {@link android.hardware.camera2.CaptureResult} shows the options have be submitted
     * completely. The future fails with {@link CameraControl.OperationCanceledException} if newer
     * options are set or camera is closed before the current request completes.
     * Cancelling the ListenableFuture is a no-op.
     */
    @NonNull
    public ListenableFuture<Void> setCaptureRequestOptions(
            @NonNull CaptureRequestOptions bundle) {
        clearCaptureRequestOptionsInternal();
        addCaptureRequestOptionsInternal(bundle);

        return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(completer -> {
            mExecutor.execute(() -> {
                updateConfig(completer);
            });
            return "setCaptureRequestOptions";
        }));
    }

    /**
     * Adds a {@link CaptureRequestOptions} updates the session with the options it
     * contains.
     *
     * <p>The options will be merged with the existing options. If one option is set with a
     * different value, it will overwrite the existing value.
     *
     * <p>Any values which are in conflict with values already set by CameraX, such as by
     * {@link androidx.camera.core.CameraControl}, will overwrite the existing values. The
     * values will be submitted with every repeating and single capture requests issued by
     * CameraX, which may result in unexpected behavior depends on the values being applied.
     *
     * @param bundle The {@link CaptureRequestOptions} which will be set.
     * @return a {@link ListenableFuture} which completes when the repeating
     * {@link android.hardware.camera2.CaptureResult} shows the options have be submitted
     * completely. The future fails with {@link CameraControl.OperationCanceledException} if newer
     * options are set or camera is closed before the current request completes.
     */
    @NonNull
    public ListenableFuture<Void> addCaptureRequestOptions(
            @NonNull CaptureRequestOptions bundle) {
        addCaptureRequestOptionsInternal(bundle);

        return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(completer -> {
            mExecutor.execute(() -> {
                updateConfig(completer);
            });
            return "addCaptureRequestOptions";
        }));
    }

    /**
     * Gets all the capture request options that is currently applied by the
     * {@link Camera2CameraControl}.
     *
     * <p>It doesn't include the capture request options applied by
     * the {@link android.hardware.camera2.CameraDevice} templates or by CameraX.
     *
     * @return The {@link CaptureRequestOptions}.
     */
    @NonNull
    public CaptureRequestOptions getCaptureRequestOptions() {
        synchronized (mLock) {
            return CaptureRequestOptions.Builder.from(mBuilder.build()).build();
        }
    }

    /**
     * Clears all capture request options.
     *
     * @return a {@link ListenableFuture} which completes when the repeating
     * {@link android.hardware.camera2.CaptureResult} shows the options have be submitted
     * completely. The future fails with {@link CameraControl.OperationCanceledException} if newer
     * options are set or camera is closed before the current request completes.
     * @hide
     */
    @RestrictTo(Scope.LIBRARY)
    @NonNull
    public ListenableFuture<Void> clearCaptureRequestOptions() {
        clearCaptureRequestOptionsInternal();

        return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(completer -> {
            mExecutor.execute(() -> {
                updateConfig(completer);
            });
            return "clearCaptureRequestOptions";
        }));
    }

    /**
     * Gets the {@link Camera2ImplConfig} that is currently applied by the
     * {@link Camera2CameraControl}.
     *
     * @hide
     */
    @RestrictTo(Scope.LIBRARY)
    @NonNull
    public Camera2ImplConfig getCamera2ImplConfig() {
        synchronized (mLock) {
            if (mCompleter != null) {
                mBuilder.getMutableConfig().insertOption(
                        Camera2ImplConfig.CAPTURE_REQUEST_TAG_OPTION,
                        mCompleter.hashCode());
            }
            return mBuilder.build();
        }
    }

    private void addCaptureRequestOptionsInternal(@NonNull CaptureRequestOptions bundle) {
        synchronized (mLock) {
            for (Config.Option<?> option : bundle.listOptions()) {
                @SuppressWarnings("unchecked")
                Config.Option<Object> objectOpt = (Config.Option<Object>) option;
                mBuilder.getMutableConfig().insertOption(objectOpt,
                        bundle.retrieveOption(objectOpt));
            }
        }
    }

    private void clearCaptureRequestOptionsInternal() {
        synchronized (mLock) {
            mBuilder = new Camera2ImplConfig.Builder();
        }
    }

    @ExecutedBy("mExecutor")
    private void updateConfig(CallbackToFutureAdapter.Completer<Void> completer) {
        mPendingUpdate = true;
        // Complete the future if CaptureResult shows it's submitted successfully.
        CallbackToFutureAdapter.Completer<Void> completerToCancel = null;
        if (mCompleter != null) {
            completerToCancel = mCompleter;
        }
        mCompleter = completer;
        if (mIsActive) {
            updateSession();
        }
        if (completerToCancel != null) {
            completerToCancel.setException(new CameraControl.OperationCanceledException(
                    "Camera2CameraControl was updated with new options."));
        }
    }

    @ExecutedBy("mExecutor")
    private void updateSession() {
        mCamera2CameraControlImpl.updateSessionConfig();
        mPendingUpdate = false;
    }

    /**
     * Set current active state.
     *
     * <p>When the state changes from active to inactive, the Camera2 options will be cleared.
     * When the state changes from inactive to active, a session update will be issued if there's
     * Camera2 options set while inactive.
     *
     * @hide
     */
    @RestrictTo(Scope.LIBRARY)
    public void setActive(boolean isActive) {
        mExecutor.execute(() -> setActiveInternal(isActive));
    }

    @ExecutedBy("mExecutor")
    private void setActiveInternal(boolean isActive) {
        if (mIsActive == isActive) {
            return;
        }

        mIsActive = isActive;

        if (mIsActive) {
            if (mPendingUpdate) {
                updateSession();
            }
        } else {
            clearCaptureRequestOptionsInternal();
            if (mCompleter != null) {
                mCompleter.setException(new CameraControl.OperationCanceledException(
                        "The camera control has became inactive."));
                mCompleter = null;
            }
        }
    }
}