Add ExposureCompensation to camera control
Add the ExposureCompensation setter to the camera control
Add the ExposureState getter to the camera info.
Relnote: "Added experimental interfaces for ExposureCompensation"
Bug: 134178402
Test: ./gradlew bOS &&
./gradlew camera:camera-core:connectedAndroidTest &&
./gradlew camera:camera-camera2:connectedAndroidTest &&
./gradlew camera:camera-extensions:connectedAndroidTest &&
./gradlew camera:camera-core:test &&
./gradlew camera:camera-camera2:test &&
./gradlew camera:camera-extensions:test &&
./gradlew camera:camera-extensions:connectedAndroidTest &&
./gradlew camera:integration-tests:camera-testapp-extensions:connectedAndroidTest
Change-Id: If96c7276cb8d20ac59ea5167245225c6327d23e0
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
new file mode 100644
index 0000000..7b32ba50
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -0,0 +1,538 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.TestCase.assertTrue;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.experimental.UseExperimental;
+import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.camera2.internal.util.SemaphoreReleasingCamera2Callbacks;
+import androidx.camera.camera2.interop.Camera2Interop;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.ExperimentalExposureCompensation;
+import androidx.camera.core.ExposureState;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.impl.CameraControlInternal;
+import androidx.camera.core.impl.CameraInfoInternal;
+import androidx.camera.core.impl.CameraInternal;
+import androidx.camera.core.impl.CameraStateRegistry;
+import androidx.camera.core.impl.DeferrableSurface;
+import androidx.camera.core.impl.ImmediateSurface;
+import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.CameraUseCaseAdapter;
+import androidx.camera.testing.CameraUtil;
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfig;
+import androidx.core.os.HandlerCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Contains tests for {@link androidx.camera.camera2.internal.ExposureControl} internal
+ * implementation.
+ */
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@UseExperimental(markerClass = ExperimentalExposureCompensation.class)
+public class ExposureDeviceTest {
+
+ @CameraSelector.LensFacing
+ private static final int DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_BACK;
+ // For the purpose of this test, always say we have 1 camera available.
+ private static final int DEFAULT_AVAILABLE_CAMERA_COUNT = 1;
+
+ @Rule
+ public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest();
+
+ private ArrayList<FakeTestUseCase> mFakeTestUseCases = new ArrayList<>();
+ private Camera2CameraImpl mCamera2CameraImpl;
+ private static ExecutorService sCameraExecutor;
+ private static HandlerThread sCameraHandlerThread;
+ private static Handler sCameraHandler;
+ private CameraStateRegistry mCameraStateRegistry;
+ Semaphore mSemaphore;
+ String mCameraId;
+ SemaphoreReleasingCamera2Callbacks.SessionStateCallback mSessionStateCallback;
+ private CameraUseCaseAdapter mCameraUseCaseAdapter;
+ private CameraInfoInternal mCameraInfoInternal;
+ private CameraControlInternal mCameraControlInternal;
+
+ @BeforeClass
+ public static void classSetup() {
+ sCameraHandlerThread = new HandlerThread("cameraThread");
+ sCameraHandlerThread.start();
+ sCameraHandler = HandlerCompat.createAsync(sCameraHandlerThread.getLooper());
+ sCameraExecutor = CameraXExecutors.newHandlerExecutor(sCameraHandler);
+ }
+
+ @AfterClass
+ public static void classTeardown() {
+ sCameraHandlerThread.quitSafely();
+ }
+
+ @Before
+ public void setup() throws CameraUnavailableException {
+ // TODO(b/162296654): Workaround the google_3a specific behavior.
+ assumeFalse("Cuttlefish uses google_3a v1 or v2 it might fail to set EV before "
+ + "first AE converge.", android.os.Build.MODEL.contains("Cuttlefish"));
+ assumeFalse("Pixel uses google_3a v1 or v2 it might fail to set EV before "
+ + "first AE converge.", android.os.Build.MODEL.contains("Pixel"));
+
+ assumeTrue(CameraUtil.deviceHasCamera());
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(DEFAULT_LENS_FACING));
+ mSessionStateCallback = new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
+ mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
+ mSemaphore = new Semaphore(0);
+ mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
+ mCamera2CameraImpl = new Camera2CameraImpl(
+ CameraManagerCompat.from(ApplicationProvider.getApplicationContext()), mCameraId,
+ mCameraStateRegistry, sCameraExecutor, sCameraHandler);
+
+ mCameraInfoInternal = mCamera2CameraImpl.getCameraInfoInternal();
+ mCameraControlInternal = mCamera2CameraImpl.getCameraControlInternal();
+ mCamera2CameraImpl.open();
+
+ FakeCameraDeviceSurfaceManager fakeCameraDeviceSurfaceManager =
+ new FakeCameraDeviceSurfaceManager();
+ fakeCameraDeviceSurfaceManager.setSuggestedResolution(mCameraId, FakeUseCaseConfig.class,
+ new Size(640, 480));
+
+ mCameraUseCaseAdapter = new CameraUseCaseAdapter(mCamera2CameraImpl,
+ new LinkedHashSet<>(Collections.singleton(mCamera2CameraImpl)),
+ fakeCameraDeviceSurfaceManager);
+ }
+
+ @After
+ public void teardown() throws InterruptedException, ExecutionException {
+ // Need to release the camera no matter what is done, otherwise the CameraDevice is not
+ // closed.
+ // When the CameraDevice is not closed, then it can cause problems with interferes with
+ // other test cases.
+ if (mCameraUseCaseAdapter != null) {
+ mCameraUseCaseAdapter.removeUseCases(
+ Collections.unmodifiableCollection(mFakeTestUseCases));
+ }
+ if (mCamera2CameraImpl != null) {
+ mCamera2CameraImpl.release().get();
+ }
+
+ for (FakeTestUseCase fakeUseCase : mFakeTestUseCases) {
+ fakeUseCase.clear();
+ }
+ }
+
+ private FakeTestUseCase openUseCase() throws CameraUseCaseAdapter.CameraException {
+ FakeUseCaseConfig.Builder configBuilder =
+ new FakeUseCaseConfig.Builder().setTargetName("UseCase");
+ new Camera2Interop.Extender<>(configBuilder).setSessionStateCallback(mSessionStateCallback);
+
+ FakeTestUseCase testUseCase = new FakeTestUseCase(configBuilder.getUseCaseConfig(),
+ mCamera2CameraImpl, mSessionStateCallback);
+ mFakeTestUseCases.add(testUseCase);
+
+ mCameraUseCaseAdapter.addUseCases(Collections.singletonList(testUseCase));
+ mCameraUseCaseAdapter.attachUseCases();
+
+ return testUseCase;
+ }
+
+ @Test
+ public void setExposure_futureResultTest() throws InterruptedException, TimeoutException,
+ ExecutionException, CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+
+ openUseCase();
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ int ret = mCameraControlInternal.setExposureCompensationIndex(upper).get(3000,
+ TimeUnit.MILLISECONDS);
+ assertThat(ret).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureTest() throws InterruptedException, TimeoutException,
+ ExecutionException, CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+
+ FakeTestUseCase useCase = openUseCase();
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ // Set the exposure compensation
+ mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
+
+ ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
+ TotalCaptureResult.class);
+ CameraCaptureSession.CaptureCallback callback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ useCase.setCameraCaptureCallback(callback);
+ verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
+ any(CameraCaptureSession.class),
+ any(CaptureRequest.class),
+ captureResultCaptor.capture());
+ List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
+ TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
+
+ // Verify the exposure compensation target result is in the capture result.
+ assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureTest_runTwice()
+ throws InterruptedException, TimeoutException, ExecutionException,
+ CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+
+ FakeTestUseCase useCase = openUseCase();
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+
+ // Set the EC value first time.
+ mCameraControlInternal.setExposureCompensationIndex(upper - 1);
+
+ // Set the EC value again, and verify this task should complete successfully.
+ mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
+
+ ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
+ TotalCaptureResult.class);
+ CameraCaptureSession.CaptureCallback callback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ useCase.setCameraCaptureCallback(callback);
+ verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
+ any(CameraCaptureSession.class),
+ any(CaptureRequest.class),
+ captureResultCaptor.capture());
+ List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
+ TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
+
+ // Verify the exposure compensation target result is in the capture result.
+ assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureAndTriggerAe_theExposureSettingShouldApply()
+ throws InterruptedException, ExecutionException, TimeoutException,
+ CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+
+ FakeTestUseCase useCase = openUseCase();
+ ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
+ TotalCaptureResult.class);
+ CameraCaptureSession.CaptureCallback callback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ useCase.setCameraCaptureCallback(callback);
+
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ // Set the exposure compensation
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+ mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
+ mCameraControlInternal.triggerAePrecapture().get(3000, TimeUnit.MILLISECONDS);
+
+ // Verify the exposure compensation target result is in the capture result.
+ verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
+ any(CameraCaptureSession.class),
+ any(CaptureRequest.class),
+ captureResultCaptor.capture());
+ List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
+ TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
+ assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureAndTriggerAf_theExposureSettingShouldApply()
+ throws InterruptedException, ExecutionException, TimeoutException,
+ CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+
+ FakeTestUseCase useCase = openUseCase();
+ ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
+ TotalCaptureResult.class);
+ CameraCaptureSession.CaptureCallback callback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ useCase.setCameraCaptureCallback(callback);
+
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+ mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
+ mCameraControlInternal.triggerAf().get(3000, TimeUnit.MILLISECONDS);
+
+ // Verify the exposure compensation target result is in the capture result.
+ verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
+ any(CameraCaptureSession.class),
+ any(CaptureRequest.class),
+ captureResultCaptor.capture());
+ List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
+ TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
+ assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureAndZoomRatio_theExposureSettingShouldApply()
+ throws InterruptedException, ExecutionException, TimeoutException,
+ CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+
+ FakeTestUseCase useCase = openUseCase();
+ ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
+ TotalCaptureResult.class);
+ CameraCaptureSession.CaptureCallback callback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ useCase.setCameraCaptureCallback(callback);
+
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+ mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
+ mCameraControlInternal.setZoomRatio(
+ mCameraInfoInternal.getZoomState().getValue().getMaxZoomRatio()).get(3000,
+ TimeUnit.MILLISECONDS);
+
+ // Verify the exposure compensation target result is in the capture result.
+ verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
+ any(CameraCaptureSession.class),
+ any(CaptureRequest.class),
+ captureResultCaptor.capture());
+ List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
+ TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
+ assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureAndLinearZoom_theExposureSettingShouldApply()
+ throws InterruptedException, ExecutionException, TimeoutException,
+ CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+
+ FakeTestUseCase useCase = openUseCase();
+ ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
+ TotalCaptureResult.class);
+ CameraCaptureSession.CaptureCallback callback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ useCase.setCameraCaptureCallback(callback);
+
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+ mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
+ mCameraControlInternal.setLinearZoom(0.5f).get(3000, TimeUnit.MILLISECONDS);
+
+ // Verify the exposure compensation target result is in the capture result.
+ verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
+ any(CameraCaptureSession.class),
+ any(CaptureRequest.class),
+ captureResultCaptor.capture());
+ List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
+ TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
+ assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureAndFlash_theExposureSettingShouldApply()
+ throws InterruptedException, ExecutionException, TimeoutException,
+ CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+
+ FakeTestUseCase useCase = openUseCase();
+ ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
+ TotalCaptureResult.class);
+ CameraCaptureSession.CaptureCallback callback = mock(
+ CameraCaptureSession.CaptureCallback.class);
+ useCase.setCameraCaptureCallback(callback);
+
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ int upper = exposureState.getExposureCompensationRange().getUpper();
+ mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
+ mCameraControlInternal.setFlashMode(ImageCapture.FLASH_MODE_AUTO);
+
+ // Verify the exposure compensation target result is in the capture result.
+ verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
+ any(CameraCaptureSession.class),
+ any(CaptureRequest.class),
+ captureResultCaptor.capture());
+ List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
+ TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
+ assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
+ }
+
+ @Test
+ public void setExposureTimeout_theNextCallShouldWork()
+ throws InterruptedException, ExecutionException, TimeoutException,
+ CameraUseCaseAdapter.CameraException {
+ ExposureState exposureState = mCameraInfoInternal.getExposureState();
+ assumeTrue(exposureState.isExposureCompensationSupported());
+
+ openUseCase();
+ // Wait a little bit for the camera to open.
+ assertTrue(mSessionStateCallback.waitForOnConfigured(1));
+
+ try {
+ // The set future should timeout in this test.
+ mCameraControlInternal.setExposureCompensationIndex(1).get(0, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(TimeoutException.class);
+ }
+
+ // Verify the second time call should set the new exposure value successfully.
+ assertThat(mCameraControlInternal.setExposureCompensationIndex(2).get(3000,
+ TimeUnit.MILLISECONDS)).isEqualTo(2);
+ }
+
+ public static class FakeTestUseCase extends FakeUseCase {
+ private FakeUseCaseConfig mConfig;
+ private DeferrableSurface mDeferrableSurface;
+ private CameraCaptureSession.StateCallback mSessionStateCallback;
+ CameraCaptureSession.CaptureCallback mCameraCaptureCallback;
+
+ FakeTestUseCase(
+ @NonNull FakeUseCaseConfig config,
+ @NonNull CameraInternal cameraInternal,
+ @NonNull CameraCaptureSession.StateCallback sessionStateCallback) {
+ super(config);
+ // Ensure we're using the combined configuration (user config + defaults)
+ mConfig = (FakeUseCaseConfig) getUseCaseConfig();
+
+ mSessionStateCallback = sessionStateCallback;
+ }
+
+ public void setCameraCaptureCallback(
+ CameraCaptureSession.CaptureCallback cameraCaptureCallback) {
+ mCameraCaptureCallback = cameraCaptureCallback;
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ if (mDeferrableSurface != null) {
+ mDeferrableSurface.close();
+ }
+ }
+
+ @Override
+ @NonNull
+ protected Size onSuggestedResolutionUpdated(
+ @NonNull Size suggestedResolution) {
+ createPipeline(suggestedResolution);
+ notifyActive();
+ return suggestedResolution;
+ }
+
+ private void createPipeline(Size resolution) {
+ SessionConfig.Builder builder = SessionConfig.Builder.createFrom(mConfig);
+
+ builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+ if (mDeferrableSurface != null) {
+ mDeferrableSurface.close();
+ }
+
+ // Create the metering DeferrableSurface
+ SurfaceTexture surfaceTexture = new SurfaceTexture(0);
+ surfaceTexture.setDefaultBufferSize(resolution.getWidth(), resolution.getHeight());
+ Surface surface = new Surface(surfaceTexture);
+
+ mDeferrableSurface = new ImmediateSurface(surface);
+ mDeferrableSurface.getTerminationFuture().addListener(() -> {
+ surface.release();
+ surfaceTexture.release();
+ }, CameraXExecutors.directExecutor());
+ builder.addSurface(mDeferrableSurface);
+ builder.addSessionStateCallback(mSessionStateCallback);
+ builder.addRepeatingCameraCaptureCallback(CaptureCallbackContainer.create(
+ new CameraCaptureSession.CaptureCallback() {
+ @Override
+ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
+ @NonNull CaptureRequest request,
+ @NonNull TotalCaptureResult result) {
+ if (mCameraCaptureCallback != null) {
+ mCameraCaptureCallback.onCaptureCompleted(session, request, result);
+ }
+ }
+ }));
+
+ builder.addErrorListener((sessionConfig, error) -> {
+ // Create new pipeline and it will close the old one.
+ createPipeline(resolution);
+ });
+ updateSessionConfig(builder.build());
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControl.java
index 2bef42b..0585cb8 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControl.java
@@ -37,6 +37,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
+import androidx.camera.core.ExperimentalExposureCompensation;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture;
@@ -107,6 +108,7 @@
private final FocusMeteringControl mFocusMeteringControl;
private final ZoomControl mZoomControl;
private final TorchControl mTorchControl;
+ private final ExposureControl mExposureControl;
private final AeFpsRange mAeFpsRange;
@GuardedBy("mLock")
private int mUseCount = 0;
@@ -149,6 +151,7 @@
// CameraCaptureCallback efficiently.
mSessionConfigBuilder.addRepeatingCameraCaptureCallback(mCameraCaptureCallbackSet);
+ mExposureControl = new ExposureControl(this, mCameraCharacteristics);
mFocusMeteringControl = new FocusMeteringControl(this, scheduler, mExecutor);
mZoomControl = new ZoomControl(this, mCameraCharacteristics, mExecutor);
mTorchControl = new TorchControl(this, mCameraCharacteristics, mExecutor);
@@ -204,6 +207,11 @@
return mTorchControl;
}
+ @NonNull
+ public ExposureControl getExposureControl() {
+ return mExposureControl;
+ }
+
/**
* Set current active state. Set active if it is ready to trigger camera control operation.
*
@@ -215,6 +223,7 @@
mFocusMeteringControl.setActive(isActive);
mZoomControl.setActive(isActive);
mTorchControl.setActive(isActive);
+ mExposureControl.setActive(isActive);
}
@ExecutedBy("mExecutor")
@@ -369,6 +378,13 @@
cancelAePrecaptureTrigger));
}
+ @NonNull
+ @Override
+ @ExperimentalExposureCompensation
+ public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
+ return mExposureControl.setExposureCompensationIndex(exposure);
+ }
+
/** {@inheritDoc} */
@Override
public void submitCaptureRequests(@NonNull final List<CaptureConfig> captureConfigs) {
@@ -515,6 +531,8 @@
builder.setCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, mCropRect);
}
+ mExposureControl.setCaptureRequestOption(builder);
+
return builder.build();
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index d0b8888..d59d58b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -24,6 +24,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalExposureCompensation;
+import androidx.camera.core.ExposureState;
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
@@ -46,6 +48,7 @@
private final Camera2CameraControl mCamera2CameraControl;
private final ZoomControl mZoomControl;
private final TorchControl mTorchControl;
+ private final ExposureControl mExposureControl;
Camera2CameraInfoImpl(@NonNull String cameraId,
@NonNull CameraCharacteristics cameraCharacteristics,
@@ -56,6 +59,7 @@
mCamera2CameraControl = camera2CameraControl;
mZoomControl = camera2CameraControl.getZoomControl();
mTorchControl = camera2CameraControl.getTorchControl();
+ mExposureControl = camera2CameraControl.getExposureControl();
logDeviceInfo();
}
@@ -173,6 +177,13 @@
return mZoomControl.getZoomState();
}
+ @NonNull
+ @Override
+ @ExperimentalExposureCompensation
+ public ExposureState getExposureState() {
+ return mExposureControl.getExposureState();
+ }
+
/**
* {@inheritDoc}
*
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
new file mode 100644
index 0000000..50a3790
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
@@ -0,0 +1,229 @@
+/*
+ * 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.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.annotation.experimental.UseExperimental;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.ExposureState;
+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;
+
+/**
+ * Implementation of Exposure compensation control.
+ *
+ * <p>It is intended to be used within {@link Camera2CameraControl} to implement the
+ * functionality of {@link Camera2CameraControl#setExposureCompensationIndex(int)}.
+ *
+ * <p>To wait for the exposure setting reach to the new requested target, it calls
+ * {@link Camera2CameraControl#addCaptureResultListener(Camera2CameraControl.CaptureResultListener)}
+ * to monitor the capture result.
+ *
+ * <p>The {@link Camera2CameraControl#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.
+ */
+@UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+public class ExposureControl {
+
+ private static final int DEFAULT_EXPOSURE_COMPENSATION = 0;
+
+ private final Object mLock = new Object();
+
+ @NonNull
+ private final Camera2CameraControl mCameraControl;
+
+ @NonNull
+ private final ExposureStateImpl mExposureStateImpl;
+
+ @GuardedBy("mLock")
+ private boolean mIsActive = false;
+
+ @Nullable
+ @GuardedBy("mLock")
+ private CallbackToFutureAdapter.Completer<Integer> mRunningCompleter;
+ @Nullable
+ @GuardedBy("mLock")
+ private Camera2CameraControl.CaptureResultListener mRunningCaptureResultListener;
+
+ /**
+ * Constructs a ExposureControl.
+ *
+ * <p>All tasks executed by {@code executor}.
+ *
+ * @param cameraControl Camera control.
+ * @param cameraCharacteristics The {@link CameraCharacteristics} of the camera.
+ */
+ ExposureControl(@NonNull Camera2CameraControl cameraControl,
+ @NonNull CameraCharacteristics cameraCharacteristics) {
+ mCameraControl = cameraControl;
+ mExposureStateImpl = 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}.
+ */
+ void setActive(boolean isActive) {
+ synchronized (mLock) {
+ if (isActive == mIsActive) {
+ return;
+ }
+
+ mIsActive = isActive;
+
+ if (!mIsActive) {
+ mExposureStateImpl.setExposureCompensationIndex(DEFAULT_EXPOSURE_COMPENSATION);
+ clearRunningTask();
+ }
+ }
+ }
+
+ /**
+ * Called by {@link Camera2CameraControl} to append the CONTROL_AE_EXPOSURE_COMPENSATION option
+ * to the shared options. It applies to all repeating requests and single requests.
+ */
+ @WorkerThread
+ void setCaptureRequestOption(@NonNull Camera2ImplConfig.Builder configBuilder) {
+ synchronized (mLock) {
+ configBuilder.setCaptureRequestOption(
+ CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION,
+ mExposureStateImpl.getExposureCompensationIndex());
+ }
+ }
+
+ @NonNull
+ ExposureState getExposureState() {
+ return mExposureStateImpl;
+ }
+
+ @NonNull
+ @WorkerThread
+ ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
+ return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(
+ completer -> {
+ synchronized (mLock) {
+ if (!mIsActive) {
+ completer.setException(new CameraControl.OperationCanceledException(
+ "Camera is not active."));
+ return "setExposureCompensation[" + exposure + "]";
+ }
+
+ if (!mExposureStateImpl.isExposureCompensationSupported()) {
+ completer.setException(new IllegalArgumentException(
+ "ExposureCompensation is not supported"));
+ return "setExposureCompensation[" + exposure + "]";
+
+ }
+
+ Range<Integer> range = mExposureStateImpl.getExposureCompensationRange();
+ if (!range.contains(exposure)) {
+ completer.setException(new IllegalArgumentException(
+ "Requested ExposureCompensation " + exposure + " is not within"
+ + " valid range [" + range.getUpper() + ".."
+ + range.getLower() + "]"));
+ return "setExposureCompensation[" + exposure + "]";
+ }
+
+ 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);
+ mExposureStateImpl.setExposureCompensationIndex(exposure);
+ mCameraControl.updateSessionConfig();
+
+ return "setExposureCompensationIndex[" + exposure + "]";
+ }
+ }));
+ }
+
+ private void clearRunningTask() {
+ synchronized (mLock) {
+ if (mRunningCompleter != null) {
+ mRunningCompleter.setException(
+ new CameraControl.OperationCanceledException(
+ "Cancelled by another setExposureCompensationIndex()"));
+ mRunningCompleter = null;
+ }
+
+ if (mRunningCaptureResultListener != null) {
+ mCameraControl.removeCaptureResultListener(mRunningCaptureResultListener);
+ mRunningCaptureResultListener = null;
+ }
+ }
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
new file mode 100644
index 0000000..674b180
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
@@ -0,0 +1,79 @@
+/*
+ * 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.util.Range;
+import android.util.Rational;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.camera.core.ExperimentalExposureCompensation;
+import androidx.camera.core.ExposureState;
+
+/**
+ * An implementation of {@link ExposureState} where the values can be set.
+ */
+@ExperimentalExposureCompensation
+class ExposureStateImpl implements ExposureState {
+
+ private final Object mLock = new Object();
+ private final CameraCharacteristics mCameraCharacteristics;
+ @GuardedBy("mLock")
+ private int mExposureCompensation;
+
+ ExposureStateImpl(CameraCharacteristics characteristics, int exposureCompensation) {
+ mCameraCharacteristics = characteristics;
+ mExposureCompensation = exposureCompensation;
+ }
+
+ @Override
+ public int getExposureCompensationIndex() {
+ synchronized (mLock) {
+ return mExposureCompensation;
+ }
+ }
+
+ void setExposureCompensationIndex(int value) {
+ synchronized (mLock) {
+ mExposureCompensation = value;
+ }
+ }
+
+ @NonNull
+ @Override
+ public Range<Integer> getExposureCompensationRange() {
+ return mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
+ }
+
+ @NonNull
+ @Override
+ public Rational getExposureCompensationStep() {
+ if (!isExposureCompensationSupported()) {
+ return Rational.ZERO;
+ }
+ return mCameraCharacteristics.get(
+ CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP);
+ }
+
+ @Override
+ public boolean isExposureCompensationSupported() {
+ Range<Integer> compensationRange =
+ mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
+ return compensationRange.getLower() != 0 && compensationRange.getUpper() != 0;
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ExposureControlTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ExposureControlTest.java
new file mode 100644
index 0000000..60f2d08
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ExposureControlTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Build;
+import android.util.Range;
+import android.util.Rational;
+
+import androidx.annotation.experimental.UseExperimental;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.ExperimentalExposureCompensation;
+import androidx.camera.core.impl.CameraControlInternal;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@DoNotInstrument
+@UseExperimental(markerClass = ExperimentalExposureCompensation.class)
+public class ExposureControlTest {
+
+ private static final String CAMERA0_ID = "0";
+ private static final String CAMERA1_ID = "1";
+
+ private ExposureControl mExposureControl;
+ private Camera2CameraControl mCamera2CameraControl;
+
+ @Before
+ public void setUp() throws CameraAccessException {
+ initCameras();
+
+ CameraControlInternal.ControlUpdateCallback updateCallback = mock(
+ CameraControlInternal.ControlUpdateCallback.class);
+
+ CameraManager cameraManager =
+ (CameraManager) ApplicationProvider.getApplicationContext().getSystemService(
+ Context.CAMERA_SERVICE);
+ CameraCharacteristics cameraCharacteristics =
+ cameraManager.getCameraCharacteristics(CAMERA0_ID);
+
+ mCamera2CameraControl = spy(new Camera2CameraControl(
+ cameraCharacteristics,
+ CameraXExecutors.mainThreadExecutor(),
+ CameraXExecutors.directExecutor(),
+ updateCallback));
+
+ mExposureControl = new ExposureControl(mCamera2CameraControl, cameraCharacteristics);
+ mExposureControl.setActive(true);
+ }
+
+ private void initCameras() {
+ CameraCharacteristics characteristics0 =
+ ShadowCameraCharacteristics.newCameraCharacteristics();
+ ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(characteristics0);
+ shadowCharacteristics0.set(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE,
+ Range.create(-4, 4));
+ shadowCharacteristics0.set(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP,
+ Rational.parseRational("1/2"));
+
+ CameraCharacteristics characteristics1 =
+ ShadowCameraCharacteristics.newCameraCharacteristics();
+ ShadowCameraCharacteristics shadowCharacteristics1 = Shadow.extract(characteristics1);
+ shadowCharacteristics1.set(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE,
+ Range.create(-0, 0));
+
+ // Add the camera to the camera service
+ ShadowCameraManager shadowCameraManager = Shadow.extract(
+ ApplicationProvider.getApplicationContext().getSystemService(
+ Context.CAMERA_SERVICE));
+
+ shadowCameraManager.addCamera(CAMERA0_ID, characteristics0);
+ shadowCameraManager.addCamera(CAMERA1_ID, characteristics1);
+ }
+
+ @Test
+ public void setExposureTwice_theFirstCallShouldBeCancelled() throws InterruptedException {
+ ListenableFuture<Integer> future1 = mExposureControl.setExposureCompensationIndex(1);
+ ListenableFuture<Integer> future2 = mExposureControl.setExposureCompensationIndex(2);
+
+ // The second call should keep working.
+ assertFalse(future2.isDone());
+
+ // The first call should be cancelled with a CameraControl.OperationCanceledException.
+ try {
+ future1.get();
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isInstanceOf(
+ CameraControlInternal.OperationCanceledException.class);
+ }
+ }
+
+ @Test
+ public void setExposureTimeout_theCompensationValueShouldKeepInControl() {
+ ListenableFuture<Integer> future1 = mExposureControl.setExposureCompensationIndex(1);
+
+ try {
+ // The set future should timeout in this test.
+ future1.get(0, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ assertThat(e).isInstanceOf(TimeoutException.class);
+ }
+
+ // The new value should be set to the exposure control even when the ListenableFuture
+ // task fails.
+ assertThat(mExposureControl.getExposureState().getExposureCompensationIndex()).isEqualTo(1);
+ }
+
+ @Test
+ public void exposureControlInactive_setExposureTaskShouldCancel()
+ throws InterruptedException, TimeoutException {
+ ListenableFuture<Integer> future = mExposureControl.setExposureCompensationIndex(1);
+ mExposureControl.setActive(false);
+
+ try {
+ // The exposure control has been set to inactive. It should throw the exception.
+ future.get(3000, TimeUnit.MILLISECONDS);
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isInstanceOf(CameraControl.OperationCanceledException.class);
+ }
+ }
+
+ @Test
+ public void setExposureNotInRange_shouldCompleteTheTaskWithException()
+ throws InterruptedException, TimeoutException {
+ try {
+ // The Exposure index to 5 not in the valid range. It should throw the exception.
+ mExposureControl.setExposureCompensationIndex(5).get(3000, TimeUnit.MILLISECONDS);
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+
+ @Test
+ public void setExposureOnNotSupportedCamera_shouldCompleteTheTaskWithException()
+ throws CameraAccessException, InterruptedException, TimeoutException {
+ CameraManager cameraManager =
+ (CameraManager) ApplicationProvider.getApplicationContext().getSystemService(
+ Context.CAMERA_SERVICE);
+ CameraCharacteristics cameraCharacteristics =
+ cameraManager.getCameraCharacteristics(CAMERA1_ID);
+
+ mCamera2CameraControl = spy(new Camera2CameraControl(
+ cameraCharacteristics,
+ CameraXExecutors.mainThreadExecutor(),
+ CameraXExecutors.directExecutor(),
+ mock(CameraControlInternal.ControlUpdateCallback.class)));
+
+ mExposureControl = new ExposureControl(mCamera2CameraControl, cameraCharacteristics);
+ mExposureControl.setActive(true);
+
+ ListenableFuture<Integer> future = mExposureControl.setExposureCompensationIndex(1);
+ try {
+ // This camera does not support the exposure compensation, the task should fail.
+ future.get(3000, TimeUnit.MILLISECONDS);
+ } catch (ExecutionException e) {
+ assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+}
diff --git a/camera/camera-core/api/public_plus_experimental_1.0.0-beta09.txt b/camera/camera-core/api/public_plus_experimental_1.0.0-beta09.txt
index 26f117f..b680467 100644
--- a/camera/camera-core/api/public_plus_experimental_1.0.0-beta09.txt
+++ b/camera/camera-core/api/public_plus_experimental_1.0.0-beta09.txt
@@ -14,6 +14,7 @@
public interface CameraControl {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @androidx.camera.core.ExperimentalExposureCompensation public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
@@ -27,6 +28,7 @@
}
public interface CameraInfo {
+ method @androidx.camera.core.ExperimentalExposureCompensation public androidx.camera.core.ExposureState getExposureState();
method public int getSensorRotationDegrees();
method public int getSensorRotationDegrees(int);
method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
@@ -90,12 +92,22 @@
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCustomizableThreads {
}
+ @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalExposureCompensation {
+ }
+
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
}
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroup {
}
+ @androidx.camera.core.ExperimentalExposureCompensation public interface ExposureState {
+ method public int getExposureCompensationIndex();
+ method public android.util.Range<java.lang.Integer!> getExposureCompensationRange();
+ method public android.util.Rational getExposureCompensationStep();
+ method public boolean isExposureCompensationSupported();
+ }
+
public interface ExtendableBuilder<T> {
method public T build();
}
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index 26f117f..b680467 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -14,6 +14,7 @@
public interface CameraControl {
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
+ method @androidx.camera.core.ExperimentalExposureCompensation public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(@FloatRange(from=0.0f, to=1.0f) float);
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
method public com.google.common.util.concurrent.ListenableFuture<androidx.camera.core.FocusMeteringResult!> startFocusAndMetering(androidx.camera.core.FocusMeteringAction);
@@ -27,6 +28,7 @@
}
public interface CameraInfo {
+ method @androidx.camera.core.ExperimentalExposureCompensation public androidx.camera.core.ExposureState getExposureState();
method public int getSensorRotationDegrees();
method public int getSensorRotationDegrees(int);
method public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
@@ -90,12 +92,22 @@
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCustomizableThreads {
}
+ @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalExposureCompensation {
+ }
+
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
}
@experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroup {
}
+ @androidx.camera.core.ExperimentalExposureCompensation public interface ExposureState {
+ method public int getExposureCompensationIndex();
+ method public android.util.Range<java.lang.Integer!> getExposureCompensationRange();
+ method public android.util.Rational getExposureCompensationStep();
+ method public boolean isExposureCompensationSupported();
+ }
+
public interface ExtendableBuilder<T> {
method public T build();
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
index dba659f..e9df24a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
@@ -150,6 +150,36 @@
ListenableFuture<Void> setLinearZoom(@FloatRange(from = 0f, to = 1f) float linearZoom);
/**
+ * Set the exposure compensation value for the camera.
+ *
+ * <p>Only one {@link #setExposureCompensationIndex} is allowed to run at the same time. If
+ * multiple {@link #setExposureCompensationIndex} are executed in a row, only the latest one
+ * setting will be kept in the camera. The other actions will be cancelled and the
+ * ListenableFuture will fail with the {@link OperationCanceledException}. After all the
+ * previous actions is cancelled, the camera device will adjust the brightness according to
+ * the latest setting.
+ *
+ * @param value the exposure compensation value to set on the camera which must be within
+ * the range of ExposureState#getExposureCompensationRange(). If the exposure
+ * compensation value is not in the range defined above, the returned
+ * {@link ListenableFuture} will fail with {@link IllegalArgumentException} and
+ * the value from ExposureState#getExposureCompensationIndex will not change.
+ * @return a {@link ListenableFuture} which is finished when the camera reaches the newly
+ * requested exposure target. Cancellation of this future is a no-op. The result of the
+ * ListenableFuture is the new target exposure value, or cancelled with the following
+ * exceptions,
+ * <ul>
+ * <li>{@link OperationCanceledException} when the camera is closed or a
+ * new {@link #setExposureCompensationIndex} is called.
+ * <li>{@link IllegalArgumentException} while the exposure compensation value to ranging
+ * within {@link ExposureState#getExposureCompensationRange}.
+ * </ul>
+ */
+ @NonNull
+ @ExperimentalExposureCompensation
+ ListenableFuture<Integer> setExposureCompensationIndex(int value);
+
+ /**
* An exception representing a failure that the operation is canceled which might be caused by
* a new value is set or camera is closed.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 29fd418..4ae94f0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -133,6 +133,15 @@
LiveData<ZoomState> getZoomState();
/**
+ * Returns a {@link ExposureState}.
+ *
+ * <p>The {@link ExposureState} contains the current exposure related information.
+ */
+ @NonNull
+ @ExperimentalExposureCompensation
+ ExposureState getExposureState();
+
+ /**
* Returns the implementation type of the camera, this depends on the {@link CameraXConfig}
* used in the initialization of CameraX.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
new file mode 100644
index 0000000..b7c7ce4
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
@@ -0,0 +1,37 @@
+/*
+ * 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.core;
+
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import androidx.annotation.experimental.Experimental;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Denotes that the annotated method uses the experimental ExposureCompensation APIs that can
+ * control the exposure compensation of the camera.
+ *
+ * <p>The feature allow the user to control the exposure compensation of the camera, it includes a
+ * setter in {@link androidx.camera.core.CameraControl} and a getter in
+ * {@link androidx.camera.core.CameraInfo}.
+ */
+@Retention(CLASS)
+@Experimental
+public @interface ExperimentalExposureCompensation {
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExposureState.java b/camera/camera-core/src/main/java/androidx/camera/core/ExposureState.java
new file mode 100644
index 0000000..919f708
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExposureState.java
@@ -0,0 +1,86 @@
+/*
+ * 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.core;
+
+import android.util.Range;
+import android.util.Rational;
+
+import androidx.annotation.NonNull;
+
+/**
+ * An interface which contains the camera exposure related information.
+ *
+ * <p>Applications can retrieve an instance via {@link CameraInfo#getExposureState()}.
+ */
+@ExperimentalExposureCompensation
+public interface ExposureState {
+
+ /**
+ * Get the current exposure compensation index.
+ *
+ * <p>The exposure value (EV) is the compensation index multiplied by the step value
+ * which is given by {@link #getExposureCompensationStep()}. Increasing the compensation
+ * index by using the {@link CameraControl#setExposureCompensationIndex} will increase
+ * exposure making the capture result brighter, decreasing the value making it darker.
+ * <p>For example, if the exposure value (EV) step size is 0.333, set the exposure compensation
+ * index value '6' will mean an exposure compensation of +2 EV; -3 will mean an exposure
+ * compensation of -1 EV.
+ * <p>The exposure value resets to default when there is no {@link UseCase} associated with
+ * the camera. For example, unbind all use cases from the camera or when the lifecycle
+ * changed that all the use case stopping data from the camera.
+ *
+ * @return The current exposure compensation index. If {@link
+ * #isExposureCompensationSupported()} is false, always return 0.
+ * @see CameraControl#setExposureCompensationIndex
+ */
+ int getExposureCompensationIndex();
+
+ /**
+ * Get the maximum and minimum exposure compensation values for
+ * {@link CameraControl#setExposureCompensationIndex}
+ *
+ * <p>The actual exposure value (EV) range that supported by the camera can be calculated by
+ * multiplying the {@link #getExposureCompensationStep()} with the maximum and minimum values:
+ * <p><code>Min.exposure compensation * {@link #getExposureCompensationStep()} <= minimum
+ * supported EV</code>
+ * <p><code>Max.exposure compensation * {@link #getExposureCompensationStep()} >= maximum
+ * supported EV</code>
+ *
+ * @return the maximum and minimum exposure compensation values range. If {@link
+ * #isExposureCompensationSupported()} is false, return Range [0,0].
+ * @see android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_RANGE
+ */
+ @NonNull
+ Range<Integer> getExposureCompensationRange();
+
+ /**
+ * Get the smallest step by which the exposure compensation can be changed.
+ *
+ * @return The exposure compensation step. If {@link
+ * #isExposureCompensationSupported()} is false, return {@link Rational#ZERO}.
+ * @see android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP
+ */
+ @NonNull
+ Rational getExposureCompensationStep();
+
+ /**
+ * Whether exposure compensation is supported for this camera.
+ *
+ * @return true if exposure compensation is supported for this camera.
+ */
+ boolean isExposureCompensationSupported();
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
index 8eacc77..86f318c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
@@ -23,6 +23,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraControl;
+import androidx.camera.core.ExperimentalExposureCompensation;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture.FlashMode;
@@ -83,6 +84,18 @@
void cancelAfAeTrigger(boolean cancelAfTrigger, boolean cancelAePrecaptureTrigger);
/**
+ * Set a exposure compensation to the camera
+ *
+ * @param exposure the exposure compensation value to set
+ * @return a ListenableFuture which is completed when the new exposure compensation reach the
+ * target.
+ */
+ @NonNull
+ @Override
+ @ExperimentalExposureCompensation
+ ListenableFuture<Integer> setExposureCompensationIndex(int exposure);
+
+ /**
* Performs capture requests.
*/
void submitCaptureRequests(@NonNull List<CaptureConfig> captureConfigs);
@@ -131,6 +144,13 @@
}
+ @NonNull
+ @Override
+ @ExperimentalExposureCompensation
+ public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
+ return Futures.immediateFuture(0);
+ }
+
@Override
public void submitCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index ae9118a..4f50bb9 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -24,6 +24,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.camera.core.ExperimentalExposureCompensation;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture;
@@ -138,6 +139,13 @@
+ cancelAePrecaptureTrigger + ")");
}
+ @NonNull
+ @Override
+ @ExperimentalExposureCompensation
+ public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
+ return Futures.immediateFuture(null);
+ }
+
@Override
public void submitCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
mSubmittedCaptureRequests.addAll(captureConfigs);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index d3d9803..19d3ab7 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -16,11 +16,15 @@
package androidx.camera.testing.fakes;
+import android.util.Range;
+import android.util.Rational;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExperimentalExposureCompensation;
+import androidx.camera.core.ExposureState;
import androidx.camera.core.TorchState;
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
@@ -119,6 +123,35 @@
@NonNull
@Override
+ @ExperimentalExposureCompensation
+ public ExposureState getExposureState() {
+ return new ExposureState() {
+ @Override
+ public int getExposureCompensationIndex() {
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public Range<Integer> getExposureCompensationRange() {
+ return Range.create(0, 0);
+ }
+
+ @NonNull
+ @Override
+ public Rational getExposureCompensationStep() {
+ return Rational.ZERO;
+ }
+
+ @Override
+ public boolean isExposureCompensationSupported() {
+ return true;
+ }
+ };
+ }
+
+ @NonNull
+ @Override
public String getImplementationType() {
return mImplementationType;
}