ExposureControl.java
/*
* Copyright 2020 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.internal;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.util.Range;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.core.CameraControl;
import androidx.camera.core.ExposureState;
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;
/**
* Implementation of Exposure compensation control.
*
* <p>It is intended to be used within {@link Camera2CameraControlImpl} to implement the
* functionality of {@link Camera2CameraControlImpl#setExposureCompensationIndex(int)}.
*
* <p>To wait for the exposure setting reach to the new requested target, it calls
* {@link Camera2CameraControlImpl#addCaptureResultListener(
* Camera2CameraControlImpl.CaptureResultListener)} to monitor the capture result.
*
* <p>The {@link Camera2CameraControlImpl#setExposureCompensationIndex(int)} can only allow to
* run one task at the same time, it will cancel the incomplete task if a new task is requested.
* The task will fails with {@link CameraControl.OperationCanceledException} if the camera is
* closed.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ExposureControl {
private static final int DEFAULT_EXPOSURE_COMPENSATION = 0;
@NonNull
private final Camera2CameraControlImpl mCameraControl;
@NonNull
private final ExposureStateImpl mExposureStateImpl;
@NonNull
@CameraExecutor
private final Executor mExecutor;
private boolean mIsActive = false;
@Nullable
private CallbackToFutureAdapter.Completer<Integer> mRunningCompleter;
@Nullable
private Camera2CameraControlImpl.CaptureResultListener mRunningCaptureResultListener;
/**
* Constructs a ExposureControl.
*
* <p>All tasks executed by {@code executor}.
*
* @param cameraControl Camera control.
* @param cameraCharacteristics The {@link CameraCharacteristics} of the camera.
* @param executor the camera executor used to run camera task.
*/
ExposureControl(@NonNull Camera2CameraControlImpl cameraControl,
@NonNull CameraCharacteristicsCompat cameraCharacteristics,
@CameraExecutor @NonNull Executor executor) {
mCameraControl = cameraControl;
mExposureStateImpl = new ExposureStateImpl(cameraCharacteristics,
DEFAULT_EXPOSURE_COMPENSATION);
mExecutor = executor;
}
static ExposureState getDefaultExposureState(
CameraCharacteristicsCompat cameraCharacteristics) {
return new ExposureStateImpl(cameraCharacteristics, DEFAULT_EXPOSURE_COMPENSATION);
}
/**
* Set current active state. Set active if it is ready to accept operations.
*
* <p>Set the active state to false will cancel the in fly
* {@link #setExposureCompensationIndex(int)} task with
* {@link CameraControl.OperationCanceledException}.
*/
@ExecutedBy("mExecutor")
void setActive(boolean isActive) {
if (isActive == mIsActive) {
return;
}
mIsActive = isActive;
if (!mIsActive) {
mExposureStateImpl.setExposureCompensationIndex(DEFAULT_EXPOSURE_COMPENSATION);
clearRunningTask();
}
}
/**
* Called by {@link Camera2CameraControlImpl} to append the CONTROL_AE_EXPOSURE_COMPENSATION
* option
* to the shared options. It applies to all repeating requests and single requests.
*/
@ExecutedBy("mExecutor")
void setCaptureRequestOption(@NonNull Camera2ImplConfig.Builder configBuilder) {
configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION,
mExposureStateImpl.getExposureCompensationIndex());
}
@NonNull
ExposureState getExposureState() {
return mExposureStateImpl;
}
@NonNull
ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
if (!mExposureStateImpl.isExposureCompensationSupported()) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"ExposureCompensation is not supported"));
}
Range<Integer> range = mExposureStateImpl.getExposureCompensationRange();
if (!range.contains(exposure)) {
return Futures.immediateFailedFuture(new IllegalArgumentException(
"Requested ExposureCompensation " + exposure + " is not within"
+ " valid range [" + range.getUpper() + ".." + range.getLower() + "]"));
}
// Set the new exposure value to the ExposureState immediately.
mExposureStateImpl.setExposureCompensationIndex(exposure);
return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(
completer -> {
mExecutor.execute(() -> {
if (!mIsActive) {
mExposureStateImpl.setExposureCompensationIndex(
DEFAULT_EXPOSURE_COMPENSATION);
completer.setException(new CameraControl.OperationCanceledException(
"Camera is not active."));
return;
}
clearRunningTask();
Preconditions.checkState(mRunningCompleter == null, "mRunningCompleter "
+ "should be null when starting set a new exposure compensation "
+ "value");
Preconditions.checkState(mRunningCaptureResultListener == null,
"mRunningCaptureResultListener "
+ "should be null when starting set a new exposure "
+ "compensation value");
mRunningCaptureResultListener =
captureResult -> {
Integer state = captureResult.get(
CaptureResult.CONTROL_AE_STATE);
Integer evResult = captureResult.get(
CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION);
if (state != null && evResult != null) {
switch (state) {
case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:
case CaptureResult.CONTROL_AE_STATE_CONVERGED:
case CaptureResult.CONTROL_AE_STATE_LOCKED:
if (evResult == exposure) {
completer.set(exposure);
// Only remove the capture result listener,
// the mRunningCompleter and
// mRunningCaptureResultListener will be
// cleared before the next set exposure task.
return true;
}
break;
default:
// Ignore other results.
}
} else if (evResult != null && evResult == exposure) {
// If AE state is null, only wait for the exposure result
// to the desired value.
completer.set(exposure);
// Only remove the capture result listener, the
// mRunningCompleter and mRunningCaptureResultListener
// will be cleared before the next set exposure task.
return true;
}
return false;
};
mRunningCompleter = completer;
mCameraControl.addCaptureResultListener(mRunningCaptureResultListener);
mCameraControl.updateSessionConfigSynchronous();
});
return "setExposureCompensationIndex[" + exposure + "]";
}));
}
@ExecutedBy("mExecutor")
private void clearRunningTask() {
if (mRunningCompleter != null) {
mRunningCompleter.setException(
new CameraControl.OperationCanceledException(
"Cancelled by another setExposureCompensationIndex()"));
mRunningCompleter = null;
}
if (mRunningCaptureResultListener != null) {
mCameraControl.removeCaptureResultListener(mRunningCaptureResultListener);
mRunningCaptureResultListener = null;
}
}
}