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()} &lt;= minimum
+     * supported EV</code>
+     * <p><code>Max.exposure compensation * {@link #getExposureCompensationStep()} &gt;= 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;
     }