Merge "Migrate to Guava 27.0.1 and use new ListenableFuture." into androidx-master-dev
am: 14c45092d6

Change-Id: Id64aaf73930a22139172547b3703c6a62f9b5f73
diff --git a/camera/.gitignore b/camera/.gitignore
new file mode 100644
index 0000000..f43947b
--- /dev/null
+++ b/camera/.gitignore
@@ -0,0 +1,5 @@
+local.properties
+**/build
+maven-repo/
+*.DS_Store
+
diff --git a/camera/OWNERS b/camera/OWNERS
new file mode 100644
index 0000000..f912330
--- /dev/null
+++ b/camera/OWNERS
@@ -0,0 +1,6 @@
+fungja@google.com
+nilknarfuw@google.com
+trevormcguire@google.com
+dmchen@google.com
+vinitmodi@google.com
+ericng@google.com
diff --git a/camera/camera2/proguard.flags b/camera/camera2/proguard.flags
new file mode 100644
index 0000000..9cfa301
--- /dev/null
+++ b/camera/camera2/proguard.flags
@@ -0,0 +1,74 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Save the obfuscation mapping to a file, so we can de-obfuscate any stack
+# traces later on. Keep a fixed source file attribute and all line number
+# tables to get line numbers in the stack traces.
+# You can comment this out if you're not interested in stack traces.
+
+-printmapping out.map
+-keepparameternames
+-renamesourcefileattribute SourceFile
+-keepattributes Exceptions,InnerClasses,Deprecated,
+                SourceFile,LineNumberTable,EnclosingMethod
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+    public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+    java.lang.Class class$(java.lang.String);
+    java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+    public static **[] values();
+    public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+    static final long serialVersionUID;
+    static final java.io.ObjectStreamField[] serialPersistentFields;
+    private void writeObject(java.io.ObjectOutputStream);
+    private void readObject(java.io.ObjectInputStream);
+    java.lang.Object writeReplace();
+    java.lang.Object readResolve();
+}
+
+# Keep generic types for the TypeReference class
+-keepattributes Signature
diff --git a/camera/camera2/src/androidTest/AndroidManifest.xml b/camera/camera2/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..ecb6540
--- /dev/null
+++ b/camera/camera2/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.camera2">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application android:debuggable="true">
+        <uses-library
+            android:name="android.test.runner"
+            android:required="false" />
+        <uses-library
+            android:name="android.test.base"
+            android:required="false" />
+        <uses-library
+            android:name="android.test.mock"
+            android:required="false" />
+
+        <activity
+            android:name="androidx.camera.core.FakeActivity"
+            android:label="Fake Activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation
+        android:name="com.google.android.apps.common.testing.testrunner.Google3InstrumentationTestRunner"
+        android:targetPackage="androidx.camera.camera2">
+    </instrumentation>
+</manifest>
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultAndroidTest.java
new file mode 100644
index 0000000..090db55
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultAndroidTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.hardware.camera2.CaptureResult;
+
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureMetaData.FlashState;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class Camera2CameraCaptureResultAndroidTest {
+
+    private CaptureResult captureResult;
+    private Camera2CameraCaptureResult cameraCaptureResult;
+
+    @Before
+    public void setUp() {
+        captureResult = Mockito.mock(CaptureResult.class);
+        cameraCaptureResult = new Camera2CameraCaptureResult(captureResult);
+    }
+
+    @Test
+    public void getAfMode_withNull() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_MODE)).thenReturn(null);
+        assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.UNKNOWN);
+    }
+
+    @Test
+    public void getAfMode_withAfModeOff() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_OFF);
+        assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.OFF);
+    }
+
+    @Test
+    public void getAfMode_withAfModeEdof() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_EDOF);
+        assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.OFF);
+    }
+
+    @Test
+    public void getAfMode_withAfModeAuto() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_AUTO);
+        assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_MANUAL_AUTO);
+    }
+
+    @Test
+    public void getAfMode_withAfModeMacro() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_MACRO);
+        assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_MANUAL_AUTO);
+    }
+
+    @Test
+    public void getAfMode_withAfModeContinuousPicture() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+        assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_CONTINUOUS_AUTO);
+    }
+
+    @Test
+    public void getAfMode_withAfModeContinuousVideo() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
+        assertThat(cameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_CONTINUOUS_AUTO);
+    }
+
+    @Test
+    public void getAfState_withNull() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(null);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.UNKNOWN);
+    }
+
+    @Test
+    public void getAfState_withAfStateInactive() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_INACTIVE);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.INACTIVE);
+    }
+
+    @Test
+    public void getAfState_withAfStateActiveScan() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+    }
+
+    @Test
+    public void getAfState_withAfStatePassiveScan() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+    }
+
+    @Test
+    public void getAfState_withAfStatePassiveUnfocused() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+    }
+
+    @Test
+    public void getAfState_withAfStatePassiveFocused() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.FOCUSED);
+    }
+
+    @Test
+    public void getAfState_withAfStateFocusedLocked() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.LOCKED_FOCUSED);
+    }
+
+    @Test
+    public void getAfState_withAfStateNotFocusedLocked() {
+        when(captureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
+        assertThat(cameraCaptureResult.getAfState()).isEqualTo(AfState.LOCKED_NOT_FOCUSED);
+    }
+
+    @Test
+    public void getAeState_withNull() {
+        when(captureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(null);
+        assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.UNKNOWN);
+    }
+
+    @Test
+    public void getAeState_withAeStateInactive() {
+        when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_INACTIVE);
+        assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.INACTIVE);
+    }
+
+    @Test
+    public void getAeState_withAeStateSearching() {
+        when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_SEARCHING);
+        assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.SEARCHING);
+    }
+
+    @Test
+    public void getAeState_withAeStatePrecapture() {
+        when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE);
+        assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.SEARCHING);
+    }
+
+    @Test
+    public void getAeState_withAeStateFlashRequired() {
+        when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED);
+        assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.FLASH_REQUIRED);
+    }
+
+    @Test
+    public void getAeState_withAeStateConverged() {
+        when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_CONVERGED);
+        assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.CONVERGED);
+    }
+
+    @Test
+    public void getAeState_withAeStateLocked() {
+        when(captureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_LOCKED);
+        assertThat(cameraCaptureResult.getAeState()).isEqualTo(AeState.LOCKED);
+    }
+
+    @Test
+    public void getAwbState_withNull() {
+        when(captureResult.get(CaptureResult.CONTROL_AWB_STATE)).thenReturn(null);
+        assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.UNKNOWN);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateInactive() {
+        when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_INACTIVE);
+        assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.INACTIVE);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateSearching() {
+        when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_SEARCHING);
+        assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.METERING);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateConverged() {
+        when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_CONVERGED);
+        assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.CONVERGED);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateLocked() {
+        when(captureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_LOCKED);
+        assertThat(cameraCaptureResult.getAwbState()).isEqualTo(AwbState.LOCKED);
+    }
+
+    @Test
+    public void getFlashState_withNull() {
+        when(captureResult.get(CaptureResult.FLASH_STATE)).thenReturn(null);
+        assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.UNKNOWN);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateUnavailable() {
+        when(captureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_UNAVAILABLE);
+        assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.NONE);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateCharging() {
+        when(captureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_CHARGING);
+        assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.NONE);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateReady() {
+        when(captureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_READY);
+        assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.READY);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateFired() {
+        when(captureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_FIRED);
+        assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
+    }
+
+    @Test
+    public void getFlashState_withFlashStatePartial() {
+        when(captureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_PARTIAL);
+        assertThat(cameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlAndroidTest.java
new file mode 100644
index 0000000..fc6fcd3
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlAndroidTest.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_OFF;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH;
+import static android.hardware.camera2.CameraMetadata.FLASH_MODE_OFF;
+import static android.hardware.camera2.CameraMetadata.FLASH_MODE_TORCH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.os.Handler;
+
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.SessionConfiguration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+@RunWith(JUnit4.class)
+public class Camera2CameraControlAndroidTest {
+
+    Camera2CameraControl camera2CameraControl;
+    Camera2RequestRunner camera2RequestRunner;
+
+    @Before
+    public void setUp() {
+        camera2RequestRunner = mock(Camera2RequestRunner.class);
+        camera2CameraControl = new Camera2CameraControl(camera2RequestRunner, new Handler());
+    }
+
+    @Test
+    public void setCropRegion_cropRectSetAndRepeatingRequestUpdated() {
+        Rect rect = new Rect(0, 0, 10, 10);
+
+        camera2CameraControl.setCropRegion(rect);
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(repeatingConfig.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null))
+                .isEqualTo(rect);
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+        assertThat(singleConfig.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null))
+                .isEqualTo(rect);
+        verify(camera2RequestRunner).updateRepeatingRequest();
+    }
+
+    @Test
+    public void focus_focusRectSetAndRequestsExecuted() {
+        Rect focusRect = new Rect(0, 0, 10, 10);
+        Rect meteringRect = new Rect(20, 20, 30, 30);
+
+        camera2CameraControl.focus(focusRect, meteringRect);
+
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(focusRect,
+                                        MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(focusRect,
+                                        MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        verify(camera2RequestRunner).updateRepeatingRequest();
+        assertThat(camera2CameraControl.isFocusLocked()).isTrue();
+
+        ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+                ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+        verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+        CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AF_TRIGGER)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START);
+    }
+
+    @Test
+    public void cancelFocus_regionRestored() {
+        Rect focusRect = new Rect(0, 0, 10, 10);
+        Rect meteringRect = new Rect(20, 20, 30, 30);
+
+        camera2CameraControl.focus(focusRect, meteringRect);
+        camera2CameraControl.cancelFocus();
+
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        MeteringRectangle zeroRegion =
+                new MeteringRectangle(new Rect(), MeteringRectangle.METERING_WEIGHT_DONT_CARE);
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, (MeteringRectangle[]) null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+
+        assertThat(camera2CameraControl.isFocusLocked()).isFalse();
+        verify(camera2RequestRunner, times(2)).updateRepeatingRequest();
+    }
+
+    @Test
+    public void defaultAFAWBMode_ShouldBeCAFWhenNotFocusLocked() {
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(
+                        camera2CameraControl
+                                .getControlSessionConfiguration()
+                                .getImplementationOptions());
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_MODE_AUTO);
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_MODE,
+                        CaptureRequest.CONTROL_AWB_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AWB_MODE_AUTO);
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_MODE_AUTO);
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_MODE,
+                        CaptureRequest.CONTROL_AWB_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AWB_MODE_AUTO);
+    }
+
+    @Test
+    public void focus_afModeSetToAuto() {
+        Rect focusRect = new Rect(0, 0, 10, 10);
+        camera2CameraControl.focus(focusRect, focusRect);
+
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(
+                        camera2CameraControl
+                                .getControlSessionConfiguration()
+                                .getImplementationOptions());
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        camera2CameraControl.cancelFocus();
+
+        Camera2Configuration repeatingConfig2 =
+                new Camera2Configuration(
+                        camera2CameraControl
+                                .getControlSessionConfiguration()
+                                .getImplementationOptions());
+        assertThat(
+                repeatingConfig2.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+        Camera2Configuration singleConfig2 =
+                new Camera2Configuration(camera2CameraControl.getSingleRequestImplOptions());
+        assertThat(
+                singleConfig2.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+    }
+
+    @Test
+    public void setFlashModeAuto_aeModeSetAndRequestUpdated() {
+        camera2CameraControl.setFlashMode(FlashMode.AUTO);
+
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration camera2Configuration =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(
+                camera2Configuration.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+                .isEqualTo(CONTROL_AE_MODE_ON_AUTO_FLASH);
+        assertThat(camera2CameraControl.getFlashMode()).isEqualTo(FlashMode.AUTO);
+        verify(camera2RequestRunner).updateRepeatingRequest();
+    }
+
+    @Test
+    public void setFlashModeOff_aeModeSetAndRequestUpdated() {
+        camera2CameraControl.setFlashMode(FlashMode.OFF);
+
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration camera2Configuration =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(
+                camera2Configuration.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+                .isEqualTo(CONTROL_AE_MODE_ON);
+        assertThat(camera2CameraControl.getFlashMode()).isEqualTo(FlashMode.OFF);
+        verify(camera2RequestRunner).updateRepeatingRequest();
+    }
+
+    @Test
+    public void setFlashModeOn_aeModeSetAndRequestUpdated() {
+        camera2CameraControl.setFlashMode(FlashMode.ON);
+
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration camera2Configuration =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(
+                camera2Configuration.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+                .isEqualTo(CONTROL_AE_MODE_ON_ALWAYS_FLASH);
+        assertThat(camera2CameraControl.getFlashMode()).isEqualTo(FlashMode.ON);
+        verify(camera2RequestRunner).updateRepeatingRequest();
+    }
+
+    @Test
+    public void enableTorch_aeModeSetAndRequestUpdated() {
+        camera2CameraControl.enableTorch(true);
+
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration camera2Configuration =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(
+                camera2Configuration.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+                .isEqualTo(CONTROL_AE_MODE_ON);
+        assertThat(
+                camera2Configuration.getCaptureRequestOption(
+                        CaptureRequest.FLASH_MODE, FLASH_MODE_OFF))
+                .isEqualTo(FLASH_MODE_TORCH);
+        assertThat(camera2CameraControl.isTorchOn()).isTrue();
+        verify(camera2RequestRunner).updateRepeatingRequest();
+    }
+
+    @Test
+    public void disableTorchFlashModeAuto_aeModeSetAndRequestUpdated() {
+        camera2CameraControl.setFlashMode(FlashMode.AUTO);
+        camera2CameraControl.enableTorch(false);
+
+        SessionConfiguration sessionConfiguration =
+                camera2CameraControl.getControlSessionConfiguration();
+        Camera2Configuration camera2Configuration =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(
+                camera2Configuration.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+                .isEqualTo(CONTROL_AE_MODE_ON_AUTO_FLASH);
+        assertThat(camera2Configuration.getCaptureRequestOption(CaptureRequest.FLASH_MODE, -1))
+                .isEqualTo(-1);
+        assertThat(camera2CameraControl.isTorchOn()).isFalse();
+        verify(camera2RequestRunner, times(2)).updateRepeatingRequest();
+        verify(camera2RequestRunner, times(1)).submitSingleRequest(any());
+
+        ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+                ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+        verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+        CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AE_MODE)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON);
+    }
+
+    @Test
+    public void triggerAf_singleRequestSent() {
+        camera2CameraControl.triggerAf();
+
+        ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+                ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+        verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+        CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AF_TRIGGER)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START);
+    }
+
+    @Test
+    public void triggerAePrecapture_singleRequestSent() {
+        camera2CameraControl.triggerAePrecapture();
+
+        ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+                ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+        verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+        CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+    }
+
+    @Test
+    public void cancelAfAeTrigger_singleRequestSent() {
+        camera2CameraControl.cancelAfAeTrigger(true, true);
+
+        ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+                ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+        verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+        CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AF_TRIGGER)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+    }
+
+    @Test
+    public void cancelAfTrigger_singleRequestSent() {
+        camera2CameraControl.cancelAfAeTrigger(true, false);
+
+        ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+                ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+        verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+        CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AF_TRIGGER)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER))
+                .isNull();
+    }
+
+    @Test
+    public void cancelAeTrigger_singleRequestSent() {
+        camera2CameraControl.cancelAfAeTrigger(false, true);
+
+        ArgumentCaptor<CaptureRequestConfiguration> argumentCaptor =
+                ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+        verify(camera2RequestRunner).submitSingleRequest(argumentCaptor.capture());
+        CaptureRequestConfiguration resultCaptureConfig = argumentCaptor.getValue();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AF_TRIGGER))
+                .isNull();
+        assertThat(
+                resultCaptureConfig
+                        .getCameraCharacteristics()
+                        .get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER)
+                        .getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksAndroidTest.java
new file mode 100644
index 0000000..e943452
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksAndroidTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Build;
+import android.view.Surface;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class Camera2CaptureSessionCaptureCallbacksAndroidTest {
+
+    @Test
+    public void comboCallbackInvokesConstituentCallbacks() {
+        CameraCaptureSession.CaptureCallback callback0 =
+                Mockito.mock(CameraCaptureSession.CaptureCallback.class);
+        CameraCaptureSession.CaptureCallback callback1 =
+                Mockito.mock(CameraCaptureSession.CaptureCallback.class);
+        CameraCaptureSession.CaptureCallback comboCallback =
+                Camera2CaptureSessionCaptureCallbacks.createComboCallback(callback0, callback1);
+        CameraCaptureSession session = Mockito.mock(CameraCaptureSession.class);
+        CaptureResult result = Mockito.mock(CaptureResult.class);
+        CaptureFailure failure = Mockito.mock(CaptureFailure.class);
+        Surface surface = Mockito.mock(Surface.class);
+        // CaptureRequest, TotalCaptureResult are final classes which cannot be mocked, and it is
+        // difficult to create fake instances without an actual Camera2 pipeline. Use null as a
+        // placeholder.
+        CaptureRequest request = null;
+        TotalCaptureResult totalResult = null;
+
+        if (Build.VERSION.SDK_INT >= 24) {
+            comboCallback.onCaptureBufferLost(session, request, surface, 1L);
+            verify(callback0, times(1)).onCaptureBufferLost(session, request, surface, 1L);
+            verify(callback1, times(1)).onCaptureBufferLost(session, request, surface, 1L);
+        }
+
+        comboCallback.onCaptureCompleted(session, request, totalResult);
+        verify(callback0, times(1)).onCaptureCompleted(session, request, totalResult);
+        verify(callback1, times(1)).onCaptureCompleted(session, request, totalResult);
+
+        comboCallback.onCaptureFailed(session, request, failure);
+        verify(callback0, times(1)).onCaptureFailed(session, request, failure);
+        verify(callback1, times(1)).onCaptureFailed(session, request, failure);
+
+        comboCallback.onCaptureProgressed(session, request, result);
+        verify(callback0, times(1)).onCaptureProgressed(session, request, result);
+        verify(callback1, times(1)).onCaptureProgressed(session, request, result);
+
+        comboCallback.onCaptureSequenceAborted(session, 1);
+        verify(callback0, times(1)).onCaptureSequenceAborted(session, 1);
+        verify(callback1, times(1)).onCaptureSequenceAborted(session, 1);
+
+        comboCallback.onCaptureSequenceCompleted(session, 1, 123L);
+        verify(callback0, times(1)).onCaptureSequenceCompleted(session, 1, 123L);
+        verify(callback1, times(1)).onCaptureSequenceCompleted(session, 1, 123L);
+
+        comboCallback.onCaptureStarted(session, request, 123L, 1L);
+        verify(callback0, times(1)).onCaptureStarted(session, request, 123L, 1L);
+        verify(callback1, times(1)).onCaptureStarted(session, request, 123L, 1L);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationAndroidTest.java
new file mode 100644
index 0000000..397f1cd
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationAndroidTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.util.Range;
+
+import androidx.camera.core.CameraCaptureSessionStateCallbacks;
+import androidx.camera.core.CameraDeviceStateCallbacks;
+import androidx.camera.testing.fakes.FakeConfiguration;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class Camera2ConfigurationAndroidTest {
+    private static final int INVALID_TEMPLATE_TYPE = -1;
+    private static final int INVALID_COLOR_CORRECTION_MODE = -1;
+    private static final CameraCaptureSession.CaptureCallback SESSION_CAPTURE_CALLBACK =
+            Camera2CaptureSessionCaptureCallbacks.createComboCallback();
+    private static final CameraCaptureSession.StateCallback SESSION_STATE_CALLBACK =
+            CameraCaptureSessionStateCallbacks.createNoOpCallback();
+    private static final CameraDevice.StateCallback DEVICE_STATE_CALLBACK =
+            CameraDeviceStateCallbacks.createNoOpCallback();
+
+    @Test
+    public void emptyConfigurationDoesNotContainTemplateType() {
+        FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        assertThat(config.getCaptureRequestTemplate(INVALID_TEMPLATE_TYPE))
+                .isEqualTo(INVALID_TEMPLATE_TYPE);
+    }
+
+    @Test
+    public void canExtendWithTemplateType() {
+        FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+        new Camera2Configuration.Extender(builder)
+                .setCaptureRequestTemplate(CameraDevice.TEMPLATE_PREVIEW);
+
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        assertThat(config.getCaptureRequestTemplate(INVALID_TEMPLATE_TYPE))
+                .isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+    }
+
+    @Test
+    public void canExtendWithSessionCaptureCallback() {
+        FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+        new Camera2Configuration.Extender(builder)
+                .setSessionCaptureCallback(SESSION_CAPTURE_CALLBACK);
+
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        assertThat(config.getSessionCaptureCallback(/*valueIfMissing=*/ null))
+                .isSameAs(SESSION_CAPTURE_CALLBACK);
+    }
+
+    @Test
+    public void canExtendWithSessionStateCallback() {
+        FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+        new Camera2Configuration.Extender(builder).setSessionStateCallback(SESSION_STATE_CALLBACK);
+
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        assertThat(config.getSessionStateCallback(/*valueIfMissing=*/ null))
+                .isSameAs(SESSION_STATE_CALLBACK);
+    }
+
+    @Test
+    public void canExtendWithDeviceStateCallback() {
+        FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(DEVICE_STATE_CALLBACK);
+
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        assertThat(config.getDeviceStateCallback(/*valueIfMissing=*/ null))
+                .isSameAs(DEVICE_STATE_CALLBACK);
+    }
+
+    @Test
+    public void canSetAndRetrieveCaptureRequestKeys() {
+        FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+        Range<Integer> fakeRange = new Range<>(0, 30);
+        new Camera2Configuration.Extender(builder)
+                .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+                .setCaptureRequestOption(
+                        CaptureRequest.COLOR_CORRECTION_MODE,
+                        CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        assertThat(
+                config.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+                        /*valueIfMissing=*/ null))
+                .isSameAs(fakeRange);
+        assertThat(
+                config.getCaptureRequestOption(
+                        CaptureRequest.COLOR_CORRECTION_MODE,
+                        INVALID_COLOR_CORRECTION_MODE))
+                .isEqualTo(CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+    }
+
+    @Test
+    public void canSetAndRetrieveCaptureRequestKeys_fromOptionIds() {
+        FakeConfiguration.Builder builder = new FakeConfiguration.Builder();
+
+        Range<Integer> fakeRange = new Range<>(0, 30);
+        new Camera2Configuration.Extender(builder)
+                .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+                .setCaptureRequestOption(
+                        CaptureRequest.COLOR_CORRECTION_MODE,
+                        CameraMetadata.COLOR_CORRECTION_MODE_FAST)
+                // Insert one non capture request option to ensure it gets filtered out
+                .setCaptureRequestTemplate(CameraDevice.TEMPLATE_PREVIEW);
+
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        config.findOptions(
+                "camera2.captureRequest.option",
+                option -> {
+                    // The token should be the capture request key
+                    assertThat(option.getToken())
+                            .isAnyOf(
+                                    CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+                                    CaptureRequest.COLOR_CORRECTION_MODE);
+                    return true;
+                });
+
+        assertThat(config.listOptions()).hasSize(3);
+    }
+
+    @Test
+    public void canSetAndRetrieveCaptureRequestKeys_byBuilder() {
+        Range<Integer> fakeRange = new Range<>(0, 30);
+        Camera2Configuration.Builder builder =
+                new Camera2Configuration.Builder()
+                        .setCaptureRequestOption(
+                                CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+                        .setCaptureRequestOption(
+                                CaptureRequest.COLOR_CORRECTION_MODE,
+                                CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+
+        Camera2Configuration config = new Camera2Configuration(builder.build());
+
+        assertThat(
+                config.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+                        /*valueIfMissing=*/ null))
+                .isSameAs(fakeRange);
+        assertThat(
+                config.getCaptureRequestOption(
+                        CaptureRequest.COLOR_CORRECTION_MODE,
+                        INVALID_COLOR_CORRECTION_MODE))
+                .isEqualTo(CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+    }
+
+    @Test
+    public void canInsertAllOptions_byBuilder() {
+        Range<Integer> fakeRange = new Range<>(0, 30);
+        Camera2Configuration.Builder builder =
+                new Camera2Configuration.Builder()
+                        .setCaptureRequestOption(
+                                CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fakeRange)
+                        .setCaptureRequestOption(
+                                CaptureRequest.COLOR_CORRECTION_MODE,
+                                CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+
+        Camera2Configuration config1 = new Camera2Configuration(builder.build());
+
+        Camera2Configuration.Builder builder2 =
+                new Camera2Configuration.Builder()
+                        .setCaptureRequestOption(
+                                CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
+                        .setCaptureRequestOption(
+                                CaptureRequest.CONTROL_AWB_MODE,
+                                CaptureRequest.CONTROL_AWB_MODE_AUTO)
+                        .insertAllOptions(config1);
+
+        Camera2Configuration config2 = new Camera2Configuration(builder2.build());
+
+        assertThat(
+                config2.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
+                        /*valueIfMissing=*/ null))
+                .isSameAs(fakeRange);
+        assertThat(
+                config2.getCaptureRequestOption(
+                        CaptureRequest.COLOR_CORRECTION_MODE,
+                        INVALID_COLOR_CORRECTION_MODE))
+                .isEqualTo(CameraMetadata.COLOR_CORRECTION_MODE_FAST);
+        assertThat(
+                config2.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, /*valueIfMissing=*/ 0))
+                .isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON);
+        assertThat(config2.getCaptureRequestOption(CaptureRequest.CONTROL_AWB_MODE, 0))
+                .isEqualTo(CaptureRequest.CONTROL_AWB_MODE_AUTO);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryAndroidTest.java
new file mode 100644
index 0000000..c7cd03d
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryAndroidTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraDevice;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.DeviceStateCallback;
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.SessionStateCallback;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraRepository;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseGroup;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+/**
+ * Contains tests for {@link androidx.camera.core.CameraRepository} which require an actual
+ * implementation to run.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class Camera2ImplCameraRepositoryAndroidTest {
+    private CameraRepository cameraRepository;
+    private UseCaseGroup useCaseGroup;
+    private FakeUseCaseConfiguration configuration;
+    private CallbackAttachingFakeUseCase useCase;
+    private CameraFactory cameraFactory;
+
+    private String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
+        try {
+            return cameraFactory.cameraIdForLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + lensFacing, e);
+        }
+    }
+
+    @Before
+    public void setUp() {
+        cameraRepository = new CameraRepository();
+        cameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
+        cameraRepository.init(cameraFactory);
+        useCaseGroup = new UseCaseGroup();
+        configuration =
+                new FakeUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK).build();
+        String cameraId = getCameraIdForLensFacingUnchecked(configuration.getLensFacing());
+        useCase = new CallbackAttachingFakeUseCase(configuration, cameraId);
+        useCaseGroup.addUseCase(useCase);
+    }
+
+    @Test(timeout = 5000)
+    public void cameraDeviceCallsAreForwardedToCallback() throws InterruptedException {
+        cameraRepository.onGroupActive(useCaseGroup);
+
+        // Wait for the CameraDevice.onOpened callback.
+        useCase.deviceStateCallback.waitForOnOpened(1);
+
+        cameraRepository.onGroupInactive(useCaseGroup);
+
+        // Wait for the CameraDevice.onClosed callback.
+        useCase.deviceStateCallback.waitForOnClosed(1);
+    }
+
+    @Test(timeout = 5000)
+    public void cameraSessionCallsAreForwardedToCallback() throws InterruptedException {
+        useCase.addStateChangeListener(
+                cameraRepository.getCamera(
+                        getCameraIdForLensFacingUnchecked(configuration.getLensFacing())));
+        useCase.doNotifyActive();
+        cameraRepository.onGroupActive(useCaseGroup);
+
+        // Wait for the CameraCaptureSession.onConfigured callback.
+        useCase.sessionStateCallback.waitForOnConfigured(1);
+
+        // Camera doesn't currently call CaptureSession.release(), because it is recommended that
+        // we don't explicitly call CameraCaptureSession.close(). Rather, we rely on another
+        // CameraCaptureSession to get opened. See
+        // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession
+        // .html#close()
+    }
+
+    /** A fake use case which attaches to a camera with various callbacks. */
+    private static class CallbackAttachingFakeUseCase extends FakeUseCase {
+        private final DeviceStateCallback deviceStateCallback = new DeviceStateCallback();
+        private final SessionStateCallback sessionStateCallback = new SessionStateCallback();
+        private final SurfaceTexture surfaceTexture = new SurfaceTexture(0);
+
+        CallbackAttachingFakeUseCase(FakeUseCaseConfiguration configuration, String cameraId) {
+            super(configuration);
+
+            SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            builder.addSurface(new ImmediateSurface(new Surface(surfaceTexture)));
+            builder.setDeviceStateCallback(deviceStateCallback);
+            builder.setSessionStateCallback(sessionStateCallback);
+
+            attachToCamera(cameraId, builder.build());
+        }
+
+        @Override
+        protected Map<String, Size> onSuggestedResolutionUpdated(
+                Map<String, Size> suggestedResolutionMap) {
+            return suggestedResolutionMap;
+        }
+
+        void doNotifyActive() {
+            super.notifyActive();
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXAndroidTest.java
new file mode 100644
index 0000000..5569401
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXAndroidTest.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.camera2.CameraDevice;
+import android.os.HandlerThread;
+
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.DeviceStateCallback;
+import androidx.camera.camera2.SemaphoreReleasingCamera2Callbacks.SessionCaptureCallback;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Contains tests for {@link androidx.camera.core.CameraX} which require an actual implementation to
+ * run.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class Camera2ImplCameraXAndroidTest {
+    private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
+    private final MutableLiveData<Long> analysisResult = new MutableLiveData<>();
+    private final ImageAnalysisUseCase.Analyzer imageAnalyzer =
+            (image, rotationDegrees) -> {
+                analysisResult.postValue(image.getTimestamp());
+            };
+    private FakeLifecycleOwner lifecycle;
+    private HandlerThread handlerThread;
+
+    private CameraDevice.StateCallback mockStateCallback;
+
+    private static Observer<Long> createCountIncrementingObserver(final AtomicLong counter) {
+        return value -> {
+            counter.incrementAndGet();
+        };
+    }
+
+    @Before
+    public void setUp() {
+        Context context = ApplicationProvider.getApplicationContext();
+        CameraX.init(context, Camera2AppConfiguration.create(context));
+        lifecycle = new FakeLifecycleOwner();
+        handlerThread = new HandlerThread("ErrorHandlerThread");
+        handlerThread.start();
+        mockStateCallback = Mockito.mock(CameraDevice.StateCallback.class);
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        CameraX.unbindAll();
+        handlerThread.quitSafely();
+
+        // Wait some time for the cameras to close. We need the cameras to close to bring CameraX
+        // back
+        // to the initial state.
+        Thread.sleep(3000);
+    }
+
+    @Test
+    public void lifecycleResume_opensCameraAndStreamsFrames() throws InterruptedException {
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder()
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(lifecycle, useCase);
+        final AtomicLong observedCount = new AtomicLong(0);
+        useCase.setAnalyzer(imageAnalyzer);
+        analysisResult.observe(lifecycle, createCountIncrementingObserver(observedCount));
+
+        lifecycle.startAndResume();
+
+        // Wait a little bit for the camera to open and stream frames.
+        Thread.sleep(5000);
+
+        // Some frames should have been observed.
+        assertThat(observedCount.get()).isAtLeast(10L);
+    }
+
+    @Test
+    public void removedUseCase_doesNotStreamWhenLifecycleResumes() throws InterruptedException {
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder()
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(lifecycle, useCase);
+        final AtomicLong observedCount = new AtomicLong(0);
+        useCase.setAnalyzer(imageAnalyzer);
+        analysisResult.observe(lifecycle, createCountIncrementingObserver(observedCount));
+        assertThat(observedCount.get()).isEqualTo(0);
+
+        CameraX.unbind(useCase);
+
+        lifecycle.startAndResume();
+
+        // Wait a little bit for the camera to open and stream frames.
+        Thread.sleep(5000);
+
+        // No frames should have been observed.
+        assertThat(observedCount.get()).isEqualTo(0);
+    }
+
+    @Test
+    public void lifecyclePause_closesCameraAndStopsStreamingFrames() throws InterruptedException {
+        ImageAnalysisUseCaseConfiguration.Builder configurationBuilder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        DeviceStateCallback deviceStateCallback = new DeviceStateCallback();
+        SessionCaptureCallback sessionCaptureCallback = new SessionCaptureCallback();
+        new Camera2Configuration.Extender(configurationBuilder)
+                .setDeviceStateCallback(deviceStateCallback)
+                .setSessionCaptureCallback(sessionCaptureCallback);
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configurationBuilder.build());
+        CameraX.bindToLifecycle(lifecycle, useCase);
+        final AtomicLong observedCount = new AtomicLong(0);
+        useCase.setAnalyzer(imageAnalyzer);
+        analysisResult.observe(lifecycle, createCountIncrementingObserver(observedCount));
+
+        lifecycle.startAndResume();
+
+        // Wait a little bit for the camera to open and stream frames.
+        sessionCaptureCallback.waitForOnCaptureCompleted(5);
+
+        lifecycle.pauseAndStop();
+
+        // Wait a little bit for the camera to close.
+        deviceStateCallback.waitForOnClosed(1);
+
+        final Long firstObservedCount = observedCount.get();
+        assertThat(firstObservedCount).isGreaterThan(1L);
+
+        // Stay in idle state for a while.
+        Thread.sleep(5000);
+
+        // Additional frames should not be observed.
+        final Long secondObservedCount = observedCount.get();
+        assertThat(secondObservedCount).isEqualTo(firstObservedCount);
+    }
+
+    @Test
+    public void bind_opensCamera() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration = builder.build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(lifecycle, useCase);
+        lifecycle.startAndResume();
+
+        verify(mockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+    }
+
+    @Test
+    public void unbindAll_closesAllCameras() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration = builder.build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(lifecycle, useCase);
+        lifecycle.startAndResume();
+
+        CameraX.unbindAll();
+
+        verify(mockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+    }
+
+    @Test
+    public void unbindAllAssociatedUseCase_closesCamera() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration = builder.build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(lifecycle, useCase);
+        lifecycle.startAndResume();
+
+        CameraX.unbind(useCase);
+
+        verify(mockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+    }
+
+    @Test
+    public void unbindPartialAssociatedUseCase_doesNotCloseCamera() throws InterruptedException {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration0 = builder.build();
+        ImageAnalysisUseCase useCase0 = new ImageAnalysisUseCase(configuration0);
+
+        ImageAnalysisUseCaseConfiguration configuration1 =
+                new ImageAnalysisUseCaseConfiguration.Builder()
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .build();
+        ImageAnalysisUseCase useCase1 = new ImageAnalysisUseCase(configuration1);
+
+        CameraX.bindToLifecycle(lifecycle, useCase0, useCase1);
+        lifecycle.startAndResume();
+
+        CameraX.unbind(useCase1);
+
+        Thread.sleep(3000);
+
+        verify(mockStateCallback, never()).onClosed(any(CameraDevice.class));
+    }
+
+    @Test
+    public void unbindAllAssociatedUseCaseInParts_ClosesCamera() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration0 = builder.build();
+        ImageAnalysisUseCase useCase0 = new ImageAnalysisUseCase(configuration0);
+
+        ImageAnalysisUseCaseConfiguration configuration1 =
+                new ImageAnalysisUseCaseConfiguration.Builder()
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .build();
+        ImageAnalysisUseCase useCase1 = new ImageAnalysisUseCase(configuration1);
+
+        CameraX.bindToLifecycle(lifecycle, useCase0, useCase1);
+        lifecycle.startAndResume();
+
+        CameraX.unbind(useCase0);
+        CameraX.unbind(useCase1);
+
+        verify(mockStateCallback, timeout(3000).times(1)).onClosed(any(CameraDevice.class));
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerAndroidTest.java
new file mode 100644
index 0000000..fe6ab47
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerAndroidTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ProviderInfo;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+import androidx.camera.core.FakeActivity;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link Camera2Initializer}.
+ *
+ * <p>The default ProviderTestCase2 cannot be used, because its mock Context returns null when we
+ * call Context.getSystemService(Context.CAMERA_SERVICE). We need to be able to get the camera
+ * service to test CameraX initialization. Therefore, we copy the test strategy employed in
+ * CalendarProvider2Test, where we override a method of the mock Context.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class Camera2InitializerAndroidTest {
+    private static final String TEST_AUTHORITY = "androidx.camera.core";
+    @Rule
+    public ActivityTestRule<FakeActivity> activityRule =
+            new ActivityTestRule<>(
+                    FakeActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
+    private Context appContext;
+    private Context testContext;
+    private ProviderInfo providerInfo;
+    private Camera2Initializer provider;
+
+    @Before
+    public void setUp() {
+        appContext = ApplicationProvider.getApplicationContext();
+        Context targetContextWrapper =
+                new RenamingDelegatingContext(new MockContext(), appContext, "test.");
+        MockContentResolver resolver = new MockContentResolver();
+        testContext =
+                new IsolatedContext(resolver, targetContextWrapper) {
+                    @Override
+                    public Object getSystemService(String name) {
+                        if (Context.CAMERA_SERVICE.equals(name)
+                                || Context.WINDOW_SERVICE.equals(name)) {
+                            return appContext.getSystemService(name);
+                        }
+                        return super.getSystemService(name);
+                    }
+                };
+
+        providerInfo = new ProviderInfo();
+        providerInfo.authority = TEST_AUTHORITY;
+        provider = new Camera2Initializer();
+        provider.attachInfo(testContext, providerInfo);
+
+        resolver.addProvider(TEST_AUTHORITY, provider);
+    }
+
+    @Test
+    public void initializerIsConnectedToContext() {
+        assertThat(provider.getContext()).isSameAs(testContext);
+    }
+
+    @Test
+    public void cameraXIsInitialized_beforeActivityIsCreated() {
+        activityRule.launchActivity(new Intent(appContext, FakeActivity.class));
+        FakeActivity activity = activityRule.getActivity();
+
+        assertThat(activity.isCameraXInitializedAtOnCreate()).isTrue();
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraAndroidTest.java
new file mode 100644
index 0000000..94ecbd3
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraAndroidTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraDevice;
+import android.media.ImageReader;
+import android.media.ImageReader.OnImageAvailableListener;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class CameraAndroidTest {
+    private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
+    static CameraFactory cameraFactory;
+
+    BaseCamera camera;
+
+    UseCase fakeUseCase;
+    OnImageAvailableListener mockOnImageAvailableListener;
+    String cameraId;
+
+    private static String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
+        try {
+            return cameraFactory.cameraIdForLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + lensFacing, e);
+        }
+    }
+
+    @BeforeClass
+    public static void classSetup() {
+        cameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
+    }
+
+    @Before
+    public void setup() {
+        mockOnImageAvailableListener = Mockito.mock(ImageReader.OnImageAvailableListener.class);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .build();
+        cameraId = getCameraIdForLensFacingUnchecked(DEFAULT_LENS_FACING);
+        fakeUseCase = new UseCase(configuration, mockOnImageAvailableListener);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, new Size(640, 480));
+        fakeUseCase.updateSuggestedResolution(suggestedResolutionMap);
+
+        camera = cameraFactory.getCamera(cameraId);
+    }
+
+    @After
+    public void teardown() throws InterruptedException {
+        // 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 (camera != null) {
+            camera.release();
+            camera = null;
+        }
+
+        // Wait a little bit for the camera device to close.
+        // TODO(b/111991758): Listen for the close signal when it becomes available.
+        Thread.sleep(2000);
+
+        if (fakeUseCase != null) {
+            fakeUseCase.close();
+            fakeUseCase = null;
+        }
+    }
+
+    @Test
+    public void onlineUseCase() {
+        camera.open();
+
+        camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+
+        verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+
+        camera.release();
+    }
+
+    @Test
+    public void activeUseCase() {
+        camera.open();
+
+        camera.onUseCaseActive(fakeUseCase);
+
+        verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+
+        camera.release();
+    }
+
+    @Test
+    public void onlineAndActiveUseCase() throws InterruptedException {
+        camera.open();
+
+        camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+        camera.onUseCaseActive(fakeUseCase);
+
+        verify(mockOnImageAvailableListener, timeout(4000).atLeastOnce())
+                .onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void removeOnlineUseCase() {
+        camera.open();
+
+        camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+        camera.removeOnlineUseCase(Collections.singletonList(fakeUseCase));
+        camera.onUseCaseActive(fakeUseCase);
+
+        verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void unopenedCamera() {
+        camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+        camera.removeOnlineUseCase(Collections.singletonList(fakeUseCase));
+
+        verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void closedCamera() {
+        camera.open();
+
+        camera.close();
+        camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+        camera.removeOnlineUseCase(Collections.singletonList(fakeUseCase));
+
+        verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void releaseUnopenedCamera() {
+        camera.release();
+        camera.open();
+
+        camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+        camera.onUseCaseActive(fakeUseCase);
+
+        verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void releasedOpenedCamera() {
+        camera.release();
+        camera.open();
+
+        camera.addOnlineUseCase(Collections.singletonList(fakeUseCase));
+        camera.onUseCaseActive(fakeUseCase);
+
+        verify(mockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    private static class UseCase extends FakeUseCase {
+        private final ImageReader.OnImageAvailableListener imageAvailableListener;
+        HandlerThread handlerThread = new HandlerThread("HandlerThread");
+        Handler handler;
+        ImageReader imageReader;
+
+        UseCase(
+                FakeUseCaseConfiguration configuration,
+                ImageReader.OnImageAvailableListener listener) {
+            super(configuration);
+            imageAvailableListener = listener;
+            handlerThread.start();
+            handler = new Handler(handlerThread.getLooper());
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            String cameraId = getCameraIdForLensFacingUnchecked(configuration.getLensFacing());
+            suggestedResolutionMap.put(cameraId, new Size(640, 480));
+            updateSuggestedResolution(suggestedResolutionMap);
+        }
+
+        void close() {
+            handler.removeCallbacksAndMessages(null);
+            handlerThread.quitSafely();
+            if (imageReader != null) {
+                imageReader.close();
+            }
+        }
+
+        @Override
+        protected Map<String, Size> onSuggestedResolutionUpdated(
+                Map<String, Size> suggestedResolutionMap) {
+            LensFacing lensFacing =
+                    ((CameraDeviceConfiguration) getUseCaseConfiguration()).getLensFacing();
+            String cameraId = getCameraIdForLensFacingUnchecked(lensFacing);
+            Size resolution = suggestedResolutionMap.get(cameraId);
+            SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            imageReader =
+                    ImageReader.newInstance(
+                            resolution.getWidth(),
+                            resolution.getHeight(),
+                            ImageFormat.YUV_420_888, /*maxImages*/
+                            2);
+            imageReader.setOnImageAvailableListener(imageAvailableListener, handler);
+            builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+
+            attachToCamera(cameraId, builder.build());
+            return suggestedResolutionMap;
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraCaptureCallbackAdapterAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraCaptureCallbackAdapterAndroidTest.java
new file mode 100644
index 0000000..ca03f51
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraCaptureCallbackAdapterAndroidTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureRequest;
+import android.view.Surface;
+
+import androidx.camera.core.CameraCaptureCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraCaptureCallbackAdapterAndroidTest {
+
+    private CameraCaptureCallback cameraCaptureCallback;
+    private CameraCaptureSession cameraCaptureSession;
+    private CaptureRequest captureRequest;
+    private CameraCaptureCallbackAdapter cameraCaptureCallbackAdapter;
+
+    @Before
+    public void setUp() {
+        cameraCaptureCallback = Mockito.mock(CameraCaptureCallback.class);
+        cameraCaptureSession = Mockito.mock(CameraCaptureSession.class);
+        // Mockito can't mock final class
+        captureRequest = null;
+        Mockito.mock(Surface.class);
+        cameraCaptureCallbackAdapter = new CameraCaptureCallbackAdapter(cameraCaptureCallback);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void createCameraCaptureCallbackAdapterWithNullArgument() {
+        new CameraCaptureCallbackAdapter(null);
+    }
+
+    @Test
+    public void onCaptureCompleted() {
+        cameraCaptureCallbackAdapter.onCaptureCompleted(
+                cameraCaptureSession, captureRequest, any());
+        verify(cameraCaptureCallback, times(1)).onCaptureCompleted(any());
+    }
+
+    @Test
+    public void onCaptureFailed() {
+        cameraCaptureCallbackAdapter.onCaptureFailed(cameraCaptureSession, captureRequest, any());
+        verify(cameraCaptureCallback, times(1)).onCaptureFailed(any());
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerAndroidTest.java
new file mode 100644
index 0000000..cfa4aad
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerAndroidTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CaptureCallbackContainerAndroidTest {
+
+    @Test(expected = NullPointerException.class)
+    public void createCaptureCallbackContainer_withNullArgument() {
+        CaptureCallbackContainer.create(null);
+    }
+
+    @Test
+    public void getCaptureCallback() {
+        CaptureCallback captureCallback = Mockito.mock(CaptureCallback.class);
+        CaptureCallbackContainer callbackContainer =
+                CaptureCallbackContainer.create(captureCallback);
+        assertThat(callbackContainer.getCaptureCallback()).isEqualTo(captureCallback);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackConverterAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackConverterAndroidTest.java
new file mode 100644
index 0000000..f5c26be
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackConverterAndroidTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CaptureCallbackConverterAndroidTest {
+
+    @Test
+    public void toCaptureCallback() {
+        CameraCaptureCallback cameraCallback = Mockito.mock(CameraCaptureCallback.class);
+        CaptureCallback callback = CaptureCallbackConverter.toCaptureCallback(cameraCallback);
+        callback.onCaptureCompleted(null, null, null);
+        verify(cameraCallback, times(1)).onCaptureCompleted(any());
+    }
+
+    @Test
+    public void toCaptureCallback_withNullArgument() {
+        CaptureCallback callback = CaptureCallbackConverter.toCaptureCallback(null);
+        assertThat(callback).isNull();
+    }
+
+    @Test
+    public void toCaptureCallback_withCaptureCallbackContainer() {
+        CaptureCallback actualCallback = Mockito.mock(CaptureCallback.class);
+        CaptureCallbackContainer callbackContainer =
+                CaptureCallbackContainer.create(actualCallback);
+        CaptureCallback callback = CaptureCallbackConverter.toCaptureCallback(callbackContainer);
+        callback.onCaptureCompleted(null, null, null);
+        verify(actualCallback, times(1)).onCaptureCompleted(any(), any(), any());
+    }
+
+    @Test
+    public void toCaptureCallback_withComboCameraCallback() {
+        CameraCaptureCallback cameraCallback1 = Mockito.mock(CameraCaptureCallback.class);
+        CameraCaptureCallback cameraCallback2 = Mockito.mock(CameraCaptureCallback.class);
+        CaptureCallback cameraCallback3 = Mockito.mock(CaptureCallback.class);
+
+        CaptureCallback callback =
+                CaptureCallbackConverter.toCaptureCallback(
+                        CameraCaptureCallbacks.createComboCallback(
+                                cameraCallback1,
+                                CameraCaptureCallbacks.createComboCallback(
+                                        cameraCallback2,
+                                        CaptureCallbackContainer.create(cameraCallback3))));
+
+        callback.onCaptureCompleted(null, null, null);
+        verify(cameraCallback1, times(1)).onCaptureCompleted(any());
+        verify(cameraCallback2, times(1)).onCaptureCompleted(any());
+        verify(cameraCallback3, times(1)).onCaptureCompleted(any(), any(), any());
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionAndroidTest.java
new file mode 100644
index 0000000..4f8e7d0d
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionAndroidTest.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.media.Image;
+import android.media.ImageReader;
+import android.media.ImageReader.OnImageAvailableListener;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.CaptureSession.State;
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks;
+import androidx.camera.core.CameraCaptureResult;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for {@link CaptureSession}. This requires an environment where a valid {@link
+ * android.hardware.camera2.CameraDevice} can be opened since it is used to open a {@link
+ * android.hardware.camera2.CaptureRequest}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class CaptureSessionAndroidTest {
+    private CaptureSessionTestParameters testParameters0;
+    private CaptureSessionTestParameters testParameters1;
+
+    private CameraDevice cameraDevice;
+
+    @Before
+    public void setup() throws CameraAccessException, InterruptedException {
+        testParameters0 = new CaptureSessionTestParameters("testParameters0");
+        testParameters1 = new CaptureSessionTestParameters("testParameters1");
+        cameraDevice = CameraUtil.getCameraDevice();
+    }
+
+    @After
+    public void tearDown() {
+        testParameters0.tearDown();
+        testParameters1.tearDown();
+        CameraUtil.releaseCameraDevice(cameraDevice);
+    }
+
+    @Test
+    public void setCaptureSessionSucceed() {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+        assertThat(captureSession.getSessionConfiguration())
+                .isEqualTo(testParameters0.sessionConfiguration);
+    }
+
+    @Test
+    public void setCaptureSessionOnClosedSession_throwsException() {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        SessionConfiguration newSessionConfiguration = testParameters0.sessionConfiguration;
+
+        captureSession.close();
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> captureSession.setSessionConfiguration(newSessionConfiguration));
+    }
+
+    @Test
+    public void openCaptureSessionSucceed() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+        captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+        testParameters0.waitForData();
+
+        assertThat(captureSession.getState()).isEqualTo(State.OPENED);
+
+        // StateCallback.onConfigured() should be called to signal the session is configured.
+        verify(testParameters0.sessionStateCallback, times(1))
+                .onConfigured(any(CameraCaptureSession.class));
+
+        // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+        verify(testParameters0.sessionCameraCaptureCallback, timeout(3000).atLeast(1))
+                .onCaptureCompleted(any());
+    }
+
+    @Test
+    public void closeUnopenedSession() {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+        captureSession.close();
+
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+    }
+
+    @Test
+    public void releaseUnopenedSession() {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+        captureSession.release();
+
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+    }
+
+    @Test
+    public void closeOpenedSession() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+        captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+        captureSession.close();
+
+        Thread.sleep(3000);
+        // Session should not get released until triggered by another session opening
+        assertThat(captureSession.getState()).isEqualTo(State.CLOSED);
+    }
+
+    @Test
+    public void releaseOpenedSession() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+        captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+        captureSession.release();
+
+        Thread.sleep(3000);
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+
+        // StateCallback.onClosed() should be called to signal the session is closed.
+        verify(testParameters0.sessionStateCallback, times(1))
+                .onClosed(any(CameraCaptureSession.class));
+    }
+
+    @Test
+    public void openSecondSession() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+        // First session is opened
+        captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+        captureSession.close();
+
+        // Open second session, which should cause first one to be released
+        CaptureSession captureSession1 = new CaptureSession(testParameters1.handler);
+        captureSession1.setSessionConfiguration(testParameters1.sessionConfiguration);
+        captureSession1.open(testParameters1.sessionConfiguration, cameraDevice);
+
+        testParameters1.waitForData();
+
+        assertThat(captureSession1.getState()).isEqualTo(State.OPENED);
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+
+        // First session should have StateCallback.onConfigured(), onClosed() calls.
+        verify(testParameters0.sessionStateCallback, times(1))
+                .onConfigured(any(CameraCaptureSession.class));
+        verify(testParameters0.sessionStateCallback, times(1))
+                .onClosed(any(CameraCaptureSession.class));
+
+        // Second session should have StateCallback.onConfigured() call.
+        verify(testParameters1.sessionStateCallback, times(1))
+                .onConfigured(any(CameraCaptureSession.class));
+
+        // Second session should have CameraCaptureCallback.onCaptureCompleted() call.
+        verify(testParameters1.sessionCameraCaptureCallback, timeout(3000).atLeast(1))
+                .onCaptureCompleted(any());
+    }
+
+    @Test
+    public void issueSingleCaptureRequest() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+        captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+        testParameters0.waitForData();
+
+        assertThat(captureSession.getState()).isEqualTo(State.OPENED);
+
+        captureSession.issueSingleCaptureRequest(testParameters0.captureRequestConfiguration);
+
+        testParameters0.waitForCameraCaptureCallback();
+
+        // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+        verify(testParameters0.cameraCaptureCallback, timeout(3000).times(1))
+                .onCaptureCompleted(any());
+    }
+
+    @Test
+    public void issueSingleCaptureRequestBeforeCaptureSessionOpened()
+            throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+        captureSession.setSessionConfiguration(testParameters0.sessionConfiguration);
+
+        captureSession.issueSingleCaptureRequest(testParameters0.captureRequestConfiguration);
+        captureSession.open(testParameters0.sessionConfiguration, cameraDevice);
+
+        testParameters0.waitForCameraCaptureCallback();
+
+        // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+        verify(testParameters0.cameraCaptureCallback, timeout(3000).times(1))
+                .onCaptureCompleted(any());
+    }
+
+    @Test
+    public void issueSingleCaptureRequestOnClosedSession_throwsException() {
+        CaptureSession captureSession = new CaptureSession(testParameters0.handler);
+
+        captureSession.close();
+
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        captureSession.issueSingleCaptureRequest(
+                                testParameters0.captureRequestConfiguration));
+    }
+
+    /**
+     * Collection of parameters required for setting a {@link CaptureSession} and wait for it to
+     * produce data.
+     */
+    private static class CaptureSessionTestParameters {
+        private static final int TIME_TO_WAIT_FOR_DATA_SECONDS = 3;
+        /** Thread for all asynchronous calls. */
+        private final HandlerThread handlerThread;
+        /** Handler for all asynchronous calls. */
+        private final Handler handler;
+        /** Latch to wait for first image data to appear. */
+        private final CountDownLatch dataLatch = new CountDownLatch(1);
+
+        /** Latch to wait for camera capture callback to be invoked. */
+        private final CountDownLatch cameraCaptureCallbackLatch = new CountDownLatch(1);
+
+        /** Image reader that unlocks the latch waiting for the first image data to appear. */
+        private final OnImageAvailableListener onImageAvailableListener =
+                reader -> {
+                    Image image = reader.acquireNextImage();
+                    if (image != null) {
+                        image.close();
+                        dataLatch.countDown();
+                    }
+                };
+
+        private final ImageReader imageReader;
+        private final SessionConfiguration sessionConfiguration;
+        private final CaptureRequestConfiguration captureRequestConfiguration;
+
+        private final CameraCaptureSession.StateCallback sessionStateCallback =
+                Mockito.mock(CameraCaptureSession.StateCallback.class);
+        private final CameraCaptureCallback sessionCameraCaptureCallback =
+                Mockito.mock(CameraCaptureCallback.class);
+        private final CameraCaptureCallback cameraCaptureCallback =
+                Mockito.mock(CameraCaptureCallback.class);
+
+        /**
+         * A composite capture callback that dispatches callbacks to both mock and real callbacks.
+         * The mock callback is used to verify the callback result. The real callback is used to
+         * unlock the latch waiting.
+         */
+        private final CameraCaptureCallback comboCameraCaptureCallback =
+                CameraCaptureCallbacks.createComboCallback(
+                        cameraCaptureCallback,
+                        new CameraCaptureCallback() {
+                            @Override
+                            public void onCaptureCompleted(@NonNull CameraCaptureResult result) {
+                                cameraCaptureCallbackLatch.countDown();
+                            }
+                        });
+
+        CaptureSessionTestParameters(String name) {
+            handlerThread = new HandlerThread(name);
+            handlerThread.start();
+            handler = new Handler(handlerThread.getLooper());
+
+            imageReader =
+                    ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, /*maxImages*/ 2);
+            imageReader.setOnImageAvailableListener(onImageAvailableListener, handler);
+
+            SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+            builder.setSessionStateCallback(sessionStateCallback);
+            builder.setCameraCaptureCallback(sessionCameraCaptureCallback);
+
+            sessionConfiguration = builder.build();
+
+            CaptureRequestConfiguration.Builder captureRequestConfigBuilder =
+                    new CaptureRequestConfiguration.Builder();
+            captureRequestConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            captureRequestConfigBuilder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+            captureRequestConfigBuilder.setCameraCaptureCallback(comboCameraCaptureCallback);
+
+            captureRequestConfiguration = captureRequestConfigBuilder.build();
+        }
+
+        /**
+         * Wait for data to get produced by the session.
+         *
+         * @throws InterruptedException if data is not produced after a set amount of time
+         */
+        void waitForData() throws InterruptedException {
+            dataLatch.await(TIME_TO_WAIT_FOR_DATA_SECONDS, TimeUnit.SECONDS);
+        }
+
+        void waitForCameraCaptureCallback() throws InterruptedException {
+            cameraCaptureCallbackLatch.await(TIME_TO_WAIT_FOR_DATA_SECONDS, TimeUnit.SECONDS);
+        }
+
+        /** Clean up resources. */
+        void tearDown() {
+            imageReader.close();
+            handlerThread.quitSafely();
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/FakeRepeatingUseCase.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/FakeRepeatingUseCase.java
new file mode 100644
index 0000000..a7534dd
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/FakeRepeatingUseCase.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraDevice;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseConfiguration;
+
+import java.util.Map;
+
+/** A fake {@link FakeUseCase} which contain a repeating surface. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class FakeRepeatingUseCase extends FakeUseCase {
+
+    /** The repeating surface. */
+    private final ImageReader imageReader =
+            ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2);
+
+    public FakeRepeatingUseCase(FakeUseCaseConfiguration configuration) {
+        super(configuration);
+
+        FakeUseCaseConfiguration configWithDefaults =
+                (FakeUseCaseConfiguration) getUseCaseConfiguration();
+        imageReader.setOnImageAvailableListener(
+                imageReader -> {
+                    Image image = imageReader.acquireLatestImage();
+                    if (image != null) {
+                        image.close();
+                    }
+                },
+                new Handler(Looper.getMainLooper()));
+
+        SessionConfiguration.Builder builder =
+                SessionConfiguration.Builder.createFrom(configWithDefaults);
+        builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+        try {
+            String cameraId = CameraX.getCameraWithLensFacing(configWithDefaults.getLensFacing());
+            attachToCamera(cameraId, builder.build());
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing "
+                            + configWithDefaults.getLensFacing(),
+                    e);
+        }
+    }
+
+    @Override
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        return new FakeUseCaseConfiguration.Builder()
+                .setLensFacing(LensFacing.BACK)
+                .setOptionUnpacker(
+                        (useCaseConfig, sessionConfigBuilder) -> {
+                            // Set the template since it is currently required by implementation
+                            sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+                        });
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        imageReader.close();
+    }
+
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        return suggestedResolutionMap;
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseAndroidTest.java
new file mode 100644
index 0000000..5852787
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseAndroidTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.BaseUseCase.StateChangeListener;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCase.Analyzer;
+import androidx.camera.core.ImageAnalysisUseCase.ImageReaderMode;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageAnalysisUseCaseAndroidTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(640, 480);
+    private final ImageAnalysisUseCaseConfiguration defaultConfiguration =
+            ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration();
+    private final StateChangeListener mockListener = Mockito.mock(StateChangeListener.class);
+    private final Analyzer mockAnalyzer = Mockito.mock(Analyzer.class);
+    private Set<ImageProperties> analysisResults;
+    private Analyzer analyzer;
+    private BaseCamera camera;
+    private HandlerThread handlerThread;
+    private Handler handler;
+    private Semaphore analysisResultsSemaphore;
+    private String cameraId;
+
+    @Before
+    public void setUp() {
+        analysisResults = new HashSet<>();
+        analysisResultsSemaphore = new Semaphore(/*permits=*/ 0);
+        analyzer =
+                (image, rotationDegrees) -> {
+                    analysisResults.add(new ImageProperties(image, rotationDegrees));
+                    analysisResultsSemaphore.release();
+                };
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration config = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = config.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+        }
+        camera = cameraFactory.getCamera(cameraId);
+
+        CameraX.init(context, config);
+
+        handlerThread = new HandlerThread("AnalysisThread");
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        handlerThread.quitSafely();
+        camera.release();
+    }
+
+    @Test
+    public void analyzerCanBeSetAndRetrieved() {
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+
+        Analyzer initialAnalyzer = useCase.getAnalyzer();
+
+        useCase.setAnalyzer(mockAnalyzer);
+
+        Analyzer retrievedAnalyzer = useCase.getAnalyzer();
+
+        // The observer is bound to the lifecycle.
+        assertThat(initialAnalyzer).isNull();
+        assertThat(retrievedAnalyzer).isSameAs(mockAnalyzer);
+    }
+
+    @Test
+    public void becomesActive_whenHasAnalyzer() {
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(mockListener);
+
+        useCase.setAnalyzer(mockAnalyzer);
+
+        verify(mockListener, times(1)).onUseCaseActive(useCase);
+    }
+
+    @Test
+    public void becomesInactive_whenNoAnalyzer() {
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(mockListener);
+        useCase.setAnalyzer(mockAnalyzer);
+        useCase.removeAnalyzer();
+
+        verify(mockListener, times(1)).onUseCaseInactive(useCase);
+    }
+
+    @Test(timeout = 5000)
+    public void analyzerAnalyzesImages_whenCameraIsOpen()
+            throws InterruptedException, CameraInfoUnavailableException {
+        final int imageFormat = ImageFormat.YUV_420_888;
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase);
+        useCase.setAnalyzer(analyzer);
+
+        int sensorRotation = CameraX.getCameraInfo(cameraId).getSensorRotationDegrees();
+        // The frames should have properties which match the configuration.
+        for (ImageProperties properties : analysisResults) {
+            assertThat(properties.resolution).isEqualTo(DEFAULT_RESOLUTION);
+            assertThat(properties.format).isEqualTo(imageFormat);
+            assertThat(properties.rotationDegrees).isEqualTo(sensorRotation);
+        }
+    }
+
+    @Test
+    public void analyzerDoesNotAnalyzeImages_whenCameraIsNotOpen() throws InterruptedException {
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.setAnalyzer(analyzer);
+        // Keep the lifecycle in an inactive state.
+        // Wait a little while for frames to be analyzed.
+        analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
+
+        // No frames should have been analyzed.
+        assertThat(analysisResults).isEmpty();
+    }
+
+    @Test(timeout = 5000)
+    public void updateSessionConfigurationWithSuggestedResolution() throws InterruptedException {
+        final int imageFormat = ImageFormat.YUV_420_888;
+        final Size[] sizes = {new Size(1280, 720), new Size(640, 480)};
+
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        useCase.setAnalyzer(analyzer);
+
+        for (Size size : sizes) {
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            suggestedResolutionMap.put(cameraId, size);
+            useCase.updateSuggestedResolution(suggestedResolutionMap);
+            CameraUtil.openCameraWithUseCase(camera, useCase);
+
+            // Clear previous results
+            analysisResults.clear();
+            // Wait a little while for frames to be analyzed.
+            analysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
+
+            // The frames should have properties which match the configuration.
+            for (ImageProperties properties : analysisResults) {
+                assertThat(properties.resolution).isEqualTo(size);
+                assertThat(properties.format).isEqualTo(imageFormat);
+            }
+
+            // Detach use case from camera device to run next resolution setting
+            CameraUtil.detachUseCaseFromCamera(camera, useCase);
+        }
+    }
+
+    @Test
+    public void defaultsIncludeImageReaderMode() {
+        ImageAnalysisUseCaseConfiguration defaultConfig =
+                ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration();
+
+        // Will throw if mode does not exist
+        ImageReaderMode mode = defaultConfig.getImageReaderMode();
+
+        // Should not be null
+        assertThat(mode).isNotNull();
+    }
+
+    @Test
+    public void defaultsIncludeImageQueueDepth() {
+        ImageAnalysisUseCaseConfiguration defaultConfig =
+                ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration();
+
+        // Will throw if depth does not exist
+        int depth = defaultConfig.getImageQueueDepth();
+
+        // Should not be less than 1
+        assertThat(depth).isAtLeast(1);
+    }
+
+    private static class ImageProperties {
+        final Size resolution;
+        final int format;
+        final long timestamp;
+        final int rotationDegrees;
+
+        ImageProperties(ImageProxy image, int rotationDegrees) {
+            this.resolution = new Size(image.getWidth(), image.getHeight());
+            this.format = image.getFormat();
+            this.timestamp = image.getTimestamp();
+            this.rotationDegrees = rotationDegrees;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (other == null) {
+                return false;
+            }
+            if (!(other instanceof ImageProperties)) {
+                return false;
+            }
+            ImageProperties otherProperties = (ImageProperties) other;
+            return resolution.equals(otherProperties.resolution)
+                    && format == otherProperties.format
+                    && otherProperties.timestamp == timestamp
+                    && otherProperties.rotationDegrees == rotationDegrees;
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 7;
+            hash = 31 * hash + resolution.getWidth();
+            hash = 31 * hash + resolution.getHeight();
+            hash = 31 * hash + format;
+            hash = 31 * hash + (int) timestamp;
+            hash = 31 * hash + rotationDegrees;
+            return hash;
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseAndroidTest.java
new file mode 100644
index 0000000..b94e9cf
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseAndroidTest.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.location.Location;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.Exif;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCase.Metadata;
+import androidx.camera.core.ImageCaptureUseCase.OnImageCapturedListener;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageCaptureUseCase.UseCaseError;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageCaptureUseCaseAndroidTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+    private static final LensFacing BACK_LENS_FACING = LensFacing.BACK;
+
+    private HandlerThread handlerThread;
+    private Handler handler;
+    private BaseCamera camera;
+    private ImageCaptureUseCaseConfiguration defaultConfiguration;
+    private ImageCaptureUseCase.OnImageCapturedListener onImageCapturedListener;
+    private ImageCaptureUseCase.OnImageCapturedListener mockImageCapturedListener;
+    private ImageCaptureUseCase.OnImageSavedListener onImageSavedListener;
+    private ImageCaptureUseCase.OnImageSavedListener mockImageSavedListener;
+    private ImageProxy capturedImage;
+    private Semaphore semaphore;
+    private FakeRepeatingUseCase repeatingUseCase;
+    private FakeUseCaseConfiguration fakeRepeatingConfiguration;
+    private String cameraId;
+
+    private ImageCaptureUseCaseConfiguration createNonRotatedConfiguration()
+            throws CameraInfoUnavailableException {
+        // Create a configuration with target rotation that matches the sensor rotation.
+        // This assumes a back-facing camera (facing away from screen)
+        String backCameraId = CameraX.getCameraWithLensFacing(BACK_LENS_FACING);
+        int sensorRotation = CameraX.getCameraInfo(backCameraId).getSensorRotationDegrees();
+
+        int surfaceRotation = Surface.ROTATION_0;
+        switch (sensorRotation) {
+            case 0:
+                surfaceRotation = Surface.ROTATION_0;
+                break;
+            case 90:
+                surfaceRotation = Surface.ROTATION_90;
+                break;
+            case 180:
+                surfaceRotation = Surface.ROTATION_180;
+                break;
+            case 270:
+                surfaceRotation = Surface.ROTATION_270;
+                break;
+            default:
+                throw new IllegalStateException("Invalid sensor rotation: " + sensorRotation);
+        }
+
+        return new ImageCaptureUseCaseConfiguration.Builder()
+                .setLensFacing(BACK_LENS_FACING)
+                .setTargetRotation(surfaceRotation)
+                .build();
+    }
+
+    @Before
+    public void setUp() {
+        handlerThread = new HandlerThread("CaptureThread");
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = appConfig.getCameraFactory(null);
+        CameraX.init(context, appConfig);
+        try {
+            cameraId = cameraFactory.cameraIdForLensFacing(BACK_LENS_FACING);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + BACK_LENS_FACING, e);
+        }
+        defaultConfiguration = new ImageCaptureUseCaseConfiguration.Builder().build();
+
+        camera = cameraFactory.getCamera(cameraId);
+        capturedImage = null;
+        semaphore = new Semaphore(/*permits=*/ 0);
+        onImageCapturedListener =
+                new OnImageCapturedListener() {
+                    @Override
+                    public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                        capturedImage = image;
+                        // Signal that the image has been captured.
+                        semaphore.release();
+                    }
+                };
+        mockImageCapturedListener = Mockito.mock(OnImageCapturedListener.class);
+        mockImageSavedListener = Mockito.mock(OnImageSavedListener.class);
+        onImageSavedListener =
+                new OnImageSavedListener() {
+                    @Override
+                    public void onImageSaved(File file) {
+                        mockImageSavedListener.onImageSaved(file);
+                        // Signal that an image was saved
+                        semaphore.release();
+                    }
+
+                    @Override
+                    public void onError(
+                            UseCaseError error, String message, @Nullable Throwable cause) {
+                        mockImageSavedListener.onError(error, message, cause);
+                        // Signal that there was an error
+                        semaphore.release();
+                    }
+                };
+
+        fakeRepeatingConfiguration = new FakeUseCaseConfiguration.Builder().build();
+        repeatingUseCase = new FakeRepeatingUseCase(fakeRepeatingConfiguration);
+    }
+
+    @After
+    public void tearDown() {
+        handlerThread.quitSafely();
+        camera.release();
+        if (capturedImage != null) {
+            capturedImage.close();
+        }
+    }
+
+    @Test
+    public void capturedImageHasCorrectProperties() throws InterruptedException {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        useCase.takePicture(onImageCapturedListener);
+        // Wait for the signal that the image has been captured.
+        semaphore.acquire();
+
+        assertThat(new Size(capturedImage.getWidth(), capturedImage.getHeight()))
+                .isEqualTo(DEFAULT_RESOLUTION);
+        assertThat(capturedImage.getFormat()).isEqualTo(useCase.getImageFormat());
+    }
+
+    @Test(timeout = 5000)
+    public void canCaptureMultipleImages() throws InterruptedException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        int numImages = 5;
+        for (int i = 0; i < numImages; ++i) {
+            useCase.takePicture(
+                    new ImageCaptureUseCase.OnImageCapturedListener() {
+                        @Override
+                        public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                            mockImageCapturedListener.onCaptureSuccess(image, rotationDegrees);
+                            image.close();
+
+                            // Signal that an image has been captured.
+                            semaphore.release();
+                        }
+                    });
+        }
+
+        // Wait for the signal that all images have been captured.
+        semaphore.acquire(numImages);
+
+        verify(mockImageCapturedListener, times(numImages)).onCaptureSuccess(any(), anyInt());
+    }
+
+    @Test(timeout = 10000)
+    public void canCaptureMultipleImagesWithMaxQuality() throws InterruptedException {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder()
+                        .setCaptureMode(ImageCaptureUseCase.CaptureMode.MAX_QUALITY)
+                        .build();
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        int numImages = 5;
+        for (int i = 0; i < numImages; ++i) {
+            useCase.takePicture(
+                    new ImageCaptureUseCase.OnImageCapturedListener() {
+                        @Override
+                        public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                            mockImageCapturedListener.onCaptureSuccess(image, rotationDegrees);
+                            image.close();
+
+                            // Signal that an image has been captured.
+                            semaphore.release();
+                        }
+                    });
+        }
+
+        // Wait for the signal that all images have been captured.
+        semaphore.acquire(numImages);
+
+        verify(mockImageCapturedListener, times(numImages)).onCaptureSuccess(any(), anyInt());
+    }
+
+    @Test(timeout = 5000)
+    public void saveCanSucceed() throws InterruptedException, IOException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        useCase.takePicture(saveLocation, onImageSavedListener);
+
+        // Wait for the signal that the image has been saved.
+        semaphore.acquire();
+
+        verify(mockImageSavedListener).onImageSaved(eq(saveLocation));
+    }
+
+    @Test(timeout = 5000)
+    public void canSaveFile_withRotation()
+            throws InterruptedException, IOException, CameraInfoUnavailableException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Metadata metadata = new Metadata();
+        useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        semaphore.acquire();
+
+        // Retrieve the sensor orientation
+        int rotationDegrees = CameraX.getCameraInfo(cameraId).getSensorRotationDegrees();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.getRotation()).isEqualTo(rotationDegrees);
+    }
+
+    @Test(timeout = 5000)
+    public void canSaveFile_flippedHorizontal()
+            throws InterruptedException, IOException, CameraInfoUnavailableException {
+        // Use a non-rotated configuration since some combinations of rotation + flipping vertically
+        // can
+        // be equivalent to flipping horizontally
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(createNonRotatedConfiguration());
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Metadata metadata = new Metadata();
+        metadata.isReversedHorizontal = true;
+        useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        semaphore.acquire();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.isFlippedHorizontally()).isTrue();
+    }
+
+    @Test(timeout = 5000)
+    public void canSaveFile_flippedVertical()
+            throws InterruptedException, IOException, CameraInfoUnavailableException {
+        // Use a non-rotated configuration since some combinations of rotation + flipping
+        // horizontally
+        // can be equivalent to flipping vertically
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(createNonRotatedConfiguration());
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Metadata metadata = new Metadata();
+        metadata.isReversedVertical = true;
+        useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        semaphore.acquire();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.isFlippedVertically()).isTrue();
+    }
+
+    @Test(timeout = 5000)
+    public void canSaveFile_withAttachedLocation() throws InterruptedException, IOException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Location location = new Location("ImageCaptureUseCaseAndroidTest");
+        Metadata metadata = new Metadata();
+        metadata.location = location;
+        useCase.takePicture(saveLocation, onImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        semaphore.acquire();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.getLocation().getProvider()).isEqualTo(location.getProvider());
+    }
+
+    @Test(timeout = 5000)
+    public void canSaveMultipleFiles() throws InterruptedException, IOException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        int numImages = 5;
+        for (int i = 0; i < numImages; ++i) {
+            File saveLocation = File.createTempFile("test" + i, ".jpg");
+            saveLocation.deleteOnExit();
+
+            useCase.takePicture(saveLocation, onImageSavedListener);
+        }
+
+        // Wait for the signal that all images have been saved.
+        semaphore.acquire(numImages);
+
+        verify(mockImageSavedListener, times(numImages)).onImageSaved(anyObject());
+    }
+
+    @Test(timeout = 5000)
+    public void saveWillFail_whenInvalidFilePathIsUsed() throws InterruptedException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+        useCase.addStateChangeListener(camera);
+
+        // Note the invalid path
+        File saveLocation = new File("/not/a/real/path.jpg");
+        useCase.takePicture(saveLocation, onImageSavedListener);
+
+        // Wait for the signal that an error occurred.
+        semaphore.acquire();
+
+        verify(mockImageSavedListener)
+                .onError(eq(UseCaseError.FILE_IO_ERROR), anyString(), anyObject());
+    }
+
+    @Test(timeout = 5000)
+    public void updateSessionConfigurationWithSuggestedResolution() throws InterruptedException {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder().setCallbackHandler(handler).build();
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+        useCase.addStateChangeListener(camera);
+        final Size[] sizes = {new Size(1920, 1080), new Size(640, 480)};
+
+        for (Size size : sizes) {
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            suggestedResolutionMap.put(cameraId, size);
+            // Update SessionConfiguration with resolution setting
+            useCase.updateSuggestedResolution(suggestedResolutionMap);
+            CameraUtil.openCameraWithUseCase(camera, useCase, repeatingUseCase);
+
+            useCase.takePicture(onImageCapturedListener);
+            // Wait for the signal that the image has been captured.
+            semaphore.acquire();
+
+            assertThat(new Size(capturedImage.getWidth(), capturedImage.getHeight()))
+                    .isEqualTo(size);
+
+            // Detach use case from camera device to run next resolution setting
+            CameraUtil.detachUseCaseFromCamera(camera, useCase);
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysAndroidTest.java
new file mode 100644
index 0000000..499e0e3
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysAndroidTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraDevice;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.FakeUseCase;
+import androidx.camera.core.FakeUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ImageReaderProxy;
+import androidx.camera.core.ImageReaderProxys;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageReaderProxysAndroidTest {
+    private static final String CAMERA_ID = "0";
+
+    private BaseCamera camera;
+    private HandlerThread handlerThread;
+    private Handler handler;
+
+    private static ImageReaderProxy.OnImageAvailableListener createSemaphoreReleasingListener(
+            Semaphore semaphore) {
+        return reader -> {
+            ImageProxy image = reader.acquireLatestImage();
+            if (image != null) {
+                semaphore.release();
+                image.close();
+            }
+        };
+    }
+
+    @Before
+    public void setUp() {
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = appConfig.getCameraFactory(null);
+        CameraX.init(context, appConfig);
+        camera = cameraFactory.getCamera(CAMERA_ID);
+        handlerThread = new HandlerThread("Background");
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        camera.release();
+        handlerThread.quitSafely();
+    }
+
+    @Test(timeout = 5000)
+    public void sharedReadersGetFramesFromCamera() throws InterruptedException {
+        List<ImageReaderProxy> readers = new ArrayList<>();
+        List<Semaphore> semaphores = new ArrayList<>();
+        for (int i = 0; i < 2; ++i) {
+            ImageReaderProxy reader =
+                    ImageReaderProxys.createSharedReader(
+                            CAMERA_ID, 640, 480, ImageFormat.YUV_420_888, 2, handler);
+            Semaphore semaphore = new Semaphore(/*permits=*/ 0);
+            reader.setOnImageAvailableListener(
+                    createSemaphoreReleasingListener(semaphore), handler);
+            readers.add(reader);
+            semaphores.add(semaphore);
+        }
+
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        UseCase useCase = new UseCase(configuration, readers);
+        CameraUtil.openCameraWithUseCase(camera, useCase);
+
+        // Wait for a few frames to be observed.
+        for (Semaphore semaphore : semaphores) {
+            semaphore.acquire(/*permits=*/ 5);
+        }
+    }
+
+    @Test(timeout = 5000)
+    public void isolatedReadersGetFramesFromCamera() throws InterruptedException {
+        List<ImageReaderProxy> readers = new ArrayList<>();
+        List<Semaphore> semaphores = new ArrayList<>();
+        for (int i = 0; i < 2; ++i) {
+            ImageReaderProxy reader =
+                    ImageReaderProxys.createIsolatedReader(
+                            640, 480, ImageFormat.YUV_420_888, 2, handler);
+            Semaphore semaphore = new Semaphore(/*permits=*/ 0);
+            reader.setOnImageAvailableListener(
+                    createSemaphoreReleasingListener(semaphore), handler);
+            readers.add(reader);
+            semaphores.add(semaphore);
+        }
+
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        UseCase useCase = new UseCase(configuration, readers);
+        CameraUtil.openCameraWithUseCase(camera, useCase);
+
+        // Wait for a few frames to be observed.
+        for (Semaphore semaphore : semaphores) {
+            semaphore.acquire(/*permits=*/ 5);
+        }
+    }
+
+    private static final class UseCase extends FakeUseCase {
+        private final List<ImageReaderProxy> imageReaders;
+
+        private UseCase(FakeUseCaseConfiguration configuration, List<ImageReaderProxy> readers) {
+            super(configuration);
+            imageReaders = readers;
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            suggestedResolutionMap.put(CAMERA_ID, new Size(640, 480));
+            updateSuggestedResolution(suggestedResolutionMap);
+        }
+
+        @Override
+        protected Map<String, Size> onSuggestedResolutionUpdated(
+                Map<String, Size> suggestedResolutionMap) {
+            SessionConfiguration.Builder sessionConfigBuilder = new SessionConfiguration.Builder();
+            sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            for (ImageReaderProxy reader : imageReaders) {
+                sessionConfigBuilder.addSurface(new ImmediateSurface(reader.getSurface()));
+            }
+            attachToCamera(CAMERA_ID, sessionConfigBuilder.build());
+            return suggestedResolutionMap;
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/SemaphoreReleasingCamera2Callbacks.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/SemaphoreReleasingCamera2Callbacks.java
new file mode 100644
index 0000000..71e4b84
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/SemaphoreReleasingCamera2Callbacks.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.view.Surface;
+
+import java.util.concurrent.Semaphore;
+
+/** Camera2 callbacks which release specific semaphores on each event. */
+final class SemaphoreReleasingCamera2Callbacks {
+
+    private SemaphoreReleasingCamera2Callbacks() {
+    }
+
+    /** A device state callback which releases a different semaphore for each method. */
+    static final class DeviceStateCallback extends CameraDevice.StateCallback {
+        private static final String TAG = DeviceStateCallback.class.getSimpleName();
+
+        private final Semaphore onOpenedSemaphore = new Semaphore(0);
+        private final Semaphore onClosedSemaphore = new Semaphore(0);
+        private final Semaphore onDisconnectedSemaphore = new Semaphore(0);
+        private final Semaphore onErrorSemaphore = new Semaphore(0);
+
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+            onOpenedSemaphore.release();
+        }
+
+        @Override
+        public void onClosed(CameraDevice cameraDevice) {
+            onClosedSemaphore.release();
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            onDisconnectedSemaphore.release();
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+            onErrorSemaphore.release();
+        }
+
+        void waitForOnOpened(int count) throws InterruptedException {
+            onOpenedSemaphore.acquire(count);
+        }
+
+        void waitForOnClosed(int count) throws InterruptedException {
+            onClosedSemaphore.acquire(count);
+        }
+
+        void waitForOnDisconnected(int count) throws InterruptedException {
+            onDisconnectedSemaphore.acquire(count);
+        }
+
+        void waitForOnError(int count) throws InterruptedException {
+            onErrorSemaphore.acquire(count);
+        }
+    }
+
+    /** A session state callback which releases a different semaphore for each method. */
+    static final class SessionStateCallback extends CameraCaptureSession.StateCallback {
+        private static final String TAG = SessionStateCallback.class.getSimpleName();
+
+        private final Semaphore onConfiguredSemaphore = new Semaphore(0);
+        private final Semaphore onActiveSemaphore = new Semaphore(0);
+        private final Semaphore onClosedSemaphore = new Semaphore(0);
+        private final Semaphore onReadySemaphore = new Semaphore(0);
+        private final Semaphore onCaptureQueueEmptySemaphore = new Semaphore(0);
+        private final Semaphore onSurfacePreparedSemaphore = new Semaphore(0);
+        private final Semaphore onConfigureFailedSemaphore = new Semaphore(0);
+
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            onConfiguredSemaphore.release();
+        }
+
+        @Override
+        public void onActive(CameraCaptureSession session) {
+            onActiveSemaphore.release();
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            onClosedSemaphore.release();
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            onReadySemaphore.release();
+        }
+
+        @Override
+        public void onCaptureQueueEmpty(CameraCaptureSession session) {
+            onCaptureQueueEmptySemaphore.release();
+        }
+
+        @Override
+        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+            onSurfacePreparedSemaphore.release();
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            onConfigureFailedSemaphore.release();
+        }
+
+        void waitForOnConfigured(int count) throws InterruptedException {
+            onConfiguredSemaphore.acquire(count);
+        }
+
+        void waitForOnActive(int count) throws InterruptedException {
+            onActiveSemaphore.acquire(count);
+        }
+
+        void waitForOnClosed(int count) throws InterruptedException {
+            onClosedSemaphore.acquire(count);
+        }
+
+        void waitForOnReady(int count) throws InterruptedException {
+            onReadySemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureQueueEmpty(int count) throws InterruptedException {
+            onCaptureQueueEmptySemaphore.acquire(count);
+        }
+
+        void waitForOnSurfacePrepared(int count) throws InterruptedException {
+            onSurfacePreparedSemaphore.acquire(count);
+        }
+
+        void waitForOnConfigureFailed(int count) throws InterruptedException {
+            onConfigureFailedSemaphore.acquire(count);
+        }
+    }
+
+    /** A session capture callback which releases a different semaphore for each method. */
+    static final class SessionCaptureCallback extends CameraCaptureSession.CaptureCallback {
+        private static final String TAG = SessionCaptureCallback.class.getSimpleName();
+
+        private final Semaphore onCaptureBufferLostSemaphore = new Semaphore(0);
+        private final Semaphore onCaptureCompletedSemaphore = new Semaphore(0);
+        private final Semaphore onCaptureFailedSemaphore = new Semaphore(0);
+        private final Semaphore onCaptureProgressedSemaphore = new Semaphore(0);
+        private final Semaphore onCaptureSequenceAbortedSemaphore = new Semaphore(0);
+        private final Semaphore onCaptureSequenceCompletedSemaphore = new Semaphore(0);
+        private final Semaphore onCaptureStartedSemaphore = new Semaphore(0);
+
+        @Override
+        public void onCaptureBufferLost(
+                CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+            onCaptureBufferLostSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureCompleted(
+                CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+            onCaptureCompletedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureFailed(
+                CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+            onCaptureFailedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureProgressed(
+                CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+            onCaptureProgressedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+            onCaptureSequenceAbortedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(
+                CameraCaptureSession session, int sequenceId, long frame) {
+            onCaptureSequenceCompletedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureStarted(
+                CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+            onCaptureStartedSemaphore.release();
+        }
+
+        void waitForOnCaptureBufferLost(int count) throws InterruptedException {
+            onCaptureBufferLostSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureCompleted(int count) throws InterruptedException {
+            onCaptureCompletedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureFailed(int count) throws InterruptedException {
+            onCaptureFailedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureProgressed(int count) throws InterruptedException {
+            onCaptureProgressedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureSequenceAborted(int count) throws InterruptedException {
+            onCaptureSequenceAbortedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureSequenceCompleted(int count) throws InterruptedException {
+            onCaptureSequenceCompletedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureStarted(int count) throws InterruptedException {
+            onCaptureStartedSemaphore.acquire(count);
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerAndroidTest.java
new file mode 100644
index 0000000..0ec2537
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerAndroidTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+/** JUnit test cases for UseCaseSurfaceOccupancyManager class. */
+@RunWith(JUnit4.class)
+public final class UseCaseSurfaceOccupancyManagerAndroidTest {
+
+    @Before
+    public void setUp() {
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraX.init(context, appConfig);
+    }
+
+    @Test
+    public void failedWhenBindTooManyImageCaptureUseCase() {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder().build();
+        ImageCaptureUseCase useCase1 = new ImageCaptureUseCase(configuration);
+        ImageCaptureUseCase useCase2 = new ImageCaptureUseCase(configuration);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+                            Arrays.asList(useCase1), Arrays.asList(useCase2));
+                });
+    }
+
+    @Test
+    public void failedWhenBindTooManyVideoCaptureUseCase() {
+        VideoCaptureUseCaseConfiguration configuration =
+                new VideoCaptureUseCaseConfiguration.Builder().build();
+        VideoCaptureUseCase useCase1 = new VideoCaptureUseCase(configuration);
+        VideoCaptureUseCase useCase2 = new VideoCaptureUseCase(configuration);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+                            Arrays.asList(useCase1), Arrays.asList(useCase2));
+                });
+    }
+
+    @Test
+    public void passWhenNotBindTooManyImageVideoCaptureUseCase() {
+        ImageCaptureUseCaseConfiguration imageCaptureConfiguration =
+                new ImageCaptureUseCaseConfiguration.Builder().build();
+        ImageCaptureUseCase imageCaptureUseCase =
+                new ImageCaptureUseCase(imageCaptureConfiguration);
+
+        VideoCaptureUseCaseConfiguration videoCaptureConfiguration =
+                new VideoCaptureUseCaseConfiguration.Builder().build();
+        VideoCaptureUseCase videoCaptureUseCase =
+                new VideoCaptureUseCase(videoCaptureConfiguration);
+
+        UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+                Arrays.asList(imageCaptureUseCase), Arrays.asList(videoCaptureUseCase));
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseAndroidTest.java
new file mode 100644
index 0000000..440ea21
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseAndroidTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.util.Size;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.BaseUseCase.StateChangeListener;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Minimal unit test for the VideoCaptureUseCase because the {@link android.media.MediaRecorder}
+ * class requires a valid preview surface in order to correctly function.
+ *
+ * <p>TODO(b/112325215): The VideoCaptureUseCase will be more thoroughly tested via integration
+ * tests
+ */
+@RunWith(AndroidJUnit4.class)
+public final class VideoCaptureUseCaseAndroidTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+
+    private final Context context = InstrumentationRegistry.getTargetContext();
+    private final StateChangeListener listener = Mockito.mock(StateChangeListener.class);
+    private final ArgumentCaptor<BaseUseCase> baseUseCaseCaptor =
+            ArgumentCaptor.forClass(BaseUseCase.class);
+    private final OnVideoSavedListener mockVideoSavedListener =
+            Mockito.mock(OnVideoSavedListener.class);
+    private VideoCaptureUseCaseConfiguration defaultConfiguration;
+    private String cameraId;
+
+    @Before
+    public void setUp() {
+        defaultConfiguration = VideoCaptureUseCase.DEFAULT_CONFIG.getConfiguration();
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfiguration = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+        }
+        CameraX.init(context, appConfiguration);
+    }
+
+    @Test
+    public void useCaseBecomesActive_whenStartingVideoRecording() {
+        VideoCaptureUseCase useCase = new VideoCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(listener);
+
+        useCase.startRecording(
+                new File(
+                        context.getFilesDir()
+                                + "/useCaseBecomesActive_whenStartingVideoRecording.mp4"),
+                mockVideoSavedListener);
+
+        verify(listener, times(1)).onUseCaseActive(baseUseCaseCaptor.capture());
+        assertThat(baseUseCaseCaptor.getValue()).isSameAs(useCase);
+    }
+
+    @Test
+    public void useCaseBecomesInactive_whenStoppingVideoRecording() {
+        VideoCaptureUseCase useCase = new VideoCaptureUseCase(defaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(listener);
+
+        useCase.startRecording(
+                new File(
+                        context.getFilesDir()
+                                + "/useCaseBecomesInactive_whenStoppingVideoRecording.mp4"),
+                mockVideoSavedListener);
+
+        try {
+            useCase.stopRecording();
+        } catch (RuntimeException e) {
+            // Need to catch the RuntimeException because for certain devices MediaRecorder
+            // contained
+            // within the VideoCaptureUseCase requires a valid preview in order to run. This unit
+            // test is
+            // just to exercise the inactive state change that the use case should trigger
+            // TODO(b/112324530): The try-catch should be removed after the bug fix
+        }
+
+        verify(listener, times(1)).onUseCaseInactive(baseUseCaseCaptor.capture());
+        assertThat(baseUseCaseCaptor.getValue()).isSameAs(useCase);
+    }
+
+    @Test
+    public void updateSessionConfigurationWithSuggestedResolution() {
+        VideoCaptureUseCase useCase = new VideoCaptureUseCase(defaultConfiguration);
+        // Create video encoder with default 1920x1080 resolution
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(cameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(listener);
+
+        // Recreate video encoder with new 640x480 resolution
+        suggestedResolutionMap.put(cameraId, new Size(640, 480));
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+
+        // Check it could be started to record and become active
+        useCase.startRecording(
+                new File(
+                        context.getFilesDir()
+                                + "/useCaseBecomesInactive_whenStoppingVideoRecording.mp4"),
+                mockVideoSavedListener);
+
+        verify(listener, times(1)).onUseCaseActive(baseUseCaseCaptor.capture());
+        assertThat(baseUseCaseCaptor.getValue()).isSameAs(useCase);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseAndroidTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseAndroidTest.java
new file mode 100644
index 0000000..dbf9d6c
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseAndroidTest.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.DeferrableSurfaces;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCase.OnViewFinderOutputUpdateListener;
+import androidx.camera.core.ViewFinderUseCase.ViewFinderOutput;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+public final class ViewFinderUseCaseAndroidTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+    private static final Size SECONDARY_RESOLUTION = new Size(1280, 720);
+
+    private ViewFinderUseCaseConfiguration defaultConfiguration;
+    @Mock
+    private OnViewFinderOutputUpdateListener mockListener;
+    private String cameraId;
+
+    @Before
+    public void setUp() {
+        // Instantiates OnViewFinderOutputUpdateListener before each test run.
+        mockListener = Mockito.mock(OnViewFinderOutputUpdateListener.class);
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = appConfig.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+        }
+        CameraX.init(context, appConfig);
+
+        // init CameraX before creating ViewFinderUseCase to get preview size with CameraX's context
+        defaultConfiguration = ViewFinderUseCase.DEFAULT_CONFIG.getConfiguration();
+    }
+
+    @Test
+    @UiThreadTest
+    public void useCaseIsConstructedWithDefaultConfiguration() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        List<Surface> surfaces =
+                DeferrableSurfaces.surfaceList(
+                        useCase.getSessionConfiguration(cameraId).getSurfaces());
+
+        assertThat(surfaces.size()).isEqualTo(1);
+        assertThat(surfaces.get(0).isValid()).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void useCaseIsConstructedWithCustomConfiguration() {
+        ViewFinderUseCaseConfiguration configuration =
+                new ViewFinderUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK).build();
+        ViewFinderUseCase useCase = new ViewFinderUseCase(configuration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        List<Surface> surfaces =
+                DeferrableSurfaces.surfaceList(
+                        useCase.getSessionConfiguration(cameraId).getSurfaces());
+
+        assertThat(surfaces.size()).isEqualTo(1);
+        assertThat(surfaces.get(0).isValid()).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void focusRegionCanBeSet() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        CameraControl cameraControl = getFakeCameraControl();
+        useCase.attachCameraControl(cameraId, cameraControl);
+
+        Rect rect = new Rect(/*left=*/ 200, /*top=*/ 200, /*right=*/ 800, /*bottom=*/ 800);
+        useCase.focus(rect, rect);
+
+        Camera2Configuration configuration =
+                new Camera2Configuration(cameraControl.getSingleRequestImplOptions());
+        MeteringRectangle[] aeMeteringRects =
+                configuration.getCaptureRequestOption(CaptureRequest.CONTROL_AE_REGIONS, null);
+        MeteringRectangle[] afMeteringRects =
+                configuration.getCaptureRequestOption(CaptureRequest.CONTROL_AF_REGIONS, null);
+        MeteringRectangle[] awbMeteringRects =
+                configuration.getCaptureRequestOption(CaptureRequest.CONTROL_AWB_REGIONS, null);
+        assertThat(aeMeteringRects).hasLength(1);
+        assertThat(afMeteringRects).hasLength(1);
+        assertThat(awbMeteringRects).hasLength(1);
+
+        assertThat(aeMeteringRects[0].getRect()).isEqualTo(rect);
+        assertThat(afMeteringRects[0].getRect()).isEqualTo(rect);
+        assertThat(awbMeteringRects[0].getRect()).isEqualTo(rect);
+    }
+
+    @Test
+    @UiThreadTest
+    public void zoomRegionCanBeSet() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        CameraControl cameraControl = getFakeCameraControl();
+        useCase.attachCameraControl(cameraId, cameraControl);
+
+        Rect rect = new Rect(/*left=*/ 200, /*top=*/ 200, /*right=*/ 800, /*bottom=*/ 800);
+        useCase.zoom(rect);
+
+        Camera2Configuration configuration =
+                new Camera2Configuration(cameraControl.getSingleRequestImplOptions());
+        Rect cropRect =
+                configuration.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null);
+        assertThat(cropRect).isEqualTo(rect);
+    }
+
+    @Test
+    @UiThreadTest
+    public void torchModeCanBeSet() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        CameraControl cameraControl = getFakeCameraControl();
+        useCase.attachCameraControl(cameraId, cameraControl);
+
+        useCase.enableTorch(true);
+
+        assertThat(useCase.isTorchOn()).isTrue();
+    }
+
+    @Test(timeout = 5000)
+    @UiThreadTest
+    public void surfaceTextureIsNotReleased()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        // This test only target SDK >= 26
+        if (Build.VERSION.SDK_INT < 26) {
+            return;
+        }
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+        SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    future0.run();
+                });
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        SurfaceTexture surfaceTexture0 = future0.get(1, TimeUnit.SECONDS);
+        surfaceTexture0.release();
+
+        SurfaceTextureCallable surfaceTextureCallable1 = new SurfaceTextureCallable();
+        FutureTask<SurfaceTexture> future1 = new FutureTask<>(surfaceTextureCallable1);
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    surfaceTextureCallable1.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    future1.run();
+                });
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture1 = future1.get(1, TimeUnit.SECONDS);
+
+        assertThat(surfaceTexture1.isReleased()).isFalse();
+    }
+
+    @Test(timeout = 5000)
+    @UiThreadTest
+    public void listenedSurfaceTextureIsNotReleased_whenCleared()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        // This test only target SDK >= 26
+        if (Build.VERSION.SDK_INT <= 26) {
+            return;
+        }
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+        SurfaceTextureCallable surfaceTextureCallable = new SurfaceTextureCallable();
+        FutureTask<SurfaceTexture> future = new FutureTask<>(surfaceTextureCallable);
+
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    surfaceTextureCallable.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    future.run();
+                });
+
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture = future.get(1, TimeUnit.SECONDS);
+
+        useCase.clear();
+
+        assertThat(surfaceTexture.isReleased()).isFalse();
+    }
+
+    @Test(timeout = 5000)
+    @UiThreadTest
+    public void surfaceTexture_isListenedOnlyOnce()
+            throws InterruptedException, ExecutionException, TimeoutException {
+
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+        SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    future0.run();
+                });
+
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture0 = future0.get();
+
+        SurfaceTextureCallable surfaceTextureCallable1 = new SurfaceTextureCallable();
+        FutureTask<SurfaceTexture> future1 = new FutureTask<>(surfaceTextureCallable1);
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    surfaceTextureCallable1.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    future1.run();
+                });
+
+        SurfaceTexture surfaceTexture1 = future1.get(1, TimeUnit.SECONDS);
+
+        assertThat(surfaceTexture0).isNotSameAs(surfaceTexture1);
+    }
+
+    @Test
+    @UiThreadTest
+    public void updateSessionConfigurationWithSuggestedResolution() {
+        ViewFinderUseCaseConfiguration configuration =
+                new ViewFinderUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK).build();
+        ViewFinderUseCase useCase = new ViewFinderUseCase(configuration);
+
+        final Size[] sizes = {new Size(1920, 1080), new Size(640, 480)};
+
+        for (Size size : sizes) {
+            useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, size));
+
+            List<Surface> surfaces =
+                    DeferrableSurfaces.surfaceList(
+                            useCase.getSessionConfiguration(cameraId).getSurfaces());
+
+            assertWithMessage("Failed at Size: " + size).that(surfaces).hasSize(1);
+            assertWithMessage("Failed at Size: " + size).that(surfaces.get(0).isValid()).isTrue();
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    public void viewFinderOutputListenerCanBeSetAndRetrieved() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+        OnViewFinderOutputUpdateListener viewFinderOutputListener =
+                useCase.getOnViewFinderOutputUpdateListener();
+        useCase.setOnViewFinderOutputUpdateListener(mockListener);
+
+        OnViewFinderOutputUpdateListener retrievedViewFinderOutputListener =
+                useCase.getOnViewFinderOutputUpdateListener();
+
+        assertThat(viewFinderOutputListener).isNull();
+        assertThat(retrievedViewFinderOutputListener).isSameAs(mockListener);
+    }
+
+    @Test
+    @UiThreadTest
+    public void clear_removeViewFinderOutputListener() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        useCase.setOnViewFinderOutputUpdateListener(mockListener);
+        useCase.clear();
+
+        assertThat(useCase.getOnViewFinderOutputUpdateListener()).isNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void viewFinderOutput_isResetOnUpdatedResolution() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        AtomicInteger calledCount = new AtomicInteger(0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    calledCount.incrementAndGet();
+                });
+
+        int initialCount = calledCount.get();
+
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, SECONDARY_RESOLUTION));
+
+        int countAfterUpdate = calledCount.get();
+
+        assertThat(initialCount).isEqualTo(1);
+        assertThat(countAfterUpdate).isEqualTo(2);
+    }
+
+    @Test
+    @UiThreadTest
+    public void viewFinderOutput_updatesWithTargetRotation() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        useCase.setTargetRotation(Surface.ROTATION_0);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        AtomicReference<ViewFinderOutput> latestViewFinderOutput = new AtomicReference<>();
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    latestViewFinderOutput.set(viewFinderOutput);
+                });
+
+        ViewFinderOutput initialOutput = latestViewFinderOutput.get();
+
+        useCase.setTargetRotation(Surface.ROTATION_90);
+
+        assertThat(initialOutput).isNotNull();
+        assertThat(initialOutput.getSurfaceTexture())
+                .isEqualTo(latestViewFinderOutput.get().getSurfaceTexture());
+        assertThat(initialOutput.getRotationDegrees())
+                .isNotEqualTo(latestViewFinderOutput.get().getRotationDegrees());
+    }
+
+    // Must not run on main thread
+    @Test(timeout = 5000)
+    public void viewFinderOutput_isResetByReleasedSurface()
+            throws InterruptedException, ExecutionException {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+        Handler mainHandler = new Handler(Looper.getMainLooper());
+        Semaphore semaphore = new Semaphore(0);
+
+        mainHandler.post(
+                () -> {
+                    useCase.updateSuggestedResolution(
+                            Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+                    useCase.setOnViewFinderOutputUpdateListener(
+                            viewFinderOutput -> {
+                                // Release the surface texture
+                                viewFinderOutput.getSurfaceTexture().release();
+
+                                semaphore.release();
+                            });
+                });
+
+        // Wait for the surface texture to be released
+        semaphore.acquire();
+
+        // Cause the surface to reset
+        useCase.getSessionConfiguration(cameraId).getSurfaces().get(0).getSurface().get();
+
+        // Wait for the surface to reset
+        semaphore.acquire();
+    }
+
+    @Test(timeout = 5000)
+    @UiThreadTest
+    public void outputIsPublished_whenListenerIsSetBefore()
+            throws InterruptedException, ExecutionException {
+
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+        SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    future0.run();
+                });
+
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture0 = future0.get();
+
+        assertThat(surfaceTexture0).isNotNull();
+    }
+
+    @Test(timeout = 5000)
+    @UiThreadTest
+    public void outputIsPublished_whenListenerIsSetAfter()
+            throws InterruptedException, ExecutionException {
+
+        ViewFinderUseCase useCase = new ViewFinderUseCase(defaultConfiguration);
+
+        SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.updateSuggestedResolution(Collections.singletonMap(cameraId, DEFAULT_RESOLUTION));
+
+        useCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    surfaceTextureCallable0.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    future0.run();
+                });
+        SurfaceTexture surfaceTexture0 = future0.get();
+
+        assertThat(surfaceTexture0).isNotNull();
+    }
+
+    private CameraControl getFakeCameraControl() {
+        return new Camera2CameraControl(
+                new Camera2RequestRunner() {
+                    @Override
+                    public void submitSingleRequest(
+                            CaptureRequestConfiguration singleRequestConfig) {
+                    }
+
+                    @Override
+                    public void updateRepeatingRequest() {
+                    }
+                },
+                new Handler());
+    }
+
+    private static final class SurfaceTextureCallable implements Callable<SurfaceTexture> {
+        SurfaceTexture surfaceTexture;
+
+        void setSurfaceTexture(SurfaceTexture surfaceTexture) {
+            this.surfaceTexture = surfaceTexture;
+        }
+
+        @Override
+        public SurfaceTexture call() {
+            return surfaceTexture;
+        }
+    }
+}
diff --git a/camera/camera2/src/main/AndroidManifest.xml b/camera/camera2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9430688
--- /dev/null
+++ b/camera/camera2/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.camera2">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <application>
+        <provider
+            android:name=".Camera2Initializer"
+            android:authorities="${applicationId}.camerax-init"
+            android:exported="false"
+            android:initOrder="100"
+            android:multiprocess="true" />
+    </application>
+
+</manifest>
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CamcorderProfileHelper.java b/camera/camera2/src/main/java/androidx/camera/camera2/CamcorderProfileHelper.java
new file mode 100644
index 0000000..b6c7b37
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CamcorderProfileHelper.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+/**
+ * This is helper class to use {@link android.media.CamcorderProfile} that may be mocked.
+ *
+ * @hide
+ */
+public interface CamcorderProfileHelper {
+
+    boolean hasProfile(int cameraId, int quality);
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera.java
new file mode 100644
index 0000000..d0f848d
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera.java
@@ -0,0 +1,706 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.annotation.SuppressLint;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraDeviceStateCallbacks;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.DeferrableSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.SessionConfiguration.ValidatingBuilder;
+import androidx.camera.core.UseCaseAttachState;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A camera which is controlled by the change of state in use cases.
+ *
+ * <p>The camera needs to be in an open state in order for use cases to control the camera. Whenever
+ * there is a non-zero number of use cases in the online state the camera will either have a capture
+ * session open or be in the process of opening up one. If the number of uses cases in the online
+ * state changes then the capture session will be reconfigured.
+ *
+ * <p>Capture requests will be issued only for use cases which are in both the online and active
+ * state.
+ */
+final class Camera implements BaseCamera, Camera2RequestRunner {
+    private static final String TAG = "Camera";
+
+    private final Object attachedUseCaseLock = new Object();
+
+    /** Map of the use cases to the information on their state. */
+    @GuardedBy("attachedUseCaseLock")
+    private final UseCaseAttachState useCaseAttachState;
+
+    /** The identifier for the {@link CameraDevice} */
+    private final String cameraId;
+
+    /** Handle to the camera service. */
+    private final CameraManager cameraManager;
+
+    private final Object cameraInfoLock = new Object();
+    /** The handler for camera callbacks and use case state management calls. */
+    private final Handler handler;
+    /**
+     * State variable for tracking state of the camera.
+     *
+     * <p>Is an atomic reference because it is initialized in the constructor which is not called on
+     * same thread as any of the other methods and callbacks.
+     */
+    final AtomicReference<State> state = new AtomicReference<>(State.UNINITIALIZED);
+    /** The camera control shared across all use cases bound to this Camera. */
+    private final CameraControl cameraControl;
+    private final StateCallback stateCallback = new StateCallback();
+    /** Information about the characteristics of this camera */
+    // Nullable because this is lazily instantiated
+    @GuardedBy("cameraInfoLock")
+    @Nullable
+    private CameraInfo cameraInfo;
+    /** The handle to the opened camera. */
+    @Nullable
+    CameraDevice cameraDevice;
+    /** The configured session which handles issuing capture requests. */
+    private CaptureSession captureSession = new CaptureSession(null);
+
+    /**
+     * Constructor for a camera.
+     *
+     * @param cameraManager the camera service used to retrieve a camera
+     * @param cameraId      the name of the camera as defined by the camera service
+     * @param handler       the handler for the thread on which all camera operations run
+     */
+    Camera(CameraManager cameraManager, String cameraId, Handler handler) {
+        this.cameraManager = cameraManager;
+        this.cameraId = cameraId;
+        this.handler = handler;
+        useCaseAttachState = new UseCaseAttachState(cameraId);
+        state.set(State.INITIALIZED);
+        cameraControl = new Camera2CameraControl(this, handler);
+    }
+
+    /**
+     * Open the camera asynchronously.
+     *
+     * <p>Once the camera has been opened use case state transitions can be used to control the
+     * camera pipeline.
+     */
+    @Override
+    public void open() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> open());
+            return;
+        }
+
+        switch (state.get()) {
+            case INITIALIZED:
+                openCameraDevice();
+                break;
+            case CLOSING:
+                state.set(State.REOPENING);
+                break;
+            default:
+                Log.d(TAG, "open() ignored due to being in state: " + state.get());
+        }
+    }
+
+    /**
+     * Close the camera asynchronously.
+     *
+     * <p>Once the camera is closed the camera will no longer produce data. The camera must be
+     * reopened for it to produce data again.
+     */
+    @Override
+    public void close() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> close());
+            return;
+        }
+
+        Log.d(TAG, "Closing camera: " + cameraId);
+        switch (state.get()) {
+            case OPENED:
+                state.set(State.CLOSING);
+                cameraDevice.close();
+                cameraDevice = null;
+                break;
+            case OPENING:
+            case REOPENING:
+                state.set(State.CLOSING);
+                break;
+            default:
+                Log.d(TAG, "close() ignored due to being in state: " + state.get());
+        }
+    }
+
+    /**
+     * Release the camera.
+     *
+     * <p>Once the camera is released it is permanently closed. A new instance must be created to
+     * access the camera.
+     */
+    @Override
+    public void release() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> release());
+            return;
+        }
+
+        switch (state.get()) {
+            case INITIALIZED:
+                state.set(State.RELEASED);
+                break;
+            case OPENED:
+                state.set(State.RELEASING);
+                cameraDevice.close();
+                break;
+            case OPENING:
+            case CLOSING:
+            case REOPENING:
+                state.set(State.RELEASING);
+                break;
+            default:
+                Log.d(TAG, "release() ignored due to being in state: " + state.get());
+        }
+    }
+
+    /**
+     * Sets the use case in a state to issue capture requests.
+     *
+     * <p>The use case must also be online in order for it to issue capture requests.
+     */
+    @Override
+    public void onUseCaseActive(BaseUseCase useCase) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> onUseCaseActive(useCase));
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " ACTIVE for camera " + cameraId);
+
+        synchronized (attachedUseCaseLock) {
+            useCaseAttachState.setUseCaseActive(useCase);
+        }
+        updateCaptureSessionConfiguration();
+    }
+
+    /** Removes the use case from a state of issuing capture requests. */
+    @Override
+    public void onUseCaseInactive(BaseUseCase useCase) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> onUseCaseInactive(useCase));
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " INACTIVE for camera " + cameraId);
+        synchronized (attachedUseCaseLock) {
+            useCaseAttachState.setUseCaseInactive(useCase);
+        }
+
+        updateCaptureSessionConfiguration();
+    }
+
+    /** Updates the capture requests based on the latest settings. */
+    @Override
+    public void onUseCaseUpdated(BaseUseCase useCase) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> onUseCaseUpdated(useCase));
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " UPDATED for camera " + cameraId);
+        synchronized (attachedUseCaseLock) {
+            useCaseAttachState.updateUseCase(useCase);
+        }
+
+        updateCaptureSessionConfiguration();
+    }
+
+    @Override
+    public void onUseCaseReset(BaseUseCase useCase) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> onUseCaseReset(useCase));
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " RESET for camera " + cameraId);
+        synchronized (attachedUseCaseLock) {
+            useCaseAttachState.updateUseCase(useCase);
+        }
+
+        updateCaptureSessionConfiguration();
+        openCaptureSession();
+    }
+
+    @Override
+    public void onUseCaseSingleRequest(
+            BaseUseCase useCase, CaptureRequestConfiguration captureRequestConfiguration) {
+        submitSingleRequest(captureRequestConfiguration);
+    }
+
+    /**
+     * Sets the use case to be in the state where the capture session will be configured to handle
+     * capture requests from the use case.
+     */
+    @Override
+    public void addOnlineUseCase(Collection<BaseUseCase> useCases) {
+        if (useCases.isEmpty()) {
+            return;
+        }
+
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> addOnlineUseCase(useCases));
+            return;
+        }
+
+        Log.d(TAG, "Use cases " + useCases + " ONLINE for camera " + cameraId);
+        synchronized (attachedUseCaseLock) {
+            for (BaseUseCase useCase : useCases) {
+                useCaseAttachState.setUseCaseOnline(useCase);
+            }
+        }
+
+        open();
+        updateCaptureSessionConfiguration();
+        openCaptureSession();
+    }
+
+    /**
+     * Removes the use case to be in the state where the capture session will be configured to
+     * handle capture requests from the use case.
+     */
+    @Override
+    public void removeOnlineUseCase(Collection<BaseUseCase> useCases) {
+        if (useCases.isEmpty()) {
+            return;
+        }
+
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> removeOnlineUseCase(useCases));
+            return;
+        }
+
+        Log.d(TAG, "Use cases " + useCases + " OFFLINE for camera " + cameraId);
+        synchronized (attachedUseCaseLock) {
+            for (BaseUseCase useCase : useCases) {
+                useCaseAttachState.setUseCaseOffline(useCase);
+            }
+
+            if (useCaseAttachState.getOnlineUseCases().isEmpty()) {
+                resetCaptureSession();
+                close();
+                return;
+            }
+        }
+
+        updateCaptureSessionConfiguration();
+    }
+
+    /** Returns an interface to retrieve characteristics of the camera. */
+    @Override
+    public CameraInfo getCameraInfo() throws CameraInfoUnavailableException {
+        synchronized (cameraInfoLock) {
+            if (cameraInfo == null) {
+                // Lazily instantiate camera info
+                cameraInfo = new Camera2CameraInfo(cameraManager, cameraId);
+            }
+
+            return cameraInfo;
+        }
+    }
+
+    /** Opens the camera device */
+    // TODO(b/124268878): Handle SecurityException and require permission in manifest.
+    @SuppressLint("MissingPermission")
+    void openCameraDevice() {
+        state.set(State.OPENING);
+
+        Log.d(TAG, "Opening camera: " + cameraId);
+
+        try {
+            cameraManager.openCamera(cameraId, createDeviceStateCallback(), handler);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Unable to open camera " + cameraId + " due to " + e.getMessage());
+            state.set(State.INITIALIZED);
+        }
+    }
+
+    /** Updates the capture request configuration for the current capture session. */
+    private void updateCaptureSessionConfiguration() {
+        ValidatingBuilder validatingBuilder;
+        synchronized (attachedUseCaseLock) {
+            validatingBuilder = useCaseAttachState.getActiveAndOnlineBuilder();
+        }
+
+        if (validatingBuilder.isValid()) {
+            // Apply CameraControl's SessionConfiguration to let CameraControl be able to control
+            // Repeating Request and process results.
+            validatingBuilder.add(cameraControl.getControlSessionConfiguration());
+
+            SessionConfiguration sessionConfiguration = validatingBuilder.build();
+            captureSession.setSessionConfiguration(sessionConfiguration);
+        }
+    }
+
+    /**
+     * Opens a new capture session.
+     *
+     * <p>The previously opened session will be safely disposed of before the new session opened.
+     */
+    void openCaptureSession() {
+        ValidatingBuilder validatingBuilder;
+        synchronized (attachedUseCaseLock) {
+            validatingBuilder = useCaseAttachState.getOnlineBuilder();
+        }
+        if (!validatingBuilder.isValid()) {
+            Log.d(TAG, "Unable to create capture session due to conflicting configurations");
+            return;
+        }
+
+        resetCaptureSession();
+
+        if (cameraDevice == null) {
+            Log.d(TAG, "CameraDevice is null");
+            return;
+        }
+
+        try {
+            captureSession.open(validatingBuilder.build(), cameraDevice);
+        } catch (CameraAccessException e) {
+            Log.d(TAG, "Unable to configure camera " + cameraId + " due to " + e.getMessage());
+        }
+    }
+
+    /**
+     * Closes the currently opened capture session, so it can be safely disposed. Replaces the old
+     * session with a new session initialized with the old session's configuration.
+     */
+    void resetCaptureSession() {
+        Log.d(TAG, "Closing Capture Session");
+        captureSession.close();
+
+        // Recreate an initialized (but not opened) capture session from the previous configuration
+        SessionConfiguration previousSessionConfiguration =
+                captureSession.getSessionConfiguration();
+        List<CaptureRequestConfiguration> unissuedCaptureRequestConfigurations =
+                captureSession.getCaptureRequestConfigurations();
+        captureSession = new CaptureSession(handler);
+        captureSession.setSessionConfiguration(previousSessionConfiguration);
+        // When the previous capture session has not reached the open state, the issued single
+        // capture
+        // requests will still be in request queue and will need to be passed to the next capture
+        // session.
+        captureSession.issueSingleCaptureRequests(unissuedCaptureRequestConfigurations);
+    }
+
+    private CameraDevice.StateCallback createDeviceStateCallback() {
+        synchronized (attachedUseCaseLock) {
+            SessionConfiguration configuration = useCaseAttachState.getOnlineBuilder().build();
+            return CameraDeviceStateCallbacks.createComboCallback(
+                    stateCallback, configuration.getDeviceStateCallback());
+        }
+    }
+
+    /**
+     * Attach a repeating surface to a {@link CaptureRequestConfiguration} when the configuration
+     * indicate that it needs a repeating surface.
+     *
+     * @param captureRequestConfiguration the configuration to attach a repeating surface
+     */
+    private void checkAndAttachRepeatingSurface(
+            CaptureRequestConfiguration captureRequestConfiguration) {
+        if (!captureRequestConfiguration.getSurfaces().isEmpty()) {
+            return;
+        }
+
+        if (!captureRequestConfiguration.isUseRepeatingSurface()) {
+            return;
+        }
+
+        Collection<BaseUseCase> activeUseCases;
+        synchronized (attachedUseCaseLock) {
+            activeUseCases = useCaseAttachState.getActiveAndOnlineUseCases();
+        }
+
+        DeferrableSurface repeatingSurface = null;
+        for (BaseUseCase useCase : activeUseCases) {
+            SessionConfiguration sessionConfiguration = useCase.getSessionConfiguration(cameraId);
+            List<DeferrableSurface> surfaces =
+                    sessionConfiguration.getCaptureRequestConfiguration().getSurfaces();
+            if (!surfaces.isEmpty()) {
+                // When an use case is active, all surfaces in its CaptureRequestConfiguration are
+                // added to
+                // the repeating request. Choose the first one here as the repeating surface.
+                repeatingSurface = surfaces.get(0);
+                break;
+            }
+        }
+
+        if (repeatingSurface == null) {
+            throw new IllegalStateException(
+                    "Unable to find a repeating surface to attach to CaptureRequestConfiguration");
+        }
+
+        captureRequestConfiguration.addSurface(repeatingSurface);
+    }
+
+    /** Returns the Camera2CameraControl attached to Camera */
+    @Override
+    public CameraControl getCameraControl() {
+        return cameraControl;
+    }
+
+    /**
+     * Submits single request
+     *
+     * @param captureRequestConfiguration capture configuration used for creating CaptureRequest
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void submitSingleRequest(CaptureRequestConfiguration captureRequestConfiguration) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> submitSingleRequest(captureRequestConfiguration));
+            return;
+        }
+        Log.d(TAG, "issue single capture request for camera " + cameraId);
+
+        checkAndAttachRepeatingSurface(captureRequestConfiguration);
+
+        // Recreates the Builder to add implementationOptions from CameraControl
+        CaptureRequestConfiguration.Builder builder =
+                CaptureRequestConfiguration.Builder.from(captureRequestConfiguration);
+        builder.addImplementationOptions(cameraControl.getSingleRequestImplOptions());
+
+        captureSession.issueSingleCaptureRequest(builder.build());
+    }
+
+    /**
+     * Re-sends repeating request based on current SessionConfigurations and CameraControl's Global
+     * SessionConfiguration
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void updateRepeatingRequest() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> updateRepeatingRequest());
+            return;
+        }
+
+        updateCaptureSessionConfiguration();
+    }
+
+    enum State {
+        /** The default state of the camera before construction. */
+        UNINITIALIZED,
+        /**
+         * Stable state once the camera has been constructed.
+         *
+         * <p>At this state the {@link CameraDevice} should be invalid, but threads should be still
+         * in a valid state. Whenever a camera device is fully closed the camera should return to
+         * this state.
+         *
+         * <p>After an error occurs the camera returns to this state so that the device can be
+         * cleanly reopened.
+         */
+        INITIALIZED,
+        /**
+         * A transitional state where the camera device is currently opening.
+         *
+         * <p>At the end of this state, the camera should move into either the OPENED or CLOSING
+         * state.
+         */
+        OPENING,
+        /**
+         * A stable state where the camera has been opened.
+         *
+         * <p>During this state the camera device should be valid. It is at this time a valid
+         * capture session can be active. Capture requests should be issued during this state only.
+         */
+        OPENED,
+        /**
+         * A transitional state where the camera device is currently closing.
+         *
+         * <p>At the end of this state, the camera should move into the INITIALIZED state.
+         */
+        CLOSING,
+        /**
+         * A transitional state where the camera was previously closing, but not fully closed before
+         * a call to open was made.
+         *
+         * <p>At the end of this state, the camera should move into one of two states. The OPENING
+         * state if the device becomes fully closed, since it must restart the process of opening a
+         * camera. The OPENED state if the device becomes opened, which can occur if a call to close
+         * had been done during the OPENING state.
+         */
+        REOPENING,
+        /**
+         * A transitional state where the camera will be closing permanently.
+         *
+         * <p>At the end of this state, the camera should move into the RELEASED state.
+         */
+        RELEASING,
+        /**
+         * A stable state where the camera has been permanently closed.
+         *
+         * <p>During this state all resources should be released and all operations on the camera
+         * will do nothing.
+         */
+        RELEASED
+    }
+
+    final class StateCallback extends CameraDevice.StateCallback {
+
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+            Log.d(TAG, "CameraDevice.onOpened(): " + cameraDevice.getId());
+            switch (state.get()) {
+                case CLOSING:
+                case RELEASING:
+                    cameraDevice.close();
+                    Camera.this.cameraDevice = null;
+                    break;
+                case OPENING:
+                case REOPENING:
+                    state.set(State.OPENED);
+                    Camera.this.cameraDevice = cameraDevice;
+                    openCaptureSession();
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "onOpened() should not be possible from state: " + state.get());
+            }
+        }
+
+        @Override
+        public void onClosed(CameraDevice cameraDevice) {
+            Log.d(TAG, "CameraDevice.onClosed(): " + cameraDevice.getId());
+            resetCaptureSession();
+            switch (state.get()) {
+                case CLOSING:
+                    state.set(State.INITIALIZED);
+                    Camera.this.cameraDevice = null;
+                    break;
+                case REOPENING:
+                    state.set(State.OPENING);
+                    openCameraDevice();
+                    break;
+                case RELEASING:
+                    state.set(State.RELEASED);
+                    Camera.this.cameraDevice = null;
+                    break;
+                default:
+                    CameraX.postError(
+                            CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT,
+                            "Camera closed while in state: " + state.get());
+            }
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            Log.d(TAG, "CameraDevice.onDisconnected(): " + cameraDevice.getId());
+            resetCaptureSession();
+            switch (state.get()) {
+                case CLOSING:
+                    state.set(State.INITIALIZED);
+                    Camera.this.cameraDevice = null;
+                    break;
+                case REOPENING:
+                case OPENED:
+                case OPENING:
+                    state.set(State.CLOSING);
+                    cameraDevice.close();
+                    Camera.this.cameraDevice = null;
+                    break;
+                case RELEASING:
+                    state.set(State.RELEASED);
+                    cameraDevice.close();
+                    Camera.this.cameraDevice = null;
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "onDisconnected() should not be possible from state: " + state.get());
+            }
+        }
+
+        private String getErrorMessage(int errorCode) {
+            switch (errorCode) {
+                case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE:
+                    return "ERROR_CAMERA_DEVICE";
+                case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED:
+                    return "ERROR_CAMERA_DISABLED";
+                case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE:
+                    return "ERROR_CAMERA_IN_USE";
+                case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE:
+                    return "ERROR_CAMERA_SERVICE";
+                case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE:
+                    return "ERROR_MAX_CAMERAS_IN_USE";
+                default: // fall out
+            }
+            return "UNKNOWN ERROR";
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+            Log.e(
+                    TAG,
+                    "CameraDevice.onError(): "
+                            + cameraDevice.getId()
+                            + " with error: "
+                            + getErrorMessage(error));
+            resetCaptureSession();
+            switch (state.get()) {
+                case CLOSING:
+                    state.set(State.INITIALIZED);
+                    Camera.this.cameraDevice = null;
+                    break;
+                case REOPENING:
+                case OPENED:
+                case OPENING:
+                    state.set(State.CLOSING);
+                    cameraDevice.close();
+                    Camera.this.cameraDevice = null;
+                    break;
+                case RELEASING:
+                    state.set(State.RELEASED);
+                    cameraDevice.close();
+                    Camera.this.cameraDevice = null;
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "onError() should not be possible from state: " + state.get());
+            }
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2AppConfiguration.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2AppConfiguration.java
new file mode 100644
index 0000000..ca2f6b9
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2AppConfiguration.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.content.Context;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.ExtendableUseCaseConfigFactory;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/**
+ * Convenience class for generating a pre-populated Camera2 {@link AppConfiguration}.
+ *
+ * @hide Until CameraX.init() is made public
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class Camera2AppConfiguration {
+
+    private Camera2AppConfiguration() {
+    }
+
+    /**
+     * Creates the {@link AppConfiguration} containing the Camera2 implementation pieces for
+     * CameraX.
+     */
+    public static AppConfiguration create(Context context) {
+        // Create the camera factory for creating Camera2 camera objects
+        CameraFactory cameraFactory = new Camera2CameraFactory(context);
+
+        // Create the DeviceSurfaceManager for Camera2
+        CameraDeviceSurfaceManager surfaceManager = new Camera2DeviceSurfaceManager(context);
+
+        // Create default configuration factory
+        ExtendableUseCaseConfigFactory configFactory = new ExtendableUseCaseConfigFactory();
+        configFactory.installDefaultProvider(
+                ImageAnalysisUseCaseConfiguration.class,
+                new DefaultImageAnalysisConfigurationProvider(cameraFactory));
+        configFactory.installDefaultProvider(
+                ImageCaptureUseCaseConfiguration.class,
+                new DefaultImageCaptureConfigurationProvider(cameraFactory));
+        configFactory.installDefaultProvider(
+                VideoCaptureUseCaseConfiguration.class,
+                new DefaultVideoCaptureConfigurationProvider(cameraFactory));
+        configFactory.installDefaultProvider(
+                ViewFinderUseCaseConfiguration.class,
+                new DefaultViewFinderConfigurationProvider(cameraFactory));
+
+        AppConfiguration.Builder appConfigBuilder =
+                new AppConfiguration.Builder()
+                        .setCameraFactory(cameraFactory)
+                        .setDeviceSurfaceManager(surfaceManager)
+                        .setUseCaseConfigFactory(configFactory);
+
+        return appConfigBuilder.build();
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraCaptureResult.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraCaptureResult.java
new file mode 100644
index 0000000..efddf0e
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraCaptureResult.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CaptureResult;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureMetaData.FlashState;
+import androidx.camera.core.CameraCaptureResult;
+
+/** The camera2 implementation for the capture result of a single image capture. */
+final class Camera2CameraCaptureResult implements CameraCaptureResult {
+    private static final String TAG = "Camera2CameraCaptureResult";
+
+    /** The actual camera2 {@link CaptureResult}. */
+    private final CaptureResult captureResult;
+
+    Camera2CameraCaptureResult(CaptureResult captureResult) {
+        this.captureResult = captureResult;
+    }
+
+    /**
+     * Converts the camera2 {@link CaptureResult#CONTROL_AF_MODE} to {@link AfMode}.
+     *
+     * @return the {@link AfMode}.
+     */
+    @NonNull
+    @Override
+    public AfMode getAfMode() {
+        Integer mode = captureResult.get(CaptureResult.CONTROL_AF_MODE);
+        if (mode == null) {
+            return AfMode.UNKNOWN;
+        }
+        switch (mode) {
+            case CaptureResult.CONTROL_AF_MODE_OFF:
+            case CaptureResult.CONTROL_AF_MODE_EDOF:
+                return AfMode.OFF;
+            case CaptureResult.CONTROL_AF_MODE_AUTO:
+            case CaptureResult.CONTROL_AF_MODE_MACRO:
+                return AfMode.ON_MANUAL_AUTO;
+            case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE:
+            case CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO:
+                return AfMode.ON_CONTINUOUS_AUTO;
+            default: // fall out
+        }
+        Log.e(TAG, "Undefined af mode: " + mode);
+        return AfMode.UNKNOWN;
+    }
+
+    /**
+     * Converts the camera2 {@link CaptureResult#CONTROL_AF_STATE} to {@link AfState}.
+     *
+     * @return the {@link AfState}.
+     */
+    @NonNull
+    @Override
+    public AfState getAfState() {
+        Integer state = captureResult.get(CaptureResult.CONTROL_AF_STATE);
+        if (state == null) {
+            return AfState.UNKNOWN;
+        }
+        switch (state) {
+            case CaptureResult.CONTROL_AF_STATE_INACTIVE:
+                return AfState.INACTIVE;
+            case CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN:
+            case CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN:
+            case CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED:
+                return AfState.SCANNING;
+            case CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED:
+                return AfState.LOCKED_FOCUSED;
+            case CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED:
+                return AfState.LOCKED_NOT_FOCUSED;
+            case CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED:
+                return AfState.FOCUSED;
+            default: // fall out
+        }
+        Log.e(TAG, "Undefined af state: " + state);
+        return AfState.UNKNOWN;
+    }
+
+    /**
+     * Converts the camera2 {@link CaptureResult#CONTROL_AE_STATE} to {@link AeState}.
+     *
+     * @return the {@link AeState}.
+     */
+    @NonNull
+    @Override
+    public AeState getAeState() {
+        Integer state = captureResult.get(CaptureResult.CONTROL_AE_STATE);
+        if (state == null) {
+            return AeState.UNKNOWN;
+        }
+        switch (state) {
+            case CaptureResult.CONTROL_AE_STATE_INACTIVE:
+                return AeState.INACTIVE;
+            case CaptureResult.CONTROL_AE_STATE_SEARCHING:
+            case CaptureResult.CONTROL_AE_STATE_PRECAPTURE:
+                return AeState.SEARCHING;
+            case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED:
+                return AeState.FLASH_REQUIRED;
+            case CaptureResult.CONTROL_AE_STATE_CONVERGED:
+                return AeState.CONVERGED;
+            case CaptureResult.CONTROL_AE_STATE_LOCKED:
+                return AeState.LOCKED;
+            default: // fall out
+        }
+        Log.e(TAG, "Undefined ae state: " + state);
+        return AeState.UNKNOWN;
+    }
+
+    /**
+     * Converts the camera2 {@link CaptureResult#CONTROL_AWB_STATE} to {@link AwbState}.
+     *
+     * @return the {@link AwbState}.
+     */
+    @NonNull
+    @Override
+    public AwbState getAwbState() {
+        Integer state = captureResult.get(CaptureResult.CONTROL_AWB_STATE);
+        if (state == null) {
+            return AwbState.UNKNOWN;
+        }
+        switch (state) {
+            case CaptureResult.CONTROL_AWB_STATE_INACTIVE:
+                return AwbState.INACTIVE;
+            case CaptureResult.CONTROL_AWB_STATE_SEARCHING:
+                return AwbState.METERING;
+            case CaptureResult.CONTROL_AWB_STATE_CONVERGED:
+                return AwbState.CONVERGED;
+            case CaptureResult.CONTROL_AWB_STATE_LOCKED:
+                return AwbState.LOCKED;
+            default: // fall out
+        }
+        Log.e(TAG, "Undefined awb state: " + state);
+        return AwbState.UNKNOWN;
+    }
+
+    /**
+     * Converts the camera2 {@link CaptureResult#FLASH_STATE} to {@link FlashState}.
+     *
+     * @return the {@link FlashState}.
+     */
+    @NonNull
+    @Override
+    public FlashState getFlashState() {
+        Integer state = captureResult.get(CaptureResult.FLASH_STATE);
+        if (state == null) {
+            return FlashState.UNKNOWN;
+        }
+        switch (state) {
+            case CaptureResult.FLASH_STATE_UNAVAILABLE:
+            case CaptureResult.FLASH_STATE_CHARGING:
+                return FlashState.NONE;
+            case CaptureResult.FLASH_STATE_READY:
+                return FlashState.READY;
+            case CaptureResult.FLASH_STATE_FIRED:
+            case CaptureResult.FLASH_STATE_PARTIAL:
+                return FlashState.FIRED;
+            default: // fall out
+        }
+        Log.e(TAG, "Undefined flash state: " + state);
+        return FlashState.UNKNOWN;
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraControl.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraControl.java
new file mode 100644
index 0000000..a557cf2
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraControl.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.MeteringRectangle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.OnFocusCompletedListener;
+import androidx.camera.core.SessionConfiguration;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A Camera2 implementation for CameraControl interface
+ *
+ * <p>It takes a {@link Camera2RequestRunner} for executing capture request and a {@link Handler} in
+ * which methods run.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class Camera2CameraControl implements CameraControl {
+    @VisibleForTesting
+    static final long FOCUS_TIMEOUT = 5000;
+    private static final String TAG = "Camera2CameraControl";
+    private final Camera2RequestRunner camera2RequestRunner;
+    private final Handler handler;
+    private final CameraControlSessionCallback sessionCallback = new CameraControlSessionCallback();
+    // use volatile modifier to make these variables in sync in all threads.
+    private volatile boolean isTorchOn = false;
+    private volatile boolean isFocusLocked = false;
+    private volatile FlashMode flashMode = FlashMode.OFF;
+    private volatile Rect cropRect = null;
+    private volatile MeteringRectangle afRect;
+    private volatile MeteringRectangle aeRect;
+    private volatile MeteringRectangle awbRect;
+    private volatile Integer currentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
+    private volatile OnFocusCompletedListener focusListener = null;
+    private volatile Handler focusListenerHandler = null;
+    private volatile CaptureResultListener sessionListenerForFocus = null;
+    private final Runnable handleFocusTimeoutRunnable =
+            () -> {
+                cancelFocus();
+
+                sessionCallback.removeListener(sessionListenerForFocus);
+
+                if (focusListener != null
+                        && currentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
+                    runInFocusListenerHandler(
+                            () -> focusListener.onFocusTimedOut(afRect.getRect()));
+                }
+            };
+    public Camera2CameraControl(Camera2RequestRunner camera2RequestRunner, Handler handler) {
+        this.camera2RequestRunner = camera2RequestRunner;
+        this.handler = handler;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setCropRegion(Rect crop) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> setCropRegion(crop));
+            return;
+        }
+
+        cropRect = crop;
+        camera2RequestRunner.updateRepeatingRequest();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void focus(
+            Rect focus,
+            Rect metering,
+            @Nullable OnFocusCompletedListener listener,
+            @Nullable Handler listenerHandler) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> focus(focus, metering, listener, listenerHandler));
+            return;
+        }
+
+        sessionCallback.removeListener(sessionListenerForFocus);
+
+        handler.removeCallbacks(handleFocusTimeoutRunnable);
+
+        afRect = new MeteringRectangle(focus, MeteringRectangle.METERING_WEIGHT_MAX);
+        aeRect = new MeteringRectangle(metering, MeteringRectangle.METERING_WEIGHT_MAX);
+        awbRect = new MeteringRectangle(metering, MeteringRectangle.METERING_WEIGHT_MAX);
+        Log.d(TAG, "Setting new AF rectangle: " + afRect);
+        Log.d(TAG, "Setting new AE rectangle: " + aeRect);
+        Log.d(TAG, "Setting new AWB rectangle: " + awbRect);
+
+        focusListener = listener;
+        focusListenerHandler =
+                (listenerHandler != null ? listenerHandler : new Handler(Looper.getMainLooper()));
+        currentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
+        isFocusLocked = true;
+
+        if (listener != null) {
+
+            sessionListenerForFocus =
+                    (result) -> {
+                        Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
+                        if (afState == null) {
+                            return false;
+                        }
+
+                        if (currentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
+                            if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED) {
+                                runInFocusListenerHandler(
+                                        () -> focusListener.onFocusLocked(afRect.getRect()));
+                                return true; // finished
+                            } else if (afState
+                                    == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
+                                runInFocusListenerHandler(
+                                        () -> focusListener.onFocusUnableToLock(afRect.getRect()));
+                                return true; // finished
+                            }
+                        }
+                        if (!currentAfState.equals(afState)) {
+                            currentAfState = afState;
+                        }
+                        return false; // continue checking
+                    };
+
+            sessionCallback.addListener(sessionListenerForFocus);
+        }
+        camera2RequestRunner.updateRepeatingRequest();
+
+        triggerAf();
+        if (FOCUS_TIMEOUT != 0) {
+            handler.postDelayed(handleFocusTimeoutRunnable, FOCUS_TIMEOUT);
+        }
+    }
+
+    private void runInFocusListenerHandler(Runnable runnable) {
+        if (focusListenerHandler != null) {
+            focusListenerHandler.post(runnable);
+        }
+    }
+
+    /** Cancels the focus operation. */
+    @VisibleForTesting
+    void cancelFocus() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> cancelFocus());
+            return;
+        }
+
+        handler.removeCallbacks(handleFocusTimeoutRunnable);
+
+        MeteringRectangle zeroRegion =
+                new MeteringRectangle(new Rect(), MeteringRectangle.METERING_WEIGHT_DONT_CARE);
+        afRect = zeroRegion;
+        aeRect = zeroRegion;
+        awbRect = zeroRegion;
+
+        // Send a single request to cancel af process
+        CaptureRequestConfiguration.Builder singleRequestBuilder =
+                new CaptureRequestConfiguration.Builder();
+        singleRequestBuilder.setTemplateType(getDefaultTemplate());
+        singleRequestBuilder.setUseRepeatingSurface(true);
+        singleRequestBuilder.addCharacteristic(
+                CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        camera2RequestRunner.submitSingleRequest(singleRequestBuilder.build());
+
+        isFocusLocked = false;
+        camera2RequestRunner.updateRepeatingRequest();
+    }
+
+    private void updateRepeatingRequest() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> updateRepeatingRequest());
+            return;
+        }
+
+        camera2RequestRunner.updateRepeatingRequest();
+    }
+
+    @Override
+    public FlashMode getFlashMode() {
+        return flashMode;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setFlashMode(FlashMode flashMode) {
+        // update flashMode immediately so that following getFlashMode() returns correct value.
+        this.flashMode = flashMode;
+
+        updateRepeatingRequest();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void enableTorch(boolean torch) {
+        // update isTorchOn immediately so that following isTorchOn() returns correct value.
+        isTorchOn = torch;
+        enableTorchInternal(torch);
+    }
+
+    private void enableTorchInternal(boolean torch) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> enableTorchInternal(torch));
+            return;
+        }
+
+        if (!torch) {
+            CaptureRequestConfiguration.Builder singleRequestBuilder =
+                    new CaptureRequestConfiguration.Builder();
+            singleRequestBuilder.setTemplateType(getDefaultTemplate());
+            singleRequestBuilder.addCharacteristic(
+                    CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+            singleRequestBuilder.setUseRepeatingSurface(true);
+
+            camera2RequestRunner.submitSingleRequest(singleRequestBuilder.build());
+        }
+        camera2RequestRunner.updateRepeatingRequest();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isTorchOn() {
+        return isTorchOn;
+    }
+
+    @Override
+    public boolean isFocusLocked() {
+        return isFocusLocked;
+    }
+
+    /**
+     * Issues a {@link CaptureRequest#CONTROL_AF_TRIGGER_START} request to start auto focus scan.
+     */
+    @Override
+    public void triggerAf() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> triggerAf());
+            return;
+        }
+
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.setTemplateType(getDefaultTemplate());
+        builder.setUseRepeatingSurface(true);
+        builder.addCharacteristic(
+                CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);
+        camera2RequestRunner.submitSingleRequest(builder.build());
+    }
+
+    /**
+     * Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
+     * exposure scan.
+     */
+    @Override
+    public void triggerAePrecapture() {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> triggerAePrecapture());
+            return;
+        }
+
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+        builder.setTemplateType(getDefaultTemplate());
+        builder.setUseRepeatingSurface(true);
+        builder.addCharacteristic(
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+        camera2RequestRunner.submitSingleRequest(builder.build());
+    }
+
+    /**
+     * Issues {@link CaptureRequest#CONTROL_AF_TRIGGER_CANCEL} or {@link
+     * CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL} request to cancel auto focus or auto
+     * exposure scan.
+     */
+    @Override
+    public void cancelAfAeTrigger(boolean cancelAfTrigger, boolean cancelAePrecaptureTrigger) {
+        if (Looper.myLooper() != handler.getLooper()) {
+            handler.post(() -> cancelAfAeTrigger(cancelAfTrigger, cancelAePrecaptureTrigger));
+            return;
+        }
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+        builder.setUseRepeatingSurface(true);
+        builder.setTemplateType(getDefaultTemplate());
+        if (cancelAfTrigger) {
+            builder.addCharacteristic(
+                    CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        }
+        if (cancelAePrecaptureTrigger) {
+            builder.addCharacteristic(
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+        }
+        camera2RequestRunner.submitSingleRequest(builder.build());
+    }
+
+    private int getDefaultTemplate() {
+        return CameraDevice.TEMPLATE_PREVIEW;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SessionConfiguration getControlSessionConfiguration() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.setTemplateType(getDefaultTemplate());
+        builder.setCameraCaptureCallback(CaptureCallbackContainer.create(sessionCallback));
+
+        Camera2Configuration.Builder requestOptionBuilder = new Camera2Configuration.Builder();
+
+        if (isTorchOn) {
+            requestOptionBuilder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+            requestOptionBuilder.setCaptureRequestOption(
+                    CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
+        } else {
+            int aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
+            switch (flashMode) {
+                case OFF:
+                    aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
+                    break;
+                case ON:
+                    aeMode = CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+                    break;
+                case AUTO:
+                    aeMode = CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH;
+                    break;
+            }
+            requestOptionBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, aeMode);
+        }
+
+        // also apply the common option for single requests.
+        Configuration singleRequestImpOptions = getSingleRequestImplOptions();
+        requestOptionBuilder.insertAllOptions(singleRequestImpOptions);
+        builder.setImplementationOptions(requestOptionBuilder.build());
+
+        return builder.build();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Configuration getSingleRequestImplOptions() {
+        Camera2Configuration.Builder builder = new Camera2Configuration.Builder();
+
+        builder.setCaptureRequestOption(
+                CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
+
+        builder.setCaptureRequestOption(
+                CaptureRequest.CONTROL_AF_MODE,
+                isFocusLocked()
+                        ? CaptureRequest.CONTROL_AF_MODE_AUTO
+                        : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+
+        if (isTorchOn) {
+            // In case some random single request turns off the torch by accident, attach FLASH_MODE
+            // and
+            // CONTROL_AE_MODE_ON to all single requests.
+            builder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
+            builder.setCaptureRequestOption(
+                    CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
+        }
+        // Turning off Flash requires a single request of AE mode set to CONTROL_AE_MODE_ON. This is
+        // the reason why we do not specify AE mode by default for single request.
+
+        builder.setCaptureRequestOption(
+                CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
+
+        if (afRect != null) {
+            builder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{afRect});
+        }
+        if (aeRect != null) {
+            builder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{aeRect});
+        }
+        if (awbRect != null) {
+            builder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{awbRect});
+        }
+
+        if (cropRect != null) {
+            builder.setCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, cropRect);
+        }
+
+        return builder.build();
+    }
+
+    /** An interface to listen to camera capture results. */
+    private interface CaptureResultListener {
+        /**
+         * Callback to handle camera capture results.
+         *
+         * @param captureResult camera capture result.
+         * @return true to finish listening, false to continue listening.
+         */
+        boolean onCaptureResult(TotalCaptureResult captureResult);
+    }
+
+    static final class CameraControlSessionCallback extends CaptureCallback {
+
+        private final Set<CaptureResultListener> resultListeners = new HashSet<>();
+
+        public void addListener(CaptureResultListener listener) {
+            synchronized (resultListeners) {
+                resultListeners.add(listener);
+            }
+        }
+
+        public void removeListener(CaptureResultListener listener) {
+            if (listener == null) {
+                return;
+            }
+            synchronized (resultListeners) {
+                resultListeners.remove(listener);
+            }
+        }
+
+        @Override
+        public void onCaptureCompleted(
+                @NonNull CameraCaptureSession session,
+                @NonNull CaptureRequest request,
+                @NonNull TotalCaptureResult result) {
+            Set<CaptureResultListener> listeners;
+            synchronized (resultListeners) {
+                if (resultListeners.isEmpty()) {
+                    return;
+                }
+                listeners = new HashSet<>(resultListeners);
+            }
+
+            Set<CaptureResultListener> removeSet = new HashSet<>();
+            for (CaptureResultListener listener : listeners) {
+                boolean isFinished = listener.onCaptureResult(result);
+                if (isFinished) {
+                    removeSet.add(listener);
+                }
+            }
+
+            if (!removeSet.isEmpty()) {
+                synchronized (resultListeners) {
+                    resultListeners.removeAll(removeSet);
+                }
+            }
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraFactory.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraFactory.java
new file mode 100644
index 0000000..32d3596
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraFactory.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.CameraXThreads;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/** The factory class that creates {@link androidx.camera.camera2.Camera} instances. */
+final class Camera2CameraFactory implements CameraFactory {
+    private static final String TAG = "Camera2CameraFactory";
+
+    private static final HandlerThread handlerThread = new HandlerThread(CameraXThreads.TAG);
+    private static final Handler handler;
+
+    static {
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+    }
+
+    private final CameraManager cameraManager;
+
+    Camera2CameraFactory(Context context) {
+        cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+    }
+
+    @Override
+    public BaseCamera getCamera(String cameraId) {
+        return new Camera(cameraManager, cameraId, handler);
+    }
+
+    @Override
+    public Set<String> getAvailableCameraIds() throws CameraInfoUnavailableException {
+        List<String> camerasList = null;
+        try {
+            camerasList = Arrays.asList(cameraManager.getCameraIdList());
+        } catch (CameraAccessException e) {
+            throw new CameraInfoUnavailableException(
+                    "Unable to retrieve list of cameras on device.", e);
+        }
+        // Use a LinkedHashSet to preserve order
+        return new LinkedHashSet<>(camerasList);
+    }
+
+    @Nullable
+    @Override
+    public String cameraIdForLensFacing(LensFacing lensFacing)
+            throws CameraInfoUnavailableException {
+        Set<String> cameraIds = getAvailableCameraIds();
+
+        // Convert to from CameraX enum to Camera2 CameraMetadata
+        Integer lensFacingInteger = -1;
+        switch (lensFacing) {
+            case BACK:
+                lensFacingInteger = CameraMetadata.LENS_FACING_BACK;
+                break;
+            case FRONT:
+                lensFacingInteger = CameraMetadata.LENS_FACING_FRONT;
+                break;
+        }
+
+        for (String cameraId : cameraIds) {
+            CameraCharacteristics characteristics = null;
+            try {
+                characteristics = cameraManager.getCameraCharacteristics(cameraId);
+            } catch (CameraAccessException e) {
+                throw new CameraInfoUnavailableException(
+                        "Unable to retrieve info for camera with id " + cameraId + ".", e);
+            }
+            Integer cameraLensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
+            if (cameraLensFacing == null) {
+                continue;
+            }
+            if (cameraLensFacing.equals(lensFacingInteger)) {
+                return cameraId;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraInfo.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraInfo.java
new file mode 100644
index 0000000..fd74c37
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraInfo.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraOrientationUtil;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/** Implementation of the {@link CameraInfo} interface that exposes parameters through camera2. */
+final class Camera2CameraInfo implements CameraInfo {
+
+    private final CameraCharacteristics cameraCharacteristics;
+
+    Camera2CameraInfo(CameraManager cameraManager, String cameraId)
+            throws CameraInfoUnavailableException {
+        try {
+            cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
+        } catch (CameraAccessException e) {
+            throw new CameraInfoUnavailableException(
+                    "Unable to retrieve info for camera " + cameraId, e);
+        }
+
+        checkCharacteristicAvailable(
+                CameraCharacteristics.SENSOR_ORIENTATION, "Sensor orientation");
+        checkCharacteristicAvailable(CameraCharacteristics.LENS_FACING, "Lens facing direction");
+    }
+
+    @Nullable
+    @Override
+    public LensFacing getLensFacing() {
+        switch (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)) {
+            case CameraCharacteristics.LENS_FACING_FRONT:
+                return LensFacing.FRONT;
+            case CameraCharacteristics.LENS_FACING_BACK:
+                return LensFacing.BACK;
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public int getSensorRotationDegrees(@RotationValue int relativeRotation) {
+        int relativeRotationDegrees =
+                CameraOrientationUtil.surfaceRotationToDegrees(relativeRotation);
+        // Currently this assumes that a back-facing camera is always opposite to the screen.
+        // This may not be the case for all devices, so in the future we may need to handle that
+        // scenario.
+        boolean isOppositeFacingScreen = LensFacing.BACK.equals(getLensFacing());
+        return CameraOrientationUtil.getRelativeImageRotation(
+                relativeRotationDegrees,
+                cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION),
+                isOppositeFacingScreen);
+    }
+
+    private void checkCharacteristicAvailable(CameraCharacteristics.Key<?> key, String readableName)
+            throws CameraInfoUnavailableException {
+        if (cameraCharacteristics.get(key) == null) {
+            throw new CameraInfoUnavailableException(
+                    "Camera characteristics map is missing value for characteristic: "
+                            + readableName);
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacks.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacks.java
new file mode 100644
index 0000000..6f678ed
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacks.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraCaptureSession.CaptureCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class Camera2CaptureSessionCaptureCallbacks {
+    private Camera2CaptureSessionCaptureCallbacks() {
+    }
+
+    /** Returns a session capture callback which does nothing. */
+    public static CameraCaptureSession.CaptureCallback createNoOpCallback() {
+        return new NoOpSessionCaptureCallback();
+    }
+
+    /** Returns a session capture callback which calls a list of other callbacks. */
+    static CameraCaptureSession.CaptureCallback createComboCallback(
+            List<CameraCaptureSession.CaptureCallback> callbacks) {
+        return new ComboSessionCaptureCallback(callbacks);
+    }
+
+    /** Returns a session capture callback which calls a list of other callbacks. */
+    public static CameraCaptureSession.CaptureCallback createComboCallback(
+            CameraCaptureSession.CaptureCallback... callbacks) {
+        return createComboCallback(Arrays.asList(callbacks));
+    }
+
+    static final class NoOpSessionCaptureCallback
+            extends CameraCaptureSession.CaptureCallback {
+        @Override
+        public void onCaptureBufferLost(
+                CameraCaptureSession session,
+                CaptureRequest request,
+                Surface surface,
+                long frame) {
+        }
+
+        @Override
+        public void onCaptureCompleted(
+                CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+        }
+
+        @Override
+        public void onCaptureFailed(
+                CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+        }
+
+        @Override
+        public void onCaptureProgressed(
+                CameraCaptureSession session,
+                CaptureRequest request,
+                CaptureResult partialResult) {
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(
+                CameraCaptureSession session, int sequenceId, long frame) {
+        }
+
+        @Override
+        public void onCaptureStarted(
+                CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+        }
+    }
+
+    private static final class ComboSessionCaptureCallback
+            extends CameraCaptureSession.CaptureCallback {
+        private final List<CameraCaptureSession.CaptureCallback> callbacks = new ArrayList<>();
+
+        ComboSessionCaptureCallback(List<CameraCaptureSession.CaptureCallback> callbacks) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                // A no-op callback doesn't do anything, so avoid adding it to the final list.
+                if (!(callback instanceof NoOpSessionCaptureCallback)) {
+                    this.callbacks.add(callback);
+                }
+            }
+        }
+
+        @Override
+        public void onCaptureBufferLost(
+                CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                callback.onCaptureBufferLost(session, request, surface, frame);
+            }
+        }
+
+        @Override
+        public void onCaptureCompleted(
+                CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                callback.onCaptureCompleted(session, request, result);
+            }
+        }
+
+        @Override
+        public void onCaptureFailed(
+                CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                callback.onCaptureFailed(session, request, failure);
+            }
+        }
+
+        @Override
+        public void onCaptureProgressed(
+                CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                callback.onCaptureProgressed(session, request, partialResult);
+            }
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                callback.onCaptureSequenceAborted(session, sequenceId);
+            }
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(
+                CameraCaptureSession session, int sequenceId, long frame) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                callback.onCaptureSequenceCompleted(session, sequenceId, frame);
+            }
+        }
+
+        @Override
+        public void onCaptureStarted(
+                CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+            for (CameraCaptureSession.CaptureCallback callback : callbacks) {
+                callback.onCaptureStarted(session, request, timestamp, frame);
+            }
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Configuration.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Configuration.java
new file mode 100644
index 0000000..c49cded
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Configuration.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.MutableConfiguration;
+import androidx.camera.core.MutableOptionsBundle;
+import androidx.camera.core.OptionsBundle;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** Configuration options related to the {@link android.hardware.camera2} APIs. */
+public final class Camera2Configuration implements Configuration.Reader {
+
+    static final String CAPTURE_REQUEST_ID_STEM = "camera2.captureRequest.option.";
+    static final Option<Integer> TEMPLATE_TYPE_OPTION =
+            Option.create("camera2.captureRequest.templateType", int.class);
+    static final Option<StateCallback> DEVICE_STATE_CALLBACK_OPTION =
+            Option.create("camera2.cameraDevice.stateCallback", StateCallback.class);
+    static final Option<CameraCaptureSession.StateCallback> SESSION_STATE_CALLBACK_OPTION =
+            Option.create(
+                    "camera2.cameraCaptureSession.stateCallback",
+                    CameraCaptureSession.StateCallback.class);
+    static final Option<CaptureCallback> SESSION_CAPTURE_CALLBACK_OPTION =
+            Option.create("camera2.cameraCaptureSession.captureCallback", CaptureCallback.class);
+    private final Configuration config;
+
+    /**
+     * Creates a Camera2Configuration for reading Camera2 options from the given config.
+     *
+     * @param config The config that potentially contains Camera2 options.
+     */
+    public Camera2Configuration(Configuration config) {
+        this.config = config;
+    }
+
+    // Unforunately, we can't get the Class<T> from the CaptureRequest.Key, so we're forced to erase
+    // the type. This shouldn't be a problem as long as we are only using these options within the
+    // Camera2Configuration and Camera2Configuration.Builder classes.
+    static Option<Object> createCaptureRequestOption(CaptureRequest.Key<?> key) {
+        return Option.create(CAPTURE_REQUEST_ID_STEM + key.getName(), Object.class, key);
+    }
+
+    /**
+     * Returns a value for the given {@link CaptureRequest.Key}.
+     *
+     * @param key            The key to retrieve.
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @param <ValueT>       The type of the value.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public <ValueT> ValueT getCaptureRequestOption(
+            CaptureRequest.Key<ValueT> key, @Nullable ValueT valueIfMissing) {
+        @SuppressWarnings(
+                "unchecked") // Type should have been only set via Builder#setCaptureRequestOption()
+                Option<ValueT> opt =
+                (Option<ValueT>) Camera2Configuration.createCaptureRequestOption(key);
+        return getConfiguration().retrieveOption(opt, valueIfMissing);
+    }
+
+    /**
+     * Returns all capture request options contained in this configuration
+     *
+     * @hide
+     */
+    Set<Option<?>> getCaptureRequestOptions() {
+        Set<Option<?>> optionSet = new HashSet<>();
+        findOptions(
+                Camera2Configuration.CAPTURE_REQUEST_ID_STEM,
+                option -> {
+                    optionSet.add(option);
+                    return true;
+                });
+        return optionSet;
+    }
+
+    /**
+     * Returns the CameraDevice template from the given configuration.
+     *
+     * <p>See {@link CameraDevice} for valid template types. For example, {@link
+     * CameraDevice#TEMPLATE_PREVIEW}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    int getCaptureRequestTemplate(int valueIfMissing) {
+        return getConfiguration().retrieveOption(TEMPLATE_TYPE_OPTION, valueIfMissing);
+    }
+
+    /**
+     * Returns the stored {@link CameraDevice.StateCallback}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public CameraDevice.StateCallback getDeviceStateCallback(
+            CameraDevice.StateCallback valueIfMissing) {
+        return getConfiguration().retrieveOption(DEVICE_STATE_CALLBACK_OPTION, valueIfMissing);
+    }
+
+    /**
+     * Returns the stored {@link CameraCaptureSession.StateCallback}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public CameraCaptureSession.StateCallback getSessionStateCallback(
+            CameraCaptureSession.StateCallback valueIfMissing) {
+        return getConfiguration().retrieveOption(SESSION_STATE_CALLBACK_OPTION, valueIfMissing);
+    }
+
+    // Option Declarations:
+    // ***********************************************************************************************
+
+    /**
+     * Returns the stored {@link CameraCaptureSession.CaptureCallback}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public CameraCaptureSession.CaptureCallback getSessionCaptureCallback(
+            CameraCaptureSession.CaptureCallback valueIfMissing) {
+        return getConfiguration().retrieveOption(SESSION_CAPTURE_CALLBACK_OPTION, valueIfMissing);
+    }
+
+    /**
+     * Returns the underlying immutable {@link Configuration} object.
+     *
+     * @return The underlying {@link Configuration} object.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** Extends a {@link Configuration.Builder} to add Camera2 options. */
+    public static final class Extender {
+
+        Configuration.Builder<?, ?> baseBuilder;
+
+        /**
+         * Creates an Extender that can be used to add Camera2 options to another Builder.
+         *
+         * @param baseBuilder The builder being extended.
+         */
+        public Extender(Configuration.Builder<?, ?> baseBuilder) {
+            this.baseBuilder = baseBuilder;
+        }
+
+        /**
+         * Sets a {@link CaptureRequest.Key} and Value on the configuration.
+         *
+         * @param key      The {@link CaptureRequest.Key} which will be set.
+         * @param value    The value for the key.
+         * @param <ValueT> The type of the value.
+         * @return The current Extender.
+         */
+        public <ValueT> Extender setCaptureRequestOption(
+                CaptureRequest.Key<ValueT> key, ValueT value) {
+            // Reify the type so we can obtain the class
+            Option<Object> opt = Camera2Configuration.createCaptureRequestOption(key);
+            baseBuilder.insertOption(opt, value);
+            return this;
+        }
+
+        /**
+         * Sets a CameraDevice template on the given configuration.
+         *
+         * <p>See {@link CameraDevice} for valid template types. For example, {@link
+         * CameraDevice#TEMPLATE_PREVIEW}.
+         *
+         * @param templateType The template type to set.
+         * @return The current Extender.
+         */
+        Extender setCaptureRequestTemplate(int templateType) {
+            baseBuilder.insertOption(TEMPLATE_TYPE_OPTION, templateType);
+            return this;
+        }
+
+        /**
+         * Sets a {@link CameraDevice.StateCallback}.
+         *
+         * <p>The caller is expected to use the {@link CameraDevice} instance accessed through the
+         * callback methods responsibly. Generally safe usages include: (1) querying the device for
+         * its id, (2) using the callbacks to determine what state the device is currently in.
+         * Generally unsafe usages include: (1) creating a new {@link CameraCaptureSession}, (2)
+         * creating a new {@link CaptureRequest}, (3) closing the device. When the caller uses the
+         * device beyond the safe usage limits, the usage may still work in conjunction with
+         * CameraX, but any strong guarantees provided by CameraX about the validity of the camera
+         * state become void.
+         *
+         * @param stateCallback The {@link CameraDevice.StateCallback}.
+         * @return The current Extender.
+         */
+        public Extender setDeviceStateCallback(CameraDevice.StateCallback stateCallback) {
+            baseBuilder.insertOption(DEVICE_STATE_CALLBACK_OPTION, stateCallback);
+            return this;
+        }
+
+        /**
+         * Sets a {@link CameraCaptureSession.StateCallback}.
+         *
+         * <p>The caller is expected to use the {@link CameraCaptureSession} instance accessed
+         * through the callback methods responsibly. Generally safe usages include: (1) querying the
+         * session for its properties, (2) using the callbacks to determine what state the session
+         * is currently in. Generally unsafe usages include: (1) submitting a new {@link
+         * CaptureRequest}, (2) stopping an existing {@link CaptureRequest}, (3) closing the
+         * session, (4) attaching a new {@link Surface} to the session. When the caller uses the
+         * session beyond the safe usage limits, the usage may still work in conjunction with
+         * CameraX, but any strong gurantees provided by CameraX about the validity of the camera
+         * state become void.
+         *
+         * @param stateCallback The {@link CameraCaptureSession.StateCallback}.
+         * @return The current Extender.
+         */
+        public Extender setSessionStateCallback(CameraCaptureSession.StateCallback stateCallback) {
+            baseBuilder.insertOption(SESSION_STATE_CALLBACK_OPTION, stateCallback);
+            return this;
+        }
+
+        /**
+         * Sets a {@link CameraCaptureSession.CaptureCallback}.
+         *
+         * <p>The caller is expected to use the {@link CameraCaptureSession} instance accessed
+         * through the callback methods responsibly. Generally safe usages include: (1) querying the
+         * session for its properties. Generally unsafe usages include: (1) submitting a new {@link
+         * CaptureRequest}, (2) stopping an existing {@link CaptureRequest}, (3) closing the
+         * session, (4) attaching a new {@link Surface} to the session. When the caller uses the
+         * session beyond the safe usage limits, the usage may still work in conjunction with
+         * CameraX, but any strong gurantees provided by CameraX about the validity of the camera
+         * state become void.
+         *
+         * <p>The caller is generally free to use the {@link CaptureRequest} and {@link
+         * CaptureResult} instances accessed through the callback methods.
+         *
+         * @param captureCallback The {@link CameraCaptureSession.CaptureCallback}.
+         * @return The current Extender.
+         */
+        public Extender setSessionCaptureCallback(
+                CameraCaptureSession.CaptureCallback captureCallback) {
+            baseBuilder.insertOption(SESSION_CAPTURE_CALLBACK_OPTION, captureCallback);
+            return this;
+        }
+    }
+
+    /**
+     * Builder for creating {@link Camera2Configuration} instance.
+     *
+     * <p>Use {@link Builder} for creating {@link Configuration} which contains camera2 options
+     * only. And use {@link Extender} to add Camera2 options on existing other {@link
+     * Configuration.Builder}.
+     *
+     * @hide
+     */
+    static final class Builder implements Configuration.Builder<Camera2Configuration, Builder> {
+
+        private final MutableOptionsBundle mutableOptionsBundle = MutableOptionsBundle.create();
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mutableOptionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        public <ValueT> Builder setCaptureRequestOption(
+                CaptureRequest.Key<ValueT> key, ValueT value) {
+            Option<Object> opt = Camera2Configuration.createCaptureRequestOption(key);
+            insertOption(opt, value);
+            return this;
+        }
+
+        public Builder insertAllOptions(Configuration configuration) {
+            for (Option<?> option : configuration.listOptions()) {
+                @SuppressWarnings("unchecked") // Options/values are being copied directly
+                        Option<Object> objectOpt = (Option<Object>) option;
+                insertOption(objectOpt, configuration.retrieveOption(objectOpt));
+            }
+            return this;
+        }
+
+        @Override
+        public Camera2Configuration build() {
+            return new Camera2Configuration(OptionsBundle.from(mutableOptionsBundle));
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2DeviceSurfaceManager.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2DeviceSurfaceManager.java
new file mode 100644
index 0000000..8e595b1
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2DeviceSurfaceManager.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraManager;
+import android.media.CamcorderProfile;
+import android.util.Size;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.SurfaceConfiguration;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Camera device manager to provide the guaranteed supported stream capabilities related info for
+ * all camera devices
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices. This structure is used to store the guaranteed supported stream capabilities related
+ * info.
+ *
+ * @hide Implementation detail
+ */
+final class Camera2DeviceSurfaceManager implements CameraDeviceSurfaceManager {
+    private static final String TAG = "Camera2DeviceSurfaceManager";
+    private static final Size MAXIMUM_PREVIEW_SIZE = new Size(1920, 1080);
+    private final Map<String, SupportedSurfaceCombination> cameraSupportedSurfaceCombinationMap =
+            new HashMap<>();
+    private boolean isInitialized = false;
+
+    public Camera2DeviceSurfaceManager(Context context) {
+        init(context, CamcorderProfile::hasProfile);
+    }
+
+    @VisibleForTesting
+    Camera2DeviceSurfaceManager(Context context, CamcorderProfileHelper camcorderProfileHelper) {
+        init(context, camcorderProfileHelper);
+    }
+
+    /**
+     * Check whether the input surface configuration list is under the capability of any combination
+     * of this object.
+     *
+     * @param cameraId                 the camera id of the camera device to be compared
+     * @param surfaceConfigurationList the surface configuration list to be compared
+     * @return the check result that whether it could be supported
+     */
+    @Override
+    public boolean checkSupported(
+            String cameraId, List<SurfaceConfiguration> surfaceConfigurationList) {
+        boolean isSupported = false;
+
+        if (!isInitialized) {
+            throw new IllegalStateException("Camera2DeviceSurfaceManager is not initialized.");
+        }
+
+        if (surfaceConfigurationList == null || surfaceConfigurationList.isEmpty()) {
+            return true;
+        }
+
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+        if (supportedSurfaceCombination != null) {
+            isSupported = supportedSurfaceCombination.checkSupported(surfaceConfigurationList);
+        }
+
+        return isSupported;
+    }
+
+    /**
+     * Transform to a SurfaceConfiguration object with cameraId, image format and size info
+     *
+     * @param cameraId    the camera id of the camera device to transform the object
+     * @param imageFormat the image format info for the surface configuration object
+     * @param size        the size info for the surface configuration object
+     * @return new {@link SurfaceConfiguration} object
+     */
+    @Override
+    public SurfaceConfiguration transformSurfaceConfiguration(
+            String cameraId, int imageFormat, Size size) {
+        SurfaceConfiguration surfaceConfiguration = null;
+
+        if (!isInitialized) {
+            throw new IllegalStateException("Camera2DeviceSurfaceManager is not initialized.");
+        }
+
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+        if (supportedSurfaceCombination != null) {
+            surfaceConfiguration =
+                    supportedSurfaceCombination.transformSurfaceConfiguration(imageFormat, size);
+        }
+
+        return surfaceConfiguration;
+    }
+
+    /**
+     * Retrieves a map of suggested resolutions for the given list of use cases.
+     *
+     * @param cameraId         the camera id of the camera device used by the use cases
+     * @param originalUseCases list of use cases with existing surfaces
+     * @param newUseCases      list of new use cases
+     * @return map of suggested resolutions for given use cases
+     */
+    @Override
+    public Map<BaseUseCase, Size> getSuggestedResolutions(
+            String cameraId, List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+
+        if (newUseCases == null || newUseCases.isEmpty()) {
+            throw new IllegalArgumentException("No new use cases to be bound.");
+        }
+
+        UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(originalUseCases, newUseCases);
+
+        // Use the small size (640x480) for new use cases to check whether there is any possible
+        // supported combination first
+        List<SurfaceConfiguration> surfaceConfigurations = new ArrayList<>();
+
+        if (originalUseCases != null) {
+            for (BaseUseCase useCase : originalUseCases) {
+                CameraDeviceConfiguration configuration =
+                        (CameraDeviceConfiguration) useCase.getUseCaseConfiguration();
+                String useCaseCameraId;
+                try {
+                    useCaseCameraId =
+                            CameraX.getCameraWithLensFacing(configuration.getLensFacing());
+                } catch (Exception e) {
+                    throw new IllegalArgumentException(
+                            "Unable to get camera ID for use case " + useCase.getName(), e);
+                }
+                Size resolution = useCase.getAttachedSurfaceResolution(useCaseCameraId);
+
+                surfaceConfigurations.add(
+                        transformSurfaceConfiguration(
+                                cameraId, useCase.getImageFormat(), resolution));
+            }
+        }
+
+        for (BaseUseCase useCase : newUseCases) {
+            surfaceConfigurations.add(
+                    transformSurfaceConfiguration(
+                            cameraId, useCase.getImageFormat(), new Size(640, 480)));
+        }
+
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+        if (supportedSurfaceCombination == null
+                || !supportedSurfaceCombination.checkSupported(surfaceConfigurations)) {
+            throw new IllegalArgumentException(
+                    "No supported surface combination is found for camera device - Id : "
+                            + cameraId);
+        }
+
+        return supportedSurfaceCombination.getSuggestedResolutions(originalUseCases, newUseCases);
+    }
+
+    private void init(Context context, CamcorderProfileHelper camcorderProfileHelper) {
+        if (!isInitialized) {
+            CameraManager cameraManager =
+                    (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+
+            try {
+                for (String cameraId : cameraManager.getCameraIdList()) {
+                    cameraSupportedSurfaceCombinationMap.put(
+                            cameraId,
+                            new SupportedSurfaceCombination(
+                                    context, cameraId, camcorderProfileHelper));
+                }
+            } catch (CameraAccessException e) {
+                throw new IllegalArgumentException("Fail to get camera id list", e);
+            }
+
+            isInitialized = true;
+        }
+    }
+
+    /**
+     * Get max supported output size for specific camera device and image format
+     *
+     * @param cameraId    the camera Id
+     * @param imageFormat the image format info
+     * @return the max supported output size for the image format
+     */
+    @Override
+    public Size getMaxOutputSize(String cameraId, int imageFormat) {
+        if (!isInitialized) {
+            throw new IllegalStateException("CameraDeviceSurfaceManager is not initialized.");
+        }
+
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                cameraSupportedSurfaceCombinationMap.get(cameraId);
+
+        if (supportedSurfaceCombination == null) {
+            throw new IllegalArgumentException(
+                    "Fail to find supported surface info - CameraId:" + cameraId);
+        }
+
+        return supportedSurfaceCombination.getMaxOutputSizeByFormat(imageFormat);
+    }
+
+    /**
+     * Retrieves the preview size, choosing the smaller of the display size and 1080P.
+     *
+     * @return preview size from {@link androidx.camera.core.SurfaceSizeDefinition}
+     */
+    @Override
+    public Size getPreviewSize() {
+        if (!isInitialized) {
+            throw new IllegalStateException("CameraDeviceSurfaceManager is not initialized.");
+        }
+
+        // 1920x1080 is maximum preview size
+        Size previewSize = MAXIMUM_PREVIEW_SIZE;
+
+        if (!cameraSupportedSurfaceCombinationMap.isEmpty()) {
+            // Preview size depends on the display size and 1080P. Therefore, we can get the first
+            // camera
+            // device's preview size to return it.
+            String cameraId = (String) cameraSupportedSurfaceCombinationMap.keySet().toArray()[0];
+            previewSize =
+                    cameraSupportedSurfaceCombinationMap
+                            .get(cameraId)
+                            .getSurfaceSizeDefinition()
+                            .getPreviewSize();
+        }
+
+        return previewSize;
+    }
+
+    enum Operation {
+        ADD_CONFIG,
+        REMOVE_CONFIG
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Initializer.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Initializer.java
new file mode 100644
index 0000000..dc37f7e
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Initializer.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.CameraX;
+
+/**
+ * A {@link ContentProvider} used to initialize {@link CameraX} from a {@link Context}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class Camera2Initializer extends ContentProvider {
+    private static final String TAG = "Camera2Initializer";
+
+    @Override
+    public boolean onCreate() {
+        Log.d(TAG, "CameraX initializing with Camera2 ...");
+
+        CameraX.init(getContext(), Camera2AppConfiguration.create(getContext()));
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(
+            Uri uri,
+            @Nullable String[] strings,
+            @Nullable String s,
+            @Nullable String[] strings1,
+            @Nullable String s1) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(Uri uri, @Nullable ContentValues contentValues) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, @Nullable String s, @Nullable String[] strings) {
+        return 0;
+    }
+
+    @Override
+    public int update(
+            Uri uri,
+            @Nullable ContentValues contentValues,
+            @Nullable String s,
+            @Nullable String[] strings) {
+        return 0;
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2OptionUnpacker.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2OptionUnpacker.java
new file mode 100644
index 0000000..e8df444
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2OptionUnpacker.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks;
+import androidx.camera.core.CameraCaptureSessionStateCallbacks;
+import androidx.camera.core.CameraDeviceStateCallbacks;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.Configuration.Option;
+import androidx.camera.core.OptionsBundle;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseConfiguration;
+
+/**
+ * A {@link SessionConfiguration.OptionUnpacker} implementation for unpacking Camera2 options into a
+ * {@link SessionConfiguration.Builder}.
+ */
+final class Camera2OptionUnpacker implements SessionConfiguration.OptionUnpacker {
+
+    static final Camera2OptionUnpacker INSTANCE = new Camera2OptionUnpacker();
+
+    @Override
+    public void unpack(UseCaseConfiguration<?> config, SessionConfiguration.Builder builder) {
+        SessionConfiguration defaultSessionConfig =
+                config.getDefaultSessionConfiguration(/*valueIfMissing=*/ null);
+
+        CameraDevice.StateCallback deviceStateCallback =
+                CameraDeviceStateCallbacks.createNoOpCallback();
+        CameraCaptureSession.StateCallback sessionStateCallback =
+                CameraCaptureSessionStateCallbacks.createNoOpCallback();
+        CameraCaptureCallback cameraCaptureCallback = CameraCaptureCallbacks.createNoOpCallback();
+        Configuration implOptions = OptionsBundle.emptyBundle();
+        int templateType =
+                SessionConfiguration.defaultEmptySessionConfiguration().getTemplateType();
+
+        // Apply/extract defaults from session config
+        if (defaultSessionConfig != null) {
+            templateType = defaultSessionConfig.getTemplateType();
+            deviceStateCallback = defaultSessionConfig.getDeviceStateCallback();
+            sessionStateCallback = defaultSessionConfig.getSessionStateCallback();
+            cameraCaptureCallback = defaultSessionConfig.getCameraCaptureCallback();
+            implOptions = defaultSessionConfig.getImplementationOptions();
+
+            // Add all default camera characteristics
+            builder.addCharacteristics(defaultSessionConfig.getCameraCharacteristics());
+        }
+
+        // Set the any additional implementation options
+        builder.setImplementationOptions(implOptions);
+
+        // Get Camera2 extended options
+        Camera2Configuration camera2Config = new Camera2Configuration(config);
+
+        // Apply template type
+        builder.setTemplateType(camera2Config.getCaptureRequestTemplate(templateType));
+
+        // Combine default config callbacks with extension callbacks
+        deviceStateCallback =
+                CameraDeviceStateCallbacks.createComboCallback(
+                        deviceStateCallback,
+                        camera2Config.getDeviceStateCallback(
+                                CameraDeviceStateCallbacks.createNoOpCallback()));
+        sessionStateCallback =
+                CameraCaptureSessionStateCallbacks.createComboCallback(
+                        sessionStateCallback,
+                        camera2Config.getSessionStateCallback(
+                                CameraCaptureSessionStateCallbacks.createNoOpCallback()));
+        cameraCaptureCallback =
+                CameraCaptureCallbacks.createComboCallback(
+                        cameraCaptureCallback,
+                        CaptureCallbackContainer.create(
+                                camera2Config.getSessionCaptureCallback(
+                                        Camera2CaptureSessionCaptureCallbacks
+                                                .createNoOpCallback())));
+
+        // Apply state callbacks
+        builder.setDeviceStateCallback(deviceStateCallback);
+        builder.setSessionStateCallback(sessionStateCallback);
+        builder.setCameraCaptureCallback(cameraCaptureCallback);
+
+        // Copy extension keys
+        camera2Config.findOptions(
+                Camera2Configuration.CAPTURE_REQUEST_ID_STEM,
+                option -> {
+                    @SuppressWarnings(
+                            "unchecked") // No way to get actual type info here, so treat as Object
+                            Option<Object> typeErasedOption = (Option<Object>) option;
+                    @SuppressWarnings("unchecked")
+                    CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+
+                    builder.addCharacteristic(key, camera2Config.retrieveOption(typeErasedOption));
+                    return true;
+                });
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/Camera2RequestRunner.java b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2RequestRunner.java
new file mode 100644
index 0000000..d44d9af
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2RequestRunner.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.graphics.Rect;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CaptureRequestConfiguration;
+
+/**
+ * A interface for executing camera2 capture requests which is required for {@link
+ * Camera2CameraControl} to achieve its functionality.
+ *
+ * <p>{@link Camera} implements this interface so Camera2CameraControl can issue {@link
+ * CaptureRequest} for manipulating the camera. Camera2CameraControl can use it to execute single
+ * request and re-send the repeating request with updated Control SessionConfiguration. For example,
+ * {@link CameraControl#focus(Rect, Rect)} needs to send a single request to trigger AF as well as
+ * resend the repeating request with updated focus area.
+ *
+ * @hide
+ */
+public interface Camera2RequestRunner {
+
+    /**
+     * Executes a single capture request.
+     *
+     * <p>CameraControl methods like focus, trigger AF need to send single request.
+     */
+    void submitSingleRequest(CaptureRequestConfiguration singleRequestConfig);
+
+    /**
+     * Re-sends the repeating request which contains the latest settings specified by {@link
+     * CameraControl}.
+     *
+     * <p>CameraControl methods like setCropRegion, zoom, focus need to update repeating request.
+     */
+    void updateRepeatingRequest();
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.java b/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.java
new file mode 100644
index 0000000..8f60884
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureFailure;
+
+/**
+ * An adapter that passes {@link CameraCaptureSession.CaptureCallback} to {@link
+ * CameraCaptureCallback}.
+ */
+final class CameraCaptureCallbackAdapter extends CameraCaptureSession.CaptureCallback {
+
+    private final CameraCaptureCallback cameraCaptureCallback;
+
+    CameraCaptureCallbackAdapter(CameraCaptureCallback cameraCaptureCallback) {
+        if (cameraCaptureCallback == null) {
+            throw new NullPointerException("cameraCaptureCallback is null");
+        }
+        this.cameraCaptureCallback = cameraCaptureCallback;
+    }
+
+    @Override
+    public void onCaptureCompleted(
+            @NonNull CameraCaptureSession session,
+            @NonNull CaptureRequest request,
+            @NonNull TotalCaptureResult result) {
+        super.onCaptureCompleted(session, request, result);
+
+        cameraCaptureCallback.onCaptureCompleted(new Camera2CameraCaptureResult(result));
+    }
+
+    @Override
+    public void onCaptureFailed(
+            @NonNull CameraCaptureSession session,
+            @NonNull CaptureRequest request,
+            @NonNull CaptureFailure failure) {
+        super.onCaptureFailed(session, request, failure);
+
+        CameraCaptureFailure cameraFailure =
+                new CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR);
+
+        cameraCaptureCallback.onCaptureFailed(cameraFailure);
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackContainer.java b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackContainer.java
new file mode 100644
index 0000000..a046d03
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackContainer.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureCallback;
+
+/**
+ * A {@link CameraCaptureCallback} which contains an {@link CaptureCallback} and doesn't handle the
+ * callback.
+ */
+final class CaptureCallbackContainer extends CameraCaptureCallback {
+
+    private final CaptureCallback captureCallback;
+
+    private CaptureCallbackContainer(CaptureCallback captureCallback) {
+        if (captureCallback == null) {
+            throw new NullPointerException("captureCallback is null");
+        }
+        this.captureCallback = captureCallback;
+    }
+
+    static CaptureCallbackContainer create(CaptureCallback captureCallback) {
+        return new CaptureCallbackContainer(captureCallback);
+    }
+
+    @NonNull
+    CaptureCallback getCaptureCallback() {
+        return captureCallback;
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackConverter.java b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackConverter.java
new file mode 100644
index 0000000..fef48118
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureCallbackConverter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks.ComboCameraCaptureCallback;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** An utility class to convert {@link CameraCaptureCallback} to camera2 {@link CaptureCallback}. */
+final class CaptureCallbackConverter {
+
+    private CaptureCallbackConverter() {
+    }
+
+    /**
+     * Converts {@link CameraCaptureCallback} to {@link CaptureCallback}.
+     *
+     * @param cameraCaptureCallback The camera capture callback.
+     * @return The capture session callback.
+     */
+    static CaptureCallback toCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+        if (cameraCaptureCallback == null) {
+            return null;
+        }
+        List<CaptureCallback> list = new ArrayList<>();
+        toCaptureCallback(cameraCaptureCallback, list);
+        return list.size() == 1
+                ? list.get(0)
+                : Camera2CaptureSessionCaptureCallbacks.createComboCallback(list);
+    }
+
+    /**
+     * Converts {@link CameraCaptureCallback} to one or more {@link CaptureCallback} and put them
+     * into the input capture callback list.
+     *
+     * <p>There are several known types of {@link CameraCaptureCallback}s. Convert the callback
+     * according to the corresponding rule.
+     *
+     * @param cameraCaptureCallback The camera capture callback.
+     * @param captureCallbackList   The output capture session callback list.
+     */
+    static void toCaptureCallback(
+            CameraCaptureCallback cameraCaptureCallback,
+            List<CaptureCallback> captureCallbackList) {
+        if (cameraCaptureCallback instanceof ComboCameraCaptureCallback) {
+            // Recursively convert callback inside the combo callback.
+            ComboCameraCaptureCallback comboCallback =
+                    (ComboCameraCaptureCallback) cameraCaptureCallback;
+            for (CameraCaptureCallback callback : comboCallback.getCallbacks()) {
+                toCaptureCallback(callback, captureCallbackList);
+            }
+        } else if (cameraCaptureCallback instanceof CaptureCallbackContainer) {
+            // Get the actual callback inside the CaptureCallbackContainer.
+            CaptureCallbackContainer callbackContainer =
+                    (CaptureCallbackContainer) cameraCaptureCallback;
+            captureCallbackList.add(callbackContainer.getCaptureCallback());
+        } else {
+            // Create a CameraCaptureCallbackAdapter.
+            captureCallbackList.add(new CameraCaptureCallbackAdapter(cameraCaptureCallback));
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/CaptureSession.java b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureSession.java
new file mode 100644
index 0000000..f313319
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureSession.java
@@ -0,0 +1,517 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraCaptureSessionStateCallbacks;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.Configuration.Option;
+import androidx.camera.core.DeferrableSurfaces;
+import androidx.camera.core.SessionConfiguration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A session for capturing images from the camera which is tied to a specific {@link CameraDevice}.
+ *
+ * <p>A session can only be opened a single time. Once has {@link CaptureSession#close()} been
+ * called then it is permanently closed so a new session has to be created for capturing images.
+ */
+final class CaptureSession {
+    private static final String TAG = "CaptureSession";
+
+    /** Handler for all the callbacks from the {@link CameraCaptureSession}. */
+    @Nullable
+    private final Handler handler;
+    /** The configuration for the currently issued single capture requests. */
+    private final List<CaptureRequestConfiguration> captureRequestConfigurations =
+            new ArrayList<>();
+    /** Lock on whether the camera is open or closed. */
+    final Object stateLock = new Object();
+    /** Callback for handling image captures. */
+    private final CameraCaptureSession.CaptureCallback captureCallback =
+            new CaptureCallback() {
+                @Override
+                public void onCaptureCompleted(
+                        CameraCaptureSession session,
+                        CaptureRequest request,
+                        TotalCaptureResult result) {
+                }
+            };
+    private final StateCallback captureSessionStateCallback = new StateCallback();
+    /** The framework camera capture session held by this session. */
+    @Nullable
+    CameraCaptureSession cameraCaptureSession;
+    /** The configuration for the currently issued capture requests. */
+    private volatile SessionConfiguration sessionConfiguration =
+            SessionConfiguration.defaultEmptySessionConfiguration();
+    /** The list of surfaces used to configure the current capture session. */
+    private List<Surface> configuredSurfaces = Collections.emptyList();
+    /** Tracks the current state of the session. */
+    @GuardedBy("stateLock")
+    State state = State.UNINITIALIZED;
+
+    /**
+     * Constructor for CaptureSession.
+     *
+     * @param handler The handler is responsible for queuing up callbacks from capture requests. If
+     *                this is null then when asynchronous methods are called on this session they
+     *                will attempt
+     *                to use the current thread's looper.
+     */
+    CaptureSession(@Nullable Handler handler) {
+        this.handler = handler;
+        state = State.INITIALIZED;
+    }
+
+    /** Returns the configurations of the capture session. */
+    SessionConfiguration getSessionConfiguration() {
+        synchronized (stateLock) {
+            return sessionConfiguration;
+        }
+    }
+
+    /**
+     * Sets the active configurations for the capture session.
+     *
+     * <p>Once both the session configuration has been set and the session has been opened, then the
+     * capture requests will immediately be issued.
+     *
+     * @param sessionConfiguration has the configuration that will currently active in issuing
+     *                             capture request. The surfaces contained in this must be a
+     *                             subset of the surfaces that
+     *                             were used to open this capture session.
+     */
+    void setSessionConfiguration(SessionConfiguration sessionConfiguration) {
+        synchronized (stateLock) {
+            switch (state) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "setSessionConfiguration() should not be possible in state: " + state);
+                case INITIALIZED:
+                case OPENING:
+                    this.sessionConfiguration = sessionConfiguration;
+                    break;
+                case OPENED:
+                    this.sessionConfiguration = sessionConfiguration;
+
+                    if (!configuredSurfaces.containsAll(
+                            DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))) {
+                        Log.e(TAG, "Does not have the proper configured lists");
+                        return;
+                    }
+
+                    Log.d(TAG, "Attempting to submit CaptureRequest after setting");
+                    issueRepeatingCaptureRequests();
+                    break;
+                case CLOSED:
+                case RELEASING:
+                case RELEASED:
+                    throw new IllegalStateException(
+                            "Session configuration cannot be set on a closed/released session.");
+            }
+        }
+    }
+
+    /**
+     * Opens the capture session synchronously.
+     *
+     * <p>When the session is opened and the configurations have been set then the capture requests
+     * will be issued.
+     *
+     * @param sessionConfiguration which is used to configure the camera capture session. This
+     *                             contains configurations which may or may not be currently
+     *                             active in issuing capture
+     *                             requests.
+     * @param cameraDevice         the camera with which to generate the capture session
+     * @throws CameraAccessException if the camera is in an invalid start state
+     */
+    void open(SessionConfiguration sessionConfiguration, CameraDevice cameraDevice)
+            throws CameraAccessException {
+        synchronized (stateLock) {
+            switch (state) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "open() should not be possible in state: " + state);
+                case INITIALIZED:
+                    configuredSurfaces =
+                            new ArrayList<>(
+                                    DeferrableSurfaces.surfaceSet(
+                                            sessionConfiguration.getSurfaces()));
+                    if (configuredSurfaces.isEmpty()) {
+                        Log.e(TAG, "Unable to open capture session with no surfaces. ");
+                        return;
+                    }
+
+                    state = State.OPENING;
+                    Log.d(TAG, "Opening capture session.");
+                    CameraCaptureSession.StateCallback comboCallback =
+                            CameraCaptureSessionStateCallbacks.createComboCallback(
+                                    captureSessionStateCallback,
+                                    sessionConfiguration.getSessionStateCallback());
+                    cameraDevice.createCaptureSession(configuredSurfaces, comboCallback, handler);
+                    break;
+                default:
+                    Log.e(TAG, "Open not allowed in state: " + state);
+            }
+        }
+    }
+
+    /**
+     * Closes the capture session.
+     *
+     * <p>Close needs be called on a session in order to safely open another session. However, this
+     * stops minimal resources so that another session can be quickly opened.
+     *
+     * <p>Once a session is closed it can no longer be opened again. After the session is closed all
+     * method calls on it do nothing.
+     */
+    void close() {
+        synchronized (stateLock) {
+            switch (state) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "close() should not be possible in state: " + state);
+                case INITIALIZED:
+                    state = State.RELEASED;
+                    break;
+                case OPENING:
+                case OPENED:
+                    state = State.CLOSED;
+                    break;
+                case CLOSED:
+                case RELEASING:
+                case RELEASED:
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Releases the capture session.
+     *
+     * <p>This releases all of the sessions resources and should be called when ready to close the
+     * camera.
+     *
+     * <p>Once a session is released it can no longer be opened again. After the session is released
+     * all method calls on it do nothing.
+     */
+    void release() {
+        synchronized (stateLock) {
+            switch (state) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "release() should not be possible in state: " + state);
+                case INITIALIZED:
+                    state = State.RELEASED;
+                    break;
+                case OPENING:
+                    state = State.RELEASING;
+                    break;
+                case OPENED:
+                case CLOSED:
+                    cameraCaptureSession.close();
+                    state = State.RELEASING;
+                    break;
+                case RELEASING:
+                case RELEASED:
+            }
+        }
+    }
+
+    /**
+     * Issues a single capture request.
+     *
+     * @param captureRequestConfiguration which is used to construct a {@link CaptureRequest}.
+     */
+    void issueSingleCaptureRequest(CaptureRequestConfiguration captureRequestConfiguration) {
+        issueSingleCaptureRequests(Collections.singletonList(captureRequestConfiguration));
+    }
+
+    /**
+     * Issues single capture requests.
+     *
+     * @param captureRequestConfigurations which is used to construct {@link CaptureRequest}.
+     */
+    void issueSingleCaptureRequests(
+            List<CaptureRequestConfiguration> captureRequestConfigurations) {
+        synchronized (stateLock) {
+            switch (state) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "issueSingleCaptureRequests() should not be possible in state: "
+                                    + state);
+                case INITIALIZED:
+                case OPENING:
+                    Log.d(TAG, "issueSingleCaptureRequests() before capture session opened.");
+                    this.captureRequestConfigurations.addAll(captureRequestConfigurations);
+                    break;
+                case OPENED:
+                    this.captureRequestConfigurations.addAll(captureRequestConfigurations);
+                    issueCaptureRequests();
+                    break;
+                case CLOSED:
+                case RELEASING:
+                case RELEASED:
+                    throw new IllegalStateException(
+                            "Cannot issue capture request on a closed/released session.");
+            }
+        }
+    }
+
+    /** Returns the configurations of the capture requests. */
+    List<CaptureRequestConfiguration> getCaptureRequestConfigurations() {
+        synchronized (stateLock) {
+            return Collections.unmodifiableList(captureRequestConfigurations);
+        }
+    }
+
+    /** Returns the current state of the session. */
+    State getState() {
+        synchronized (stateLock) {
+            return state;
+        }
+    }
+
+    /**
+     * Sets the {@link CaptureRequest} so that the camera will start producing data.
+     *
+     * <p>Will skip setting requests if there are no surfaces since it is illegal to do so.
+     */
+    void issueRepeatingCaptureRequests() {
+        CaptureRequestConfiguration captureRequestConfiguration =
+                sessionConfiguration.getCaptureRequestConfiguration();
+
+        try {
+            Log.d(TAG, "Issuing request for session.");
+            CaptureRequest.Builder builder =
+                    captureRequestConfiguration.buildCaptureRequest(
+                            cameraCaptureSession.getDevice());
+            if (builder == null) {
+                Log.d(TAG, "Skipping issuing empty request for session.");
+                return;
+            }
+
+            applyImplementationOptionTCaptureBuilder(
+                    builder, captureRequestConfiguration.getImplementationOptions());
+
+            CameraCaptureSession.CaptureCallback comboCaptureCallback =
+                    Camera2CaptureSessionCaptureCallbacks.createComboCallback(
+                            captureCallback,
+                            CaptureCallbackConverter.toCaptureCallback(
+                                    captureRequestConfiguration.getCameraCaptureCallback()));
+            cameraCaptureSession.setRepeatingRequest(
+                    builder.build(), comboCaptureCallback, handler);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Unable to access camera: " + e.getMessage());
+            Thread.dumpStack();
+        }
+    }
+
+    private void applyImplementationOptionTCaptureBuilder(
+            CaptureRequest.Builder builder, Configuration configuration) {
+        Camera2Configuration camera2Config = new Camera2Configuration(configuration);
+        for (Option<?> option : camera2Config.getCaptureRequestOptions()) {
+            /* Although type is erased below, it is safe to pass it to CaptureRequest.Builder
+            because
+            these option are created via Camera2Configuration.Extender.setCaptureRequestOption
+            (CaptureRequest.Key<ValueT> key, ValueT value) and hence the type compatibility of
+            key and
+            value are ensured by the compiler. */
+            @SuppressWarnings("unchecked")
+            Option<Object> typeErasedOption = (Option<Object>) option;
+            @SuppressWarnings("unchecked")
+            CaptureRequest.Key<Object> key = (CaptureRequest.Key<Object>) option.getToken();
+            builder.set(key, camera2Config.retrieveOption(typeErasedOption));
+        }
+    }
+
+    /** Issues captureRequestConfigurations to {@link CameraCaptureSession}. */
+    void issueCaptureRequests() {
+        if (captureRequestConfigurations.isEmpty()) {
+            return;
+        }
+
+        for (CaptureRequestConfiguration captureRequestConfiguration :
+                captureRequestConfigurations) {
+            if (captureRequestConfiguration.getSurfaces().isEmpty()) {
+                Log.d(TAG, "Skipping issuing empty capture request.");
+                continue;
+            }
+            try {
+                Log.d(TAG, "Issuing capture request.");
+                CaptureRequest.Builder builder =
+                        captureRequestConfiguration.buildCaptureRequest(
+                                cameraCaptureSession.getDevice());
+
+                applyImplementationOptionTCaptureBuilder(
+                        builder, captureRequestConfiguration.getImplementationOptions());
+
+                cameraCaptureSession.capture(
+                        builder.build(),
+                        CaptureCallbackConverter.toCaptureCallback(
+                                captureRequestConfiguration.getCameraCaptureCallback()),
+                        handler);
+            } catch (CameraAccessException e) {
+                Log.e(TAG, "Unable to access camera: " + e.getMessage());
+                Thread.dumpStack();
+            }
+        }
+        captureRequestConfigurations.clear();
+    }
+
+    enum State {
+        /** The default state of the session before construction. */
+        UNINITIALIZED,
+        /**
+         * Stable state once the session has been constructed, but prior to the {@link
+         * CameraCaptureSession} being opened.
+         */
+        INITIALIZED,
+        /**
+         * Transitional state when the {@link CameraCaptureSession} is in the process of being
+         * opened.
+         */
+        OPENING,
+        /**
+         * Stable state where the {@link CameraCaptureSession} has been successfully opened. During
+         * this state if a valid {@link SessionConfiguration} has been set then the {@link
+         * CaptureRequest} will be issued.
+         */
+        OPENED,
+        /**
+         * Stable state where the session has been closed. However the {@link CameraCaptureSession}
+         * is still valid. It will remain valid until a new instance is opened at which point {@link
+         * CameraCaptureSession.StateCallback#onClosed(CameraCaptureSession)} will be called to do
+         * final cleanup.
+         */
+        CLOSED,
+        /** Transitional state where the resources are being cleaned up. */
+        RELEASING,
+        /**
+         * Terminal state where the session has been cleaned up. At this point the session should
+         * not be used as nothing will happen in this state.
+         */
+        RELEASED
+    }
+
+    /**
+     * Callback for handling state changes to the {@link CameraCaptureSession}.
+     *
+     * <p>State changes are ignored once the CaptureSession has been closed.
+     */
+    final class StateCallback extends CameraCaptureSession.StateCallback {
+        /**
+         * {@inheritDoc}
+         *
+         * <p>Once the {@link CameraCaptureSession} has been configured then the capture request
+         * will be immediately issued.
+         */
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            synchronized (stateLock) {
+                switch (state) {
+                    case UNINITIALIZED:
+                    case INITIALIZED:
+                    case OPENED:
+                    case RELEASED:
+                        throw new IllegalStateException(
+                                "onConfigured() should not be possible in state: " + state);
+                    case OPENING:
+                        state = State.OPENED;
+                        cameraCaptureSession = session;
+                        Log.d(TAG, "Attempting to send capture request onConfigured");
+                        issueRepeatingCaptureRequests();
+                        issueCaptureRequests();
+                        break;
+                    case CLOSED:
+                        cameraCaptureSession = session;
+                        break;
+                    case RELEASING:
+                        session.close();
+                        break;
+                }
+                Log.d(TAG, "CameraCaptureSession.onConfigured()");
+            }
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            synchronized (stateLock) {
+                switch (state) {
+                    case UNINITIALIZED:
+                        throw new IllegalStateException(
+                                "onReady() should not be possible in state: " + state);
+                    default:
+                }
+                Log.d(TAG, "CameraCaptureSession.onReady()");
+            }
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            synchronized (stateLock) {
+                switch (state) {
+                    case UNINITIALIZED:
+                        throw new IllegalStateException(
+                                "onClosed() should not be possible in state: " + state);
+                    default:
+                        state = State.RELEASED;
+                        cameraCaptureSession = null;
+                }
+                Log.d(TAG, "CameraCaptureSession.onClosed()");
+            }
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            synchronized (stateLock) {
+                switch (state) {
+                    case UNINITIALIZED:
+                    case INITIALIZED:
+                    case OPENED:
+                    case RELEASED:
+                        throw new IllegalStateException(
+                                "onConfiguredFailed() should not be possible in state: " + state);
+                    case OPENING:
+                    case CLOSED:
+                        state = State.CLOSED;
+                        cameraCaptureSession = session;
+                        break;
+                    case RELEASING:
+                        state = State.RELEASING;
+                        session.close();
+                }
+                Log.e(TAG, "CameraCaptureSession.onConfiguredFailed()");
+            }
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageAnalysisConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageAnalysisConfigurationProvider.java
new file mode 100644
index 0000000..6ca7660
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageAnalysisConfigurationProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.SessionConfiguration;
+
+/**
+ * Provides defaults for {@link ImageAnalysisUseCaseConfiguration} in the Camera2 implementation.
+ */
+final class DefaultImageAnalysisConfigurationProvider
+        implements ConfigurationProvider<ImageAnalysisUseCaseConfiguration> {
+    private static final String TAG = "DefaultImageAnalysisConfigurationProvider";
+
+    private final CameraFactory cameraFactory;
+
+    public DefaultImageAnalysisConfigurationProvider(CameraFactory cameraFactory) {
+        this.cameraFactory = cameraFactory;
+    }
+
+    @Override
+    public ImageAnalysisUseCaseConfiguration getConfiguration() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                ImageAnalysisUseCaseConfiguration.Builder.fromConfig(
+                        ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration());
+
+        // SessionConfiguration containing all intrinsic properties needed for ImageAnalysisUseCase
+        SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+        // TODO(b/114762170): Must set to preview here until we allow for multiple template types
+        sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+        // Add options to UseCaseConfiguration
+        builder.setDefaultSessionConfiguration(sessionBuilder.build());
+        builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+        // Add default lensFacing if we can
+        try {
+            String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+            if (defaultId != null) {
+                builder.setLensFacing(LensFacing.BACK);
+            } else {
+                defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+                if (defaultId != null) {
+                    builder.setLensFacing(LensFacing.FRONT);
+                }
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Unable to determine default lens facing for ImageAnalysisUseCase.", e);
+        }
+
+        return builder.build();
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageCaptureConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageCaptureConfigurationProvider.java
new file mode 100644
index 0000000..3a58272
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageCaptureConfigurationProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.SessionConfiguration;
+
+/** Provides defaults for {@link ImageCaptureUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultImageCaptureConfigurationProvider
+        implements ConfigurationProvider<ImageCaptureUseCaseConfiguration> {
+    private static final String TAG = "DefaultImageCaptureConfigurationProvider";
+
+    private final CameraFactory cameraFactory;
+
+    public DefaultImageCaptureConfigurationProvider(CameraFactory cameraFactory) {
+        this.cameraFactory = cameraFactory;
+    }
+
+    @Override
+    public ImageCaptureUseCaseConfiguration getConfiguration() {
+        ImageCaptureUseCaseConfiguration.Builder builder =
+                ImageCaptureUseCaseConfiguration.Builder.fromConfig(
+                        ImageCaptureUseCase.DEFAULT_CONFIG.getConfiguration());
+
+        // SessionConfiguration containing all intrinsic properties needed for ImageCaptureUseCase
+        SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+        // TODO(b/114762170): Must set to preview here until we allow for multiple template types
+        sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+        // Add options to UseCaseConfiguration
+        builder.setDefaultSessionConfiguration(sessionBuilder.build());
+        builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+        // Add default lensFacing if we can
+        try {
+            String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+            if (defaultId != null) {
+                builder.setLensFacing(LensFacing.BACK);
+            } else {
+                defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+                if (defaultId != null) {
+                    builder.setLensFacing(LensFacing.FRONT);
+                }
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Unable to determine default lens facing for ImageCaptureUseCase.", e);
+        }
+
+        return builder.build();
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultVideoCaptureConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultVideoCaptureConfigurationProvider.java
new file mode 100644
index 0000000..79bd127
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultVideoCaptureConfigurationProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+
+/** Provides defaults for {@link VideoCaptureUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultVideoCaptureConfigurationProvider
+        implements ConfigurationProvider<VideoCaptureUseCaseConfiguration> {
+    private static final String TAG = "DefaultVideoCaptureConfigurationProvider";
+
+    private final CameraFactory cameraFactory;
+
+    public DefaultVideoCaptureConfigurationProvider(CameraFactory cameraFactory) {
+        this.cameraFactory = cameraFactory;
+    }
+
+    @Override
+    public VideoCaptureUseCaseConfiguration getConfiguration() {
+        VideoCaptureUseCaseConfiguration.Builder builder =
+                VideoCaptureUseCaseConfiguration.Builder.fromConfig(
+                        VideoCaptureUseCase.DEFAULT_CONFIG.getConfiguration());
+
+        // SessionConfiguration containing all intrinsic properties needed for VideoCaptureUseCase
+        SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+        // TODO(b/114762170): Must set to preview here until we allow for multiple template types
+        sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+        // Add options to UseCaseConfiguration
+        builder.setDefaultSessionConfiguration(sessionBuilder.build());
+        builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+        // Add default lensFacing if we can
+        try {
+            String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+            if (defaultId != null) {
+                builder.setLensFacing(LensFacing.BACK);
+            } else {
+                defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+                if (defaultId != null) {
+                    builder.setLensFacing(LensFacing.FRONT);
+                }
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Unable to determine default lens facing for VideoCaptureUseCase.", e);
+        }
+
+        return builder.build();
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/DefaultViewFinderConfigurationProvider.java b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultViewFinderConfigurationProvider.java
new file mode 100644
index 0000000..bfb590b
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultViewFinderConfigurationProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ConfigurationProvider;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/** Provides defaults for {@link ViewFinderUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultViewFinderConfigurationProvider
+        implements ConfigurationProvider<ViewFinderUseCaseConfiguration> {
+    private static final String TAG = "DefaultViewFinderConfigurationProvider";
+
+    private final CameraFactory cameraFactory;
+
+    public DefaultViewFinderConfigurationProvider(CameraFactory cameraFactory) {
+        this.cameraFactory = cameraFactory;
+    }
+
+    @Override
+    public ViewFinderUseCaseConfiguration getConfiguration() {
+        ViewFinderUseCaseConfiguration.Builder builder =
+                ViewFinderUseCaseConfiguration.Builder.fromConfig(
+                        ViewFinderUseCase.DEFAULT_CONFIG.getConfiguration());
+
+        // SessionConfiguration containing all intrinsic properties needed for ViewFinderUseCase
+        SessionConfiguration.Builder sessionBuilder = new SessionConfiguration.Builder();
+        sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+
+        // Add options to UseCaseConfiguration
+        builder.setDefaultSessionConfiguration(sessionBuilder.build());
+        builder.setOptionUnpacker(Camera2OptionUnpacker.INSTANCE);
+
+        // Add default lensFacing if we can
+        try {
+            String defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+            if (defaultId != null) {
+                builder.setLensFacing(LensFacing.BACK);
+            } else {
+                defaultId = cameraFactory.cameraIdForLensFacing(LensFacing.FRONT);
+                if (defaultId != null) {
+                    builder.setLensFacing(LensFacing.FRONT);
+                }
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Unable to determine default lens facing for ViewFinderUseCase.", e);
+        }
+
+        return builder.build();
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/SupportedSurfaceCombination.java b/camera/camera2/src/main/java/androidx/camera/camera2/SupportedSurfaceCombination.java
new file mode 100644
index 0000000..eee8d21
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/SupportedSurfaceCombination.java
@@ -0,0 +1,988 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.CamcorderProfile;
+import android.os.Build;
+import android.util.Rational;
+import android.util.Size;
+import android.view.WindowManager;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ImageFormatConstants;
+import androidx.camera.core.ImageOutputConfiguration;
+import androidx.camera.core.SurfaceCombination;
+import androidx.camera.core.SurfaceConfiguration;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationSize;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationType;
+import androidx.camera.core.SurfaceSizeDefinition;
+import androidx.camera.core.UseCaseConfiguration;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Camera device supported surface configuration combinations
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices. This structure is used to store a list of surface combinations that are guaranteed to
+ * support for this camera device.
+ */
+final class SupportedSurfaceCombination {
+    private static final Size MAX_PREVIEW_SIZE = new Size(1920, 1080);
+    private static final Size DEFAULT_SIZE = new Size(640, 480);
+    private static final Size ZERO_SIZE = new Size(0, 0);
+    private static final Size QUALITY_2160P_SIZE = new Size(3840, 2160);
+    private static final Size QUALITY_1080P_SIZE = new Size(1920, 1080);
+    private static final Size QUALITY_720P_SIZE = new Size(1280, 720);
+    private static final Size QUALITY_480P_SIZE = new Size(720, 480);
+    private final List<SurfaceCombination> surfaceCombinationList = new ArrayList<>();
+    private String cameraId;
+    private CameraCharacteristics characteristics;
+    private int hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+    private boolean isRawSupported = false;
+    private boolean isBurstCaptureSupported = false;
+    private Size displayViewSize;
+    private SurfaceSizeDefinition surfaceSizeDefinition;
+    private CamcorderProfileHelper camcorderProfileHelper;
+
+    SupportedSurfaceCombination(
+            Context context, String cameraId, CamcorderProfileHelper camcorderProfileHelper) {
+        this.cameraId = cameraId;
+        this.camcorderProfileHelper = camcorderProfileHelper;
+        init(context);
+    }
+
+    private SupportedSurfaceCombination() {
+    }
+
+    String getCameraId() {
+        return cameraId;
+    }
+
+    boolean isRawSupported() {
+        return isRawSupported;
+    }
+
+    boolean isBurstCaptureSupported() {
+        return isBurstCaptureSupported;
+    }
+
+    /**
+     * Check whether the input surface configuration list is under the capability of any combination
+     * of this object.
+     *
+     * @param surfaceConfigurationList the surface configuration list to be compared
+     * @return the check result that whether it could be supported
+     */
+    boolean checkSupported(List<SurfaceConfiguration> surfaceConfigurationList) {
+        boolean isSupported = false;
+
+        for (SurfaceCombination surfaceCombination : surfaceCombinationList) {
+            isSupported = surfaceCombination.isSupported(surfaceConfigurationList);
+
+            if (isSupported) {
+                break;
+            }
+        }
+
+        return isSupported;
+    }
+
+    /**
+     * Transform to a SurfaceConfiguration object with image format and size info
+     *
+     * @param imageFormat the image format info for the surface configuration object
+     * @param size        the size info for the surface configuration object
+     * @return new {@link SurfaceConfiguration} object
+     */
+    SurfaceConfiguration transformSurfaceConfiguration(int imageFormat, Size size) {
+        ConfigurationType configurationType;
+        ConfigurationSize configurationSize = ConfigurationSize.NOT_SUPPORT;
+
+        if (getAllOutputSizesByFormat(imageFormat) == null) {
+            throw new IllegalArgumentException(
+                    "Can not get supported output size for the format: " + imageFormat);
+        }
+
+        /**
+         * PRIV refers to any target whose available sizes are found using
+         * StreamConfigurationMap.getOutputSizes(Class) with no direct application-visible format,
+         * YUV refers to a target Surface using the ImageFormat.YUV_420_888 format, JPEG refers to
+         * the ImageFormat.JPEG format, and RAW refers to the ImageFormat.RAW_SENSOR format.
+         */
+        if (imageFormat == ImageFormat.YUV_420_888) {
+            configurationType = ConfigurationType.YUV;
+        } else if (imageFormat == ImageFormat.JPEG) {
+            configurationType = ConfigurationType.JPEG;
+        } else if (imageFormat == ImageFormat.RAW_SENSOR) {
+            configurationType = ConfigurationType.RAW;
+        } else {
+            configurationType = ConfigurationType.PRIV;
+        }
+
+        Size maxSize = surfaceSizeDefinition.getMaximumSizeMap().get(imageFormat);
+
+        // Compare with surface size definition to determine the surface configuration size
+        if (size.getWidth() * size.getHeight()
+                <= surfaceSizeDefinition.getAnalysisSize().getWidth()
+                * surfaceSizeDefinition.getAnalysisSize().getHeight()) {
+            configurationSize = ConfigurationSize.ANALYSIS;
+        } else if (size.getWidth() * size.getHeight()
+                <= surfaceSizeDefinition.getPreviewSize().getWidth()
+                * surfaceSizeDefinition.getPreviewSize().getHeight()) {
+            configurationSize = ConfigurationSize.PREVIEW;
+        } else if (size.getWidth() * size.getHeight()
+                <= surfaceSizeDefinition.getRecordSize().getWidth()
+                * surfaceSizeDefinition.getRecordSize().getHeight()) {
+            configurationSize = ConfigurationSize.RECORD;
+        } else if (size.getWidth() * size.getHeight() <= maxSize.getWidth() * maxSize.getHeight()) {
+            configurationSize = ConfigurationSize.MAXIMUM;
+        }
+
+        return SurfaceConfiguration.create(configurationType, configurationSize);
+    }
+
+    Map<BaseUseCase, Size> getSuggestedResolutions(
+            List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+        Map<BaseUseCase, Size> suggestedResolutionsMap = new HashMap<>();
+
+        // Get the index order list by the use case priority for finding stream configuration
+        List<Integer> useCasesPriorityOrder = getUseCasesPriorityOrder(newUseCases);
+        List<List<Size>> supportedOutputSizesList = new ArrayList<>();
+
+        // Collect supported output sizes for all use cases
+        for (Integer index : useCasesPriorityOrder) {
+            List<Size> supportedOutputSizes = getSupportedOutputSizes(newUseCases.get(index));
+            supportedOutputSizesList.add(supportedOutputSizes);
+        }
+
+        // Get all possible size arrangements
+        List<List<Size>> allPossibleSizeArrangements =
+                getAllPossibleSizeArrangements(supportedOutputSizesList);
+
+        // Transform use cases to SurfaceConfiguration list and find the first (best) workable
+        // combination
+        for (List<Size> possibleSizeList : allPossibleSizeArrangements) {
+            List<SurfaceConfiguration> surfaceConfigurationList = new ArrayList<>();
+
+            // Attach SurfaceConfiguration of original use cases since it will impact the new use
+            // cases
+            if (originalUseCases != null) {
+                for (BaseUseCase useCase : originalUseCases) {
+                    CameraDeviceConfiguration configuration =
+                            (CameraDeviceConfiguration) useCase.getUseCaseConfiguration();
+                    String useCaseCameraId;
+                    try {
+                        useCaseCameraId =
+                                CameraX.getCameraWithLensFacing(configuration.getLensFacing());
+                    } catch (Exception e) {
+                        throw new IllegalArgumentException(
+                                "Unable to get camera ID for use case " + useCase.getName(), e);
+                    }
+                    Size resolution = useCase.getAttachedSurfaceResolution(useCaseCameraId);
+
+                    surfaceConfigurationList.add(
+                            transformSurfaceConfiguration(useCase.getImageFormat(), resolution));
+                }
+            }
+
+            // Attach SurfaceConfiguration of new use cases
+            for (Size size : possibleSizeList) {
+                BaseUseCase newUseCase =
+                        newUseCases.get(useCasesPriorityOrder.get(possibleSizeList.indexOf(size)));
+                surfaceConfigurationList.add(
+                        transformSurfaceConfiguration(newUseCase.getImageFormat(), size));
+            }
+
+            // Check whether the SurfaceConfiguration combination can be supported
+            if (checkSupported(surfaceConfigurationList)) {
+                for (BaseUseCase useCase : newUseCases) {
+                    suggestedResolutionsMap.put(
+                            useCase,
+                            possibleSizeList.get(
+                                    useCasesPriorityOrder.indexOf(newUseCases.indexOf(useCase))));
+                }
+                break;
+            }
+        }
+
+        return suggestedResolutionsMap;
+    }
+
+    SurfaceSizeDefinition getSurfaceSizeDefinition() {
+        return surfaceSizeDefinition;
+    }
+
+    private List<Integer> getUseCasesPriorityOrder(List<BaseUseCase> newUseCases) {
+        List<Integer> priorityOrder = new ArrayList<>();
+
+        /**
+         * Once the stream resource is occupied by one use case, it will impact the other use cases.
+         * Therefore, we need to define the priority for stream resource usage. For the use cases
+         * with the higher priority, we will try to find the best one for them in priority as
+         * possible.
+         */
+        List<Integer> priorityValueList = new ArrayList<>();
+
+        for (BaseUseCase useCase : newUseCases) {
+            UseCaseConfiguration<?> configuration = useCase.getUseCaseConfiguration();
+            int priority = configuration.getSurfaceOccupancyPriority(0);
+            if (!priorityValueList.contains(priority)) {
+                priorityValueList.add(priority);
+            }
+        }
+
+        Collections.sort(priorityValueList);
+        // Reverse the priority value list in descending order since larger value means higher
+        // priority
+        Collections.reverse(priorityValueList);
+
+        for (int priorityValue : priorityValueList) {
+            for (BaseUseCase useCase : newUseCases) {
+                UseCaseConfiguration<?> configuration = useCase.getUseCaseConfiguration();
+                if (priorityValue == configuration.getSurfaceOccupancyPriority(0)) {
+                    priorityOrder.add(newUseCases.indexOf(useCase));
+                }
+            }
+        }
+
+        return priorityOrder;
+    }
+
+    private List<Size> getSupportedOutputSizes(BaseUseCase useCase) {
+        int imageFormat = useCase.getImageFormat();
+        Size[] outputSizes = getAllOutputSizesByFormat(imageFormat);
+        List<Size> outputSizeCandidates = new ArrayList<>();
+        ImageOutputConfiguration configuration =
+                (ImageOutputConfiguration) useCase.getUseCaseConfiguration();
+        Size maxSize = configuration.getMaxResolution(getMaxOutputSizeByFormat(imageFormat));
+
+        // Sort the output sizes. The Comparator result must be reversed to have a descending order
+        // result.
+        Collections.sort(Arrays.asList(outputSizes), new CompareSizesByArea(true));
+
+        // Filter out the ones that exceed the maximum size
+        for (Size outputSize : outputSizes) {
+            if (outputSize.getWidth() * outputSize.getHeight()
+                    <= maxSize.getWidth() * maxSize.getHeight()) {
+                outputSizeCandidates.add(outputSize);
+            }
+        }
+
+        if (outputSizeCandidates.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Can not get supported output size under supported maximum for the format: "
+                            + imageFormat);
+        }
+
+        // Check whether the desired default resolution is included in the original supported list
+        boolean isDefaultResolutionSupported = outputSizeCandidates.contains(DEFAULT_SIZE);
+
+        // If the target resolution is set, use it to find the minimum one from big enough items
+        Size targetSize = configuration.getTargetResolution(ZERO_SIZE);
+
+        if (!targetSize.equals(ZERO_SIZE)) {
+            int indexBigEnough = 0;
+
+            // Get the index of the item that is big enough for the view size
+            for (Size outputSize : outputSizeCandidates) {
+                if (outputSize.getWidth() * outputSize.getHeight()
+                        >= targetSize.getWidth() * targetSize.getHeight()) {
+                    indexBigEnough = outputSizeCandidates.indexOf(outputSize);
+                } else {
+                    break;
+                }
+            }
+
+            // Remove the additional items that is larger than the big enough item
+            outputSizeCandidates.subList(0, indexBigEnough).clear();
+        }
+
+        if (outputSizeCandidates.isEmpty() && !isDefaultResolutionSupported) {
+            throw new IllegalArgumentException(
+                    "Can not get supported output size for the desired output size quality for "
+                            + "the format: "
+                            + imageFormat);
+        }
+
+        // Rearrange the supported size to put the ones with the same aspect ratio in the front of
+        // the
+        // list and put others in the end from large to small. Some low end devices may not able to
+        // get
+        // an supported resolution that match the preferred aspect ratio.
+        List<Size> sizesMatchAspectRatio = new ArrayList<>();
+        List<Size> sizesNotMatchAspectRatio = new ArrayList<>();
+        Rational aspectRatio = configuration.getTargetAspectRatio();
+
+        for (Size outputSize : outputSizeCandidates) {
+            if (aspectRatio.equals(new Rational(outputSize.getWidth(), outputSize.getHeight()))
+                    || aspectRatio.equals(
+                    new Rational(outputSize.getHeight(), outputSize.getWidth()))) {
+                sizesMatchAspectRatio.add(outputSize);
+            } else {
+                sizesNotMatchAspectRatio.add(outputSize);
+            }
+        }
+
+        List<Size> supportedResolutions = new ArrayList<>();
+        // No need to sort again since the source list has been sorted previously
+        supportedResolutions.addAll(sizesMatchAspectRatio);
+        supportedResolutions.addAll(sizesNotMatchAspectRatio);
+
+        // If there is no available size for the conditions and default resolution is in the
+        // supported
+        // list, return the default resolution.
+        if (supportedResolutions.isEmpty() && !isDefaultResolutionSupported) {
+            supportedResolutions.add(DEFAULT_SIZE);
+        }
+
+        return supportedResolutions;
+    }
+
+    private List<List<Size>> getAllPossibleSizeArrangements(
+            List<List<Size>> supportedOutputSizesList) {
+        int totalArrangementsCount = 1;
+
+        for (List<Size> supportedOutputSizes : supportedOutputSizesList) {
+            totalArrangementsCount *= supportedOutputSizes.size();
+        }
+
+        // If totalArrangementsCount is 0 means that there may some problem to get
+        // supportedOutputSizes
+        // for some use case
+        if (totalArrangementsCount == 0) {
+            throw new IllegalArgumentException("Failed to find supported resolutions.");
+        }
+
+        List<List<Size>> allPossibleSizeArrangements = new ArrayList<>();
+
+        // Initialize allPossibleSizeArrangements for the following operations
+        for (int i = 0; i < totalArrangementsCount; i++) {
+            List<Size> sizeList = new ArrayList<>();
+            allPossibleSizeArrangements.add(sizeList);
+        }
+
+        /**
+         * Try to list out all possible arrangements by attaching all possible size of each column
+         * in sequence. We have generated supportedOutputSizesList by the priority order for
+         * different use cases. And the supported outputs sizes for each use case are also arranged
+         * from large to small. Therefore, the earlier size arrangement in the result list will be
+         * the better one to choose if finally it won't exceed the camera device's stream
+         * combination capability.
+         */
+        int currentRunCount = totalArrangementsCount;
+        int nextRunCount = currentRunCount / supportedOutputSizesList.get(0).size();
+
+        for (List<Size> supportedOutputSizes : supportedOutputSizesList) {
+            for (int i = 0; i < totalArrangementsCount; i++) {
+                List<Size> surfaceConfigurationList = allPossibleSizeArrangements.get(i);
+
+                surfaceConfigurationList.add(
+                        supportedOutputSizes.get((i % currentRunCount) / nextRunCount));
+            }
+
+            int currentIndex = supportedOutputSizesList.indexOf(supportedOutputSizes);
+
+            if (currentIndex < supportedOutputSizesList.size() - 1) {
+                currentRunCount = nextRunCount;
+                nextRunCount =
+                        currentRunCount / supportedOutputSizesList.get(currentIndex + 1).size();
+            }
+        }
+
+        return allPossibleSizeArrangements;
+    }
+
+    // TODO(b/124267925): Remove @SuppressLint once we target API 21
+    @SuppressLint("ObsoleteSdkInt")
+    private Size[] getAllOutputSizesByFormat(int imageFormat) {
+        if (characteristics == null) {
+            throw new IllegalStateException("CameraCharacteristics is null.");
+        }
+
+        StreamConfigurationMap map =
+                characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+        if (map == null) {
+            throw new IllegalArgumentException(
+                    "Can not get supported output size for the format: " + imageFormat);
+        }
+
+        Size[] outputSizes;
+        if (Build.VERSION.SDK_INT < 23
+                && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+            // This is a little tricky that 0x22 that is internal defined in
+            // StreamConfigurationMap.java
+            // to be equal to ImageFormat.PRIVATE that is public after Android level 23 but not
+            // public in
+            // Android L. Use {@link SurfaceTexture} or {@link MediaCodec} will finally mapped to
+            // 0x22 in
+            // StreamConfigurationMap to retrieve the output sizes information.
+            outputSizes = map.getOutputSizes(SurfaceTexture.class);
+        } else {
+            outputSizes = map.getOutputSizes(imageFormat);
+        }
+
+        if (outputSizes == null) {
+            throw new IllegalArgumentException(
+                    "Can not get supported output size for the format: " + imageFormat);
+        }
+
+        // Sort the output sizes. The Comparator result must be reversed to have a descending order
+        // result.
+        Collections.sort(Arrays.asList(outputSizes), new CompareSizesByArea(true));
+
+        return outputSizes;
+    }
+
+    /**
+     * Get max supported output size for specific image format
+     *
+     * @param imageFormat the image format info
+     * @return the max supported output size for the image format
+     */
+    Size getMaxOutputSizeByFormat(int imageFormat) {
+        Size[] outputSizes = getAllOutputSizesByFormat(imageFormat);
+
+        return Collections.max(Arrays.asList(outputSizes), new CompareSizesByArea());
+    }
+
+    private void init(Context context) {
+        CameraManager cameraManager =
+                (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+        WindowManager windowManager =
+                (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+        try {
+            generateSupportedCombinationList(cameraManager);
+            generateSurfaceSizeDefinition(windowManager);
+        } catch (CameraAccessException e) {
+            throw new IllegalArgumentException(
+                    "Generate supported combination list and size definition fail - CameraId:"
+                            + cameraId,
+                    e);
+        }
+        checkCustomization();
+    }
+
+    List<SurfaceCombination> getLegacySupportedCombinationList() {
+        List<SurfaceCombination> combinationList = new ArrayList<>();
+
+        // (PRIV, MAXIMUM)
+        SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination1);
+
+        // (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination2);
+
+        // (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination3);
+
+        // Below two combinations are all supported in the combination
+        // (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination4);
+
+        // (YUV, PREVIEW) + (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination5);
+
+        // (PRIV, PREVIEW) + (PRIV, PREVIEW)
+        SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        combinationList.add(surfaceCombination6);
+
+        // (PRIV, PREVIEW) + (YUV, PREVIEW)
+        SurfaceCombination surfaceCombination7 = new SurfaceCombination();
+        surfaceCombination7.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination7.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        combinationList.add(surfaceCombination7);
+
+        // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination8 = new SurfaceCombination();
+        surfaceCombination8.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination8.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination8.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination8);
+
+        return combinationList;
+    }
+
+    List<SurfaceCombination> getLimitedSupportedCombinationList() {
+        List<SurfaceCombination> combinationList = new ArrayList<>();
+
+        // (PRIV, PREVIEW) + (PRIV, RECORD)
+        SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.RECORD));
+        combinationList.add(surfaceCombination1);
+
+        // (PRIV, PREVIEW) + (YUV, RECORD)
+        SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD));
+        combinationList.add(surfaceCombination2);
+
+        // (YUV, PREVIEW) + (YUV, RECORD)
+        SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD));
+        combinationList.add(surfaceCombination3);
+
+        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+        SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.RECORD));
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD));
+        combinationList.add(surfaceCombination4);
+
+        // (PRIV, PREVIEW) + (YUV, RECORD) + (JPEG, RECORD)
+        SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD));
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD));
+        combinationList.add(surfaceCombination5);
+
+        // (YUV, PREVIEW) + (YUV, PREVIEW) + (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination6);
+
+        return combinationList;
+    }
+
+    List<SurfaceCombination> getFullSupportedCombinationList() {
+        List<SurfaceCombination> combinationList = new ArrayList<>();
+
+        // (PRIV, PREVIEW) + (PRIV, MAXIMUM)
+        SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination1);
+
+        // (PRIV, PREVIEW) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination2);
+
+        // (YUV, PREVIEW) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination3);
+
+        // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (JPEG, MAXIMUM)
+        SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination4);
+
+        // (YUV, ANALYSIS) + (PRIV, PREVIEW) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS));
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination5);
+
+        // (YUV, ANALYSIS) + (YUV, PREVIEW) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS));
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination6);
+
+        return combinationList;
+    }
+
+    List<SurfaceCombination> getRAWSupportedCombinationList() {
+        List<SurfaceCombination> combinationList = new ArrayList<>();
+
+        // (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination1);
+
+        // (PRIV, PREVIEW) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination2);
+
+        // (YUV, PREVIEW) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination3);
+
+        // (PRIV, PREVIEW) + (PRIV, PREVIEW) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination4 = new SurfaceCombination();
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination4.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination4);
+
+        // (PRIV, PREVIEW) + (YUV, PREVIEW) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination5 = new SurfaceCombination();
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination5.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination5);
+
+        // (YUV, PREVIEW) + (YUV, PREVIEW) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination6 = new SurfaceCombination();
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination6.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination6);
+
+        // (PRIV, PREVIEW) + (JPEG, MAXIMUM) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination7 = new SurfaceCombination();
+        surfaceCombination7.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination7.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        surfaceCombination7.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination7);
+
+        // (YUV, PREVIEW) + (JPEG, MAXIMUM) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination8 = new SurfaceCombination();
+        surfaceCombination8.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination8.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        surfaceCombination8.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination8);
+
+        return combinationList;
+    }
+
+    List<SurfaceCombination> getBurstSupportedCombinationList() {
+        List<SurfaceCombination> combinationList = new ArrayList<>();
+
+        // (PRIV, PREVIEW) + (PRIV, MAXIMUM)
+        SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination1);
+
+        // (PRIV, PREVIEW) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination2);
+
+        // (YUV, PREVIEW) + (YUV, MAXIMUM)
+        SurfaceCombination surfaceCombination3 = new SurfaceCombination();
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW));
+        surfaceCombination3.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination3);
+
+        return combinationList;
+    }
+
+    List<SurfaceCombination> getLevel3SupportedCombinationList() {
+        List<SurfaceCombination> combinationList = new ArrayList<>();
+
+        // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (YUV, MAXIMUM) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination1 = new SurfaceCombination();
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.ANALYSIS));
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM));
+        surfaceCombination1.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination1);
+
+        // (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (JPEG, MAXIMUM) + (RAW, MAXIMUM)
+        SurfaceCombination surfaceCombination2 = new SurfaceCombination();
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.PREVIEW));
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.PRIV, ConfigurationSize.ANALYSIS));
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM));
+        surfaceCombination2.addSurfaceConfiguration(
+                SurfaceConfiguration.create(ConfigurationType.RAW, ConfigurationSize.MAXIMUM));
+        combinationList.add(surfaceCombination2);
+
+        return combinationList;
+    }
+
+    private void generateSupportedCombinationList(CameraManager cameraManager)
+            throws CameraAccessException {
+        characteristics = cameraManager.getCameraCharacteristics(cameraId);
+
+        Integer keyValue = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+
+        if (keyValue != null) {
+            hardwareLevel = keyValue;
+        }
+
+        surfaceCombinationList.addAll(getLegacySupportedCombinationList());
+
+        if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+                || hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+                || hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+            surfaceCombinationList.addAll(getLimitedSupportedCombinationList());
+        }
+
+        if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+                || hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+            surfaceCombinationList.addAll(getFullSupportedCombinationList());
+        }
+
+        int[] availableCapabilities =
+                characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+
+        if (availableCapabilities != null) {
+            for (int capability : availableCapabilities) {
+                if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) {
+                    isRawSupported = true;
+                } else if (capability
+                        == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE) {
+                    isBurstCaptureSupported = true;
+                }
+            }
+        }
+
+        if (isRawSupported) {
+            surfaceCombinationList.addAll(getRAWSupportedCombinationList());
+        }
+
+        if (isBurstCaptureSupported
+                && hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED) {
+            surfaceCombinationList.addAll(getBurstSupportedCombinationList());
+        }
+
+        if (hardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+            surfaceCombinationList.addAll(getLevel3SupportedCombinationList());
+        }
+    }
+
+    private void checkCustomization() {
+        // TODO(b/119466260): Integrate found feasible stream combinations into supported list
+    }
+
+    // Utility classes and methods:
+    // *********************************************************************************************
+
+    private void generateSurfaceSizeDefinition(WindowManager windowManager) {
+        Size analysisSize = new Size(640, 480);
+        Size previewSize = getPreviewSize(windowManager);
+        Size recordSize = getRecordSize();
+
+        Map<Integer, Size> maximumSizeMap = new HashMap<>();
+        maximumSizeMap.put(ImageFormat.JPEG, getMaxOutputSizeByFormat(ImageFormat.JPEG));
+        maximumSizeMap.put(
+                ImageFormat.YUV_420_888, getMaxOutputSizeByFormat(ImageFormat.YUV_420_888));
+        /**
+         * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats like {@link
+         * android.graphics.SurfaceTexture} or {@link android.media.MediaCodec} classes will be
+         * mapped to internal defined format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED (0x22) in
+         * StreamConfigurationMap.java. 0x22 is also the code for ImageFormat.PRIVATE that is public
+         * after Android level 23.Before Android level 23, there is same internal code 0x22 for
+         * internal defined format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED. Therefore, using the
+         * code 0x22 to store maximum size for ViewFinder or VideCapture use cases since they will
+         * finally map to this code.
+         */
+        maximumSizeMap.put(
+                ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
+                getMaxOutputSizeByFormat(
+                        ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE));
+
+        surfaceSizeDefinition =
+                SurfaceSizeDefinition.create(analysisSize, previewSize, recordSize, maximumSizeMap);
+    }
+
+    /**
+     * PREVIEW refers to the best size match to the device's screen resolution, or to 1080p
+     * (1920x1080), whichever is smaller.
+     */
+    private Size getPreviewSize(WindowManager windowManager) {
+        Point displaySize = new Point();
+        windowManager.getDefaultDisplay().getRealSize(displaySize);
+
+        if (displaySize.x > displaySize.y) {
+            displayViewSize = new Size(displaySize.x, displaySize.y);
+        } else {
+            displayViewSize = new Size(displaySize.y, displaySize.x);
+        }
+
+        // Limit the max preview size to under min(display size, 1080P) by comparing the area size
+        Size previewSize =
+                Collections.min(
+                        Arrays.asList(
+                                new Size(displayViewSize.getWidth(), displayViewSize.getHeight()),
+                                MAX_PREVIEW_SIZE),
+                        new CompareSizesByArea());
+
+        return previewSize;
+    }
+
+    /**
+     * RECORD refers to the camera device's maximum supported recording resolution, as determined by
+     * CamcorderProfile.
+     */
+    private Size getRecordSize() {
+        Size recordSize = QUALITY_480P_SIZE;
+
+        // Check whether 2160P, 1080P, 720P, 480P are supported by CamcorderProfile
+        if (camcorderProfileHelper.hasProfile(
+                Integer.parseInt(cameraId), CamcorderProfile.QUALITY_2160P)) {
+            recordSize = QUALITY_2160P_SIZE;
+        } else if (camcorderProfileHelper.hasProfile(
+                Integer.parseInt(cameraId), CamcorderProfile.QUALITY_1080P)) {
+            recordSize = QUALITY_1080P_SIZE;
+        } else if (camcorderProfileHelper.hasProfile(
+                Integer.parseInt(cameraId), CamcorderProfile.QUALITY_720P)) {
+            recordSize = QUALITY_720P_SIZE;
+        } else if (camcorderProfileHelper.hasProfile(
+                Integer.parseInt(cameraId), CamcorderProfile.QUALITY_480P)) {
+            recordSize = QUALITY_480P_SIZE;
+        }
+
+        return recordSize;
+    }
+
+    /** Comparator based on area of the given {@link Size} objects. */
+    static final class CompareSizesByArea implements Comparator<Size> {
+        private boolean reverse = false;
+
+        CompareSizesByArea() {
+        }
+
+        CompareSizesByArea(boolean reverse) {
+            this.reverse = reverse;
+        }
+
+        @Override
+        public int compare(Size lhs, Size rhs) {
+            // We cast here to ensure the multiplications won't overflow
+            int result =
+                    Long.signum(
+                            (long) lhs.getWidth() * lhs.getHeight()
+                                    - (long) rhs.getWidth() * rhs.getHeight());
+
+            if (reverse) {
+                result *= -1;
+            }
+
+            return result;
+        }
+    }
+}
diff --git a/camera/camera2/src/main/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManager.java b/camera/camera2/src/main/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManager.java
new file mode 100644
index 0000000..6c48513
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManager.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase;
+
+import java.util.List;
+
+/**
+ * Collect the use case surface occupancy customization rules in this class to make
+ * Camera2DeviceSurfaceManager independent from use case type.
+ */
+final class UseCaseSurfaceOccupancyManager {
+    private UseCaseSurfaceOccupancyManager() {
+    }
+
+    static void checkUseCaseLimitNotExceeded(
+            List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+        int imageCaptureUseCaseCount = 0;
+        int videoCaptureUseCaseCount = 0;
+
+        if (newUseCases == null || newUseCases.isEmpty()) {
+            throw new IllegalArgumentException("No new use cases to be bound.");
+        }
+
+        if (originalUseCases != null) {
+            for (BaseUseCase useCase : originalUseCases) {
+                if (useCase instanceof ImageCaptureUseCase) {
+                    imageCaptureUseCaseCount++;
+                } else if (useCase instanceof VideoCaptureUseCase) {
+                    videoCaptureUseCaseCount++;
+                }
+            }
+        }
+
+        for (BaseUseCase useCase : newUseCases) {
+            if (useCase instanceof ImageCaptureUseCase) {
+                imageCaptureUseCaseCount++;
+            } else if (useCase instanceof VideoCaptureUseCase) {
+                videoCaptureUseCaseCount++;
+            }
+        }
+
+        if (imageCaptureUseCaseCount > 1) {
+            throw new IllegalArgumentException(
+                    "Exceeded max simultaneously bound image capture use cases.");
+        }
+
+        if (videoCaptureUseCaseCount > 1) {
+            throw new IllegalArgumentException(
+                    "Exceeded max simultaneously bound video capture use cases.");
+        }
+    }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoRobolectricTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoRobolectricTest.java
new file mode 100644
index 0000000..82387b6
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoRobolectricTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.view.Surface;
+
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class Camera2CameraInfoRobolectricTest {
+
+    private static final String CAMERA0_ID = "0";
+    private static final int CAMERA0_SENSOR_ORIENTATION = 90;
+    private static final LensFacing CAMERA0_LENS_FACING_ENUM = LensFacing.BACK;
+    private static final int CAMERA0_LENS_FACING_INT = CameraCharacteristics.LENS_FACING_BACK;
+
+    private static final String CAMERA1_ID = "1";
+    private static final int CAMERA1_SENSOR_ORIENTATION = 0;
+    private static final int CAMERA1_LENS_FACING_INT = CameraCharacteristics.LENS_FACING_FRONT;
+
+    private CameraManager cameraManager;
+
+    @Before
+    public void setUp() {
+        initCameras();
+        cameraManager =
+                ApplicationProvider.getApplicationContext().getSystemService(CameraManager.class);
+    }
+
+    @Test
+    public void canCreateCameraInfo() throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+        assertThat(cameraInfo).isNotNull();
+    }
+
+    @Test
+    public void cameraInfo_canReturnSensorOrientation() throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+        assertThat(cameraInfo.getSensorRotationDegrees()).isEqualTo(CAMERA0_SENSOR_ORIENTATION);
+    }
+
+    @Test
+    public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera()
+            throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+
+        // Note: these numbers depend on the camera being a back-facing camera.
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_0))
+                .isEqualTo(CAMERA0_SENSOR_ORIENTATION);
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_90))
+                .isEqualTo((CAMERA0_SENSOR_ORIENTATION - 90 + 360) % 360);
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_180))
+                .isEqualTo((CAMERA0_SENSOR_ORIENTATION - 180 + 360) % 360);
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_270))
+                .isEqualTo((CAMERA0_SENSOR_ORIENTATION - 270 + 360) % 360);
+    }
+
+    @Test
+    public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera()
+            throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA1_ID);
+
+        // Note: these numbers depend on the camera being a front-facing camera.
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_0))
+                .isEqualTo(CAMERA1_SENSOR_ORIENTATION);
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_90))
+                .isEqualTo((CAMERA1_SENSOR_ORIENTATION + 90) % 360);
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_180))
+                .isEqualTo((CAMERA1_SENSOR_ORIENTATION + 180) % 360);
+        assertThat(cameraInfo.getSensorRotationDegrees(Surface.ROTATION_270))
+                .isEqualTo((CAMERA1_SENSOR_ORIENTATION + 270) % 360);
+    }
+
+    @Test
+    public void cameraInfo_canReturnLensFacing() throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(cameraManager, CAMERA0_ID);
+        assertThat(cameraInfo.getLensFacing()).isEqualTo(CAMERA0_LENS_FACING_ENUM);
+    }
+
+    private void initCameras() {
+        // **** Camera 0 characteristics ****//
+        CameraCharacteristics characteristics0 =
+                ShadowCameraCharacteristics.newCameraCharacteristics();
+
+        ShadowCameraCharacteristics shadowCharacteristics0 = Shadow.extract(characteristics0);
+
+        // Add a lens facing to the camera
+        shadowCharacteristics0.set(CameraCharacteristics.LENS_FACING, CAMERA0_LENS_FACING_INT);
+
+        // Mock the sensor orientation
+        shadowCharacteristics0.set(
+                CameraCharacteristics.SENSOR_ORIENTATION, CAMERA0_SENSOR_ORIENTATION);
+
+        // Add the camera to the camera service
+        ((ShadowCameraManager)
+                Shadow.extract(
+                        ApplicationProvider.getApplicationContext()
+                                .getSystemService(CameraManager.class)))
+                .addCamera(CAMERA0_ID, characteristics0);
+
+        // **** Camera 1 characteristics ****//
+        CameraCharacteristics characteristics1 =
+                ShadowCameraCharacteristics.newCameraCharacteristics();
+
+        ShadowCameraCharacteristics shadowCharacteristics1 = Shadow.extract(characteristics1);
+
+        // Add a lens facing to the camera
+        shadowCharacteristics1.set(CameraCharacteristics.LENS_FACING, CAMERA1_LENS_FACING_INT);
+
+        // Mock the sensor orientation
+        shadowCharacteristics1.set(
+                CameraCharacteristics.SENSOR_ORIENTATION, CAMERA1_SENSOR_ORIENTATION);
+
+        // Add the camera to the camera service
+        ((ShadowCameraManager)
+                Shadow.extract(
+                        ApplicationProvider.getApplicationContext()
+                                .getSystemService(CameraManager.class)))
+                .addCamera(CAMERA1_ID, characteristics1);
+    }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerRobolectricTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerRobolectricTest.java
new file mode 100644
index 0000000..5acb00b
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerRobolectricTest.java
@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build.VERSION_CODES;
+import android.util.Rational;
+import android.util.Size;
+import android.view.WindowManager;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ExtendableUseCaseConfigFactory;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageFormatConstants;
+import androidx.camera.core.StreamConfigurationMapUtil;
+import androidx.camera.core.SurfaceCombination;
+import androidx.camera.core.SurfaceConfiguration;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationSize;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationType;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+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.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Robolectric test for {@link Camera2DeviceSurfaceManager} class */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = VERSION_CODES.LOLLIPOP)
+public final class Camera2DeviceSurfaceManagerRobolectricTest {
+    private static final String LEGACY_CAMERA_ID = "0";
+    private static final String LIMITED_CAMERA_ID = "1";
+    private static final String FULL_CAMERA_ID = "2";
+    private static final String LEVEL3_CAMERA_ID = "3";
+    private final Size displaySize = new Size(1280, 720);
+    private final Size analysisSize = new Size(640, 480);
+    private final Size previewSize = displaySize;
+    private final Size recordSize = new Size(3840, 2160);
+    private final Size maximumSize = new Size(4032, 3024);
+    private final Size maximumVideoSize = new Size(1920, 1080);
+    private final CamcorderProfileHelper mockCamcorderProfileHelper =
+            Mockito.mock(CamcorderProfileHelper.class);
+
+    /**
+     * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats will be mapped to
+     * ImageFormat.PRIVATE (0x22) including SurfaceTexture or MediaCodec classes. Before Android
+     * level 23, there is no ImageFormat.PRIVATE. But there is same internal code 0x22 for internal
+     * corresponding format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED. Therefore, set 0x22 as default
+     * image formate.
+     */
+    private final int[] supportedFormats =
+            new int[]{
+                    ImageFormat.YUV_420_888,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+            };
+
+    private final Size[] supportedSizes =
+            new Size[]{
+                    new Size(4032, 3024),
+                    new Size(3840, 2160),
+                    new Size(1920, 1080),
+                    new Size(1280, 720),
+                    new Size(640, 480),
+                    new Size(320, 240),
+                    new Size(320, 180)
+            };
+
+    private final Context context = RuntimeEnvironment.application.getApplicationContext();
+    private CameraDeviceSurfaceManager surfaceManager;
+
+    @Before
+    public void setUp() {
+        WindowManager windowManager =
+                (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealWidth(displaySize.getWidth());
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealHeight(displaySize.getHeight());
+
+        when(mockCamcorderProfileHelper.hasProfile(anyInt(), anyInt())).thenReturn(true);
+
+        setupCamera();
+    }
+
+    @Test
+    public void checkLegacySurfaceCombinationSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            FULL_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            FULL_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationSupportedInLevel3Device() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEVEL3_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    surfaceManager.checkSupported(
+                            LEVEL3_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+        Rational aspectRatio = new Rational(16, 9);
+        ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+                new ViewFinderUseCaseConfiguration.Builder();
+        VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+                new VideoCaptureUseCaseConfiguration.Builder();
+        ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+                new ImageCaptureUseCaseConfiguration.Builder();
+
+        viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+        videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+        imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+        imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        ImageCaptureUseCase imageCaptureUseCase =
+                new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+        videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        VideoCaptureUseCase videoCaptureUseCase =
+                new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+        viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+        ViewFinderUseCase viewFinderUseCase =
+                new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+        List<BaseUseCase> useCases = new ArrayList<>();
+        useCases.add(imageCaptureUseCase);
+        useCases.add(videoCaptureUseCase);
+        useCases.add(viewFinderUseCase);
+
+        boolean exceptionHappened = false;
+
+        try {
+            // Will throw IllegalArgumentException
+            surfaceManager.getSuggestedResolutions(LEGACY_CAMERA_ID, null, useCases);
+        } catch (IllegalArgumentException e) {
+            exceptionHappened = true;
+        }
+
+        assertTrue(exceptionHappened);
+    }
+
+    @Test
+    public void getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
+        Rational aspectRatio = new Rational(16, 9);
+        ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+                new ViewFinderUseCaseConfiguration.Builder();
+        VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+                new VideoCaptureUseCaseConfiguration.Builder();
+        ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+                new ImageCaptureUseCaseConfiguration.Builder();
+
+        viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+        videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+        imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+        imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        ImageCaptureUseCase imageCaptureUseCase =
+                new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+        videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        VideoCaptureUseCase videoCaptureUseCase =
+                new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+        viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+        ViewFinderUseCase viewFinderUseCase =
+                new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+        List<BaseUseCase> useCases = new ArrayList<>();
+        useCases.add(imageCaptureUseCase);
+        useCases.add(videoCaptureUseCase);
+        useCases.add(viewFinderUseCase);
+        Map<BaseUseCase, Size> suggestedResolutionMap =
+                surfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, null, useCases);
+
+        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+        assertThat(suggestedResolutionMap).containsEntry(imageCaptureUseCase, recordSize);
+        assertThat(suggestedResolutionMap).containsEntry(videoCaptureUseCase, maximumVideoSize);
+        assertThat(suggestedResolutionMap).containsEntry(viewFinderUseCase, previewSize);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVAnalysisSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, analysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVPreviewSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, previewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVRecordSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, recordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVMaximumSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, maximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVNotSupportSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID,
+                        ImageFormat.YUV_420_888,
+                        new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGAnalysisSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, analysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGPreviewSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, previewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGRecordSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, recordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGMaximumSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, maximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGNotSupportSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                surfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID,
+                        ImageFormat.JPEG,
+                        new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void getMaximumSizeForImageFormat() {
+        Size maximumYUVSize =
+                surfaceManager.getMaxOutputSize(LEGACY_CAMERA_ID, ImageFormat.YUV_420_888);
+        assertEquals(maximumSize, maximumYUVSize);
+        Size maximumJPEGSize = surfaceManager.getMaxOutputSize(LEGACY_CAMERA_ID, ImageFormat.JPEG);
+        assertEquals(maximumSize, maximumJPEGSize);
+    }
+
+    private void setupCamera() {
+        addBackFacingCamera(
+                LEGACY_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, null);
+        addBackFacingCamera(
+                LIMITED_CAMERA_ID,
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                null);
+        addBackFacingCamera(
+                FULL_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, null);
+        addBackFacingCamera(
+                LEVEL3_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3, null);
+        initCameraX();
+    }
+
+    private void addBackFacingCamera(String cameraId, int hardwareLevel, int[] capabilities) {
+        CameraCharacteristics characteristics =
+                ShadowCameraCharacteristics.newCameraCharacteristics();
+
+        ShadowCameraCharacteristics shadowCharacteristics = Shadow.extract(characteristics);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel);
+
+        if (capabilities != null) {
+            shadowCharacteristics.set(
+                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities);
+        }
+
+        ((ShadowCameraManager) Shadow.extract(application.getSystemService(Context.CAMERA_SERVICE)))
+                .addCamera(cameraId, characteristics);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(
+                        supportedFormats, supportedSizes));
+    }
+
+    private void initCameraX() {
+        AppConfiguration appConfig = createFakeAppConfiguration();
+        CameraX.init(context, appConfig);
+        surfaceManager = CameraX.getSurfaceManager();
+    }
+
+    private AppConfiguration createFakeAppConfiguration() {
+
+        // Create the camera factory for creating Camera2 camera objects
+        CameraFactory cameraFactory = new Camera2CameraFactory(context);
+
+        // Create the DeviceSurfaceManager for Camera2
+        CameraDeviceSurfaceManager surfaceManager =
+                new Camera2DeviceSurfaceManager(context, mockCamcorderProfileHelper);
+
+        // Create default configuration factory
+        ExtendableUseCaseConfigFactory configFactory = new ExtendableUseCaseConfigFactory();
+        configFactory.installDefaultProvider(
+                ImageAnalysisUseCaseConfiguration.class,
+                new DefaultImageAnalysisConfigurationProvider(cameraFactory));
+        configFactory.installDefaultProvider(
+                ImageCaptureUseCaseConfiguration.class,
+                new DefaultImageCaptureConfigurationProvider(cameraFactory));
+        configFactory.installDefaultProvider(
+                VideoCaptureUseCaseConfiguration.class,
+                new DefaultVideoCaptureConfigurationProvider(cameraFactory));
+        configFactory.installDefaultProvider(
+                ViewFinderUseCaseConfiguration.class,
+                new DefaultViewFinderConfigurationProvider(cameraFactory));
+
+        AppConfiguration.Builder appConfigBuilder =
+                new AppConfiguration.Builder()
+                        .setCameraFactory(cameraFactory)
+                        .setDeviceSurfaceManager(surfaceManager)
+                        .setUseCaseConfigFactory(configFactory);
+
+        return appConfigBuilder.build();
+    }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationRobolectricTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationRobolectricTest.java
new file mode 100644
index 0000000..50abf6e
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationRobolectricTest.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build.VERSION_CODES;
+import android.util.Rational;
+import android.util.Size;
+import android.view.WindowManager;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageFormatConstants;
+import androidx.camera.core.StreamConfigurationMapUtil;
+import androidx.camera.core.SurfaceCombination;
+import androidx.camera.core.SurfaceConfiguration;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationSize;
+import androidx.camera.core.SurfaceConfiguration.ConfigurationType;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+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.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** Robolectric test for {@link SupportedSurfaceCombination} class */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = VERSION_CODES.LOLLIPOP)
+public final class SupportedSurfaceCombinationRobolectricTest {
+    private static final String LEGACY_CAMERA_ID = "0";
+    private static final String LIMITED_CAMERA_ID = "1";
+    private static final String FULL_CAMERA_ID = "2";
+    private static final String LEVEL3_CAMERA_ID = "3";
+    private final Size displaySize = new Size(1280, 720);
+    private final Size analysisSize = new Size(640, 480);
+    private final Size previewSize = displaySize;
+    private final Size recordSize = new Size(3840, 2160);
+    private final Size maximumSize = new Size(4032, 3024);
+    private final Size maximumVideoSize = new Size(1920, 1080);
+    private final CamcorderProfileHelper mockCamcorderProfileHelper =
+            Mockito.mock(CamcorderProfileHelper.class);
+
+    /**
+     * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats will be mapped to
+     * ImageFormat.PRIVATE (0x22) including SurfaceTexture or MediaCodec classes. Before Android
+     * level 23, there is no ImageFormat.PRIVATE. But there is same internal code 0x22 for internal
+     * corresponding format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED. Therefore, set 0x22 as default
+     * image formate.
+     */
+    private final int[] supportedFormats =
+            new int[]{
+                    ImageFormat.YUV_420_888,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+            };
+
+    private final Size[] supportedSizes =
+            new Size[]{
+                    new Size(4032, 3024),
+                    new Size(3840, 2160),
+                    new Size(1920, 1080),
+                    new Size(1280, 720),
+                    new Size(640, 480),
+                    new Size(320, 240),
+                    new Size(320, 180)
+            };
+
+    private final Context context = RuntimeEnvironment.application.getApplicationContext();
+
+    @Before
+    public void setUp() {
+        WindowManager windowManager =
+                (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealWidth(displaySize.getWidth());
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealHeight(displaySize.getHeight());
+
+        when(mockCamcorderProfileHelper.hasProfile(anyInt(), anyInt())).thenReturn(true);
+
+        setupCamera();
+    }
+
+    @Test
+    public void checkLegacySurfaceCombinationSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLegacySurfaceCombinationSubListSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationSubListSupportedInLimited3Device() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationSubListSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, FULL_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationSupportedInLevel3Device() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEVEL3_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    supportedSurfaceCombination.checkSupported(
+                            combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationSubListSupportedInLevel3Device() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEVEL3_CAMERA_ID, mockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+
+        Rational aspectRatio = new Rational(16, 9);
+        ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+                new ViewFinderUseCaseConfiguration.Builder();
+        VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+                new VideoCaptureUseCaseConfiguration.Builder();
+        ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+                new ImageCaptureUseCaseConfiguration.Builder();
+
+        viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+        videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+        imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+        imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        ImageCaptureUseCase imageCaptureUseCase =
+                new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+        videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        VideoCaptureUseCase videoCaptureUseCase =
+                new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+        viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+        ViewFinderUseCase viewFinderUseCase =
+                new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+        List<BaseUseCase> useCases = new ArrayList<>();
+        useCases.add(imageCaptureUseCase);
+        useCases.add(videoCaptureUseCase);
+        useCases.add(viewFinderUseCase);
+        Map<BaseUseCase, Size> suggestedResolutionMap =
+                supportedSurfaceCombination.getSuggestedResolutions(null, useCases);
+
+        assertTrue(suggestedResolutionMap.size() != 3);
+    }
+
+    @Test
+    public void getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LIMITED_CAMERA_ID, mockCamcorderProfileHelper);
+
+        Rational aspectRatio = new Rational(16, 9);
+        ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+                new ViewFinderUseCaseConfiguration.Builder();
+        VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder =
+                new VideoCaptureUseCaseConfiguration.Builder();
+        ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder =
+                new ImageCaptureUseCaseConfiguration.Builder();
+
+        viewFinderConfigBuilder.setTargetAspectRatio(aspectRatio);
+        videoCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+        imageCaptureConfigBuilder.setTargetAspectRatio(aspectRatio);
+
+        imageCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        ImageCaptureUseCase imageCaptureUseCase =
+                new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+        videoCaptureConfigBuilder.setLensFacing(LensFacing.BACK);
+        VideoCaptureUseCase videoCaptureUseCase =
+                new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+        viewFinderConfigBuilder.setLensFacing(LensFacing.BACK);
+        ViewFinderUseCase viewFinderUseCase =
+                new ViewFinderUseCase(viewFinderConfigBuilder.build());
+
+        List<BaseUseCase> useCases = new ArrayList<>();
+        useCases.add(imageCaptureUseCase);
+        useCases.add(videoCaptureUseCase);
+        useCases.add(viewFinderUseCase);
+        Map<BaseUseCase, Size> suggestedResolutionMap =
+                supportedSurfaceCombination.getSuggestedResolutions(null, useCases);
+
+        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+        assertThat(suggestedResolutionMap).containsEntry(imageCaptureUseCase, recordSize);
+        assertThat(suggestedResolutionMap).containsEntry(videoCaptureUseCase, maximumVideoSize);
+        assertThat(suggestedResolutionMap).containsEntry(viewFinderUseCase, previewSize);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVAnalysisSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, analysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVPreviewSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, previewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVRecordSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, recordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVMaximumSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, maximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVNotSupportSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888,
+                        new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGAnalysisSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, analysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGPreviewSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, previewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGRecordSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, recordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGMaximumSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, maximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGNotSupportSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG,
+                        new Size(maximumSize.getWidth() + 1, maximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void getMaximumSizeForImageFormat() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        context, LEGACY_CAMERA_ID, mockCamcorderProfileHelper);
+        Size maximumYUVSize =
+                supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.YUV_420_888);
+        assertEquals(maximumSize, maximumYUVSize);
+        Size maximumJPEGSize =
+                supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG);
+        assertEquals(maximumSize, maximumJPEGSize);
+    }
+
+    private void setupCamera() {
+        addBackFacingCamera(
+                LEGACY_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, null);
+        addBackFacingCamera(
+                LIMITED_CAMERA_ID,
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                null);
+        addBackFacingCamera(
+                FULL_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, null);
+        addBackFacingCamera(
+                LEVEL3_CAMERA_ID, CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3, null);
+        initCameraX();
+    }
+
+    private void addBackFacingCamera(String cameraId, int hardwareLevel, int[] capabilities) {
+        CameraCharacteristics characteristics =
+                ShadowCameraCharacteristics.newCameraCharacteristics();
+
+        ShadowCameraCharacteristics shadowCharacteristics = Shadow.extract(characteristics);
+        shadowCharacteristics.set(
+                CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel);
+
+        if (capabilities != null) {
+            shadowCharacteristics.set(
+                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities);
+        }
+
+        ((ShadowCameraManager) Shadow.extract(application.getSystemService(Context.CAMERA_SERVICE)))
+                .addCamera(cameraId, characteristics);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(
+                        supportedFormats, supportedSizes));
+    }
+
+    private void initCameraX() {
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraX.init(context, appConfig);
+    }
+
+    private boolean isAllSubConfigurationListSupported(
+            SupportedSurfaceCombination supportedSurfaceCombination,
+            List<SurfaceCombination> combinationList) {
+        boolean isSupported = true;
+
+        for (SurfaceCombination combination : combinationList) {
+            List<SurfaceConfiguration> configurationList =
+                    combination.getSurfaceConfigurationList();
+            int length = configurationList.size();
+
+            if (length <= 1) {
+                continue;
+            }
+
+            for (int index = 0; index < length; index++) {
+                List<SurfaceConfiguration> subConfigurationList = new ArrayList<>();
+                subConfigurationList.addAll(configurationList);
+                subConfigurationList.remove(index);
+
+                isSupported &= supportedSurfaceCombination.checkSupported(subConfigurationList);
+
+                if (!isSupported) {
+                    return false;
+                }
+            }
+        }
+
+        return isSupported;
+    }
+}
diff --git a/camera/core/proguard.flags b/camera/core/proguard.flags
new file mode 100644
index 0000000..06c47ac
--- /dev/null
+++ b/camera/core/proguard.flags
@@ -0,0 +1,85 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Save the obfuscation mapping to a file, so we can de-obfuscate any stack
+# traces later on. Keep a fixed source file attribute and all line number
+# tables to get line numbers in the stack traces.
+# You can comment this out if you're not interested in stack traces.
+
+-printmapping out.map
+-keepparameternames
+-renamesourcefileattribute SourceFile
+-keepattributes Exceptions,InnerClasses,Deprecated,
+                SourceFile,LineNumberTable,EnclosingMethod
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+    public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+    java.lang.Class class$(java.lang.String);
+    java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+    public static **[] values();
+    public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+    static final long serialVersionUID;
+    static final java.io.ObjectStreamField[] serialPersistentFields;
+    private void writeObject(java.io.ObjectOutputStream);
+    private void readObject(java.io.ObjectInputStream);
+    java.lang.Object writeReplace();
+    java.lang.Object readResolve();
+}
+
+# Keep constructors for UseCase classes. They are used in reflection.
+-keepclassmembers class androidx.camera.core.BaseUseCase {
+   protected <init>(...);
+}
+-keepclassmembers class * extends androidx.camera.core.BaseUseCase {
+  public <init>(...);
+}
+
+# Keep generic types for the TypeReference class
+-keepattributes Signature
+
+# Keep the TypeReference class as it uses self-inspection
+-keep class * extends androidx.camera.core.TypeReference
diff --git a/camera/core/src/androidTest/AndroidManifest.xml b/camera/core/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..cc2b1cb
--- /dev/null
+++ b/camera/core/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.core">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application android:debuggable="true">
+        <uses-library
+            android:name="android.test.runner"
+            android:required="false" />
+        <uses-library
+            android:name="android.test.base"
+            android:required="false" />
+        <uses-library
+            android:name="android.test.mock"
+            android:required="false" />
+
+        <activity
+            android:name="androidx.camera.core.FakeActivity"
+            android:label="Fake Activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyAndroidTest.java
new file mode 100644
index 0000000..62da537
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyAndroidTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.media.Image;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+public final class AndroidImageProxyAndroidTest {
+    private static final long INITIAL_TIMESTAMP = 138990020L;
+
+    private final Image image = mock(Image.class);
+    private final Image.Plane yPlane = mock(Image.Plane.class);
+    private final Image.Plane uPlane = mock(Image.Plane.class);
+    private final Image.Plane vPlane = mock(Image.Plane.class);
+    private ImageProxy imageProxy;
+
+    @Before
+    public void setUp() {
+        when(image.getPlanes()).thenReturn(new Image.Plane[]{yPlane, uPlane, vPlane});
+        when(yPlane.getRowStride()).thenReturn(640);
+        when(yPlane.getPixelStride()).thenReturn(1);
+        when(yPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(640 * 480));
+        when(uPlane.getRowStride()).thenReturn(320);
+        when(uPlane.getPixelStride()).thenReturn(1);
+        when(uPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(320 * 240));
+        when(vPlane.getRowStride()).thenReturn(320);
+        when(vPlane.getPixelStride()).thenReturn(1);
+        when(vPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(320 * 240));
+
+        when(image.getTimestamp()).thenReturn(INITIAL_TIMESTAMP);
+        imageProxy = new AndroidImageProxy(image);
+    }
+
+    @Test
+    public void close_closesWrappedImage() {
+        imageProxy.close();
+
+        verify(image).close();
+    }
+
+    @Test
+    public void getCropRect_returnsCropRectForWrappedImage() {
+        when(image.getCropRect()).thenReturn(new Rect(0, 0, 20, 20));
+
+        assertThat(imageProxy.getCropRect()).isEqualTo(new Rect(0, 0, 20, 20));
+    }
+
+    @Test
+    public void setCropRect_setsCropRectForWrappedImage() {
+        imageProxy.setCropRect(new Rect(0, 0, 40, 40));
+
+        verify(image).setCropRect(new Rect(0, 0, 40, 40));
+    }
+
+    @Test
+    public void getFormat_returnsFormatForWrappedImage() {
+        when(image.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+        assertThat(imageProxy.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
+    }
+
+    @Test
+    public void getHeight_returnsHeightForWrappedImage() {
+        when(image.getHeight()).thenReturn(480);
+
+        assertThat(imageProxy.getHeight()).isEqualTo(480);
+    }
+
+    @Test
+    public void getWidth_returnsWidthForWrappedImage() {
+        when(image.getWidth()).thenReturn(640);
+
+        assertThat(imageProxy.getWidth()).isEqualTo(640);
+    }
+
+    @Test
+    public void getTimestamp_returnsTimestampForWrappedImage() {
+        assertThat(imageProxy.getTimestamp()).isEqualTo(INITIAL_TIMESTAMP);
+    }
+
+    public void setTimestamp_setsTimestampForWrappedImage() {
+        imageProxy.setTimestamp(INITIAL_TIMESTAMP + 10);
+
+        assertThat(imageProxy.getTimestamp()).isEqualTo(INITIAL_TIMESTAMP + 10);
+    }
+
+    @Test
+    public void getPlanes_returnsPlanesForWrappedImage() {
+        ImageProxy.PlaneProxy[] wrappedPlanes = imageProxy.getPlanes();
+
+        Image.Plane[] originalPlanes = new Image.Plane[]{yPlane, uPlane, vPlane};
+        assertThat(wrappedPlanes.length).isEqualTo(3);
+        for (int i = 0; i < 3; ++i) {
+            assertThat(wrappedPlanes[i].getRowStride()).isEqualTo(originalPlanes[i].getRowStride());
+            assertThat(wrappedPlanes[i].getPixelStride())
+                    .isEqualTo(originalPlanes[i].getPixelStride());
+            assertThat(wrappedPlanes[i].getBuffer()).isEqualTo(originalPlanes[i].getBuffer());
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyAndroidTest.java
new file mode 100644
index 0000000..053681c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyAndroidTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+@RunWith(AndroidJUnit4.class)
+public final class AndroidImageReaderProxyAndroidTest {
+    private final ImageReader imageReader = mock(ImageReader.class);
+    private ImageReaderProxy imageReaderProxy;
+
+    @Before
+    public void setUp() {
+        imageReaderProxy = new AndroidImageReaderProxy(imageReader);
+        when(imageReader.acquireLatestImage()).thenReturn(mock(Image.class));
+        when(imageReader.acquireNextImage()).thenReturn(mock(Image.class));
+    }
+
+    @Test
+    public void acquireLatestImage_invokesMethodOnWrappedReader() {
+        imageReaderProxy.acquireLatestImage();
+
+        verify(imageReader, times(1)).acquireLatestImage();
+    }
+
+    @Test
+    public void acquireNextImage_invokesMethodOnWrappedReader() {
+        imageReaderProxy.acquireNextImage();
+
+        verify(imageReader, times(1)).acquireNextImage();
+    }
+
+    @Test
+    public void close_invokesMethodOnWrappedReader() {
+        imageReaderProxy.close();
+
+        verify(imageReader, times(1)).close();
+    }
+
+    @Test
+    public void getWidth_returnsWidthOfWrappedReader() {
+        when(imageReader.getWidth()).thenReturn(640);
+
+        assertThat(imageReaderProxy.getWidth()).isEqualTo(640);
+    }
+
+    @Test
+    public void getHeight_returnsHeightOfWrappedReader() {
+        when(imageReader.getHeight()).thenReturn(480);
+
+        assertThat(imageReaderProxy.getHeight()).isEqualTo(480);
+    }
+
+    @Test
+    public void getImageFormat_returnsImageFormatOfWrappedReader() {
+        when(imageReader.getImageFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+        assertThat(imageReaderProxy.getImageFormat()).isEqualTo(ImageFormat.YUV_420_888);
+    }
+
+    @Test
+    public void getMaxImages_returnsMaxImagesOfWrappedReader() {
+        when(imageReader.getMaxImages()).thenReturn(8);
+
+        assertThat(imageReaderProxy.getMaxImages()).isEqualTo(8);
+    }
+
+    @Test
+    public void getSurface_returnsSurfaceOfWrappedReader() {
+        Surface surface = mock(Surface.class);
+        when(imageReader.getSurface()).thenReturn(surface);
+
+        assertThat(imageReaderProxy.getSurface()).isSameAs(surface);
+    }
+
+    @Test
+    public void setOnImageAvailableListener_setsListenerOfWrappedReader() {
+        ImageReaderProxy.OnImageAvailableListener listener =
+                mock(ImageReaderProxy.OnImageAvailableListener.class);
+
+        imageReaderProxy.setOnImageAvailableListener(listener, /*handler=*/ null);
+
+        ArgumentCaptor<ImageReader.OnImageAvailableListener> transformedListenerCaptor =
+                ArgumentCaptor.forClass(ImageReader.OnImageAvailableListener.class);
+        ArgumentCaptor<Handler> handlerCaptor = ArgumentCaptor.forClass(Handler.class);
+        verify(imageReader, times(1))
+                .setOnImageAvailableListener(
+                        transformedListenerCaptor.capture(), handlerCaptor.capture());
+
+        transformedListenerCaptor.getValue().onImageAvailable(imageReader);
+        verify(listener, times(1)).onImageAvailable(imageReaderProxy);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseAndroidTest.java
new file mode 100644
index 0000000..bd7fdf0
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseAndroidTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.util.Size;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class BaseUseCaseAndroidTest {
+    private BaseUseCase.StateChangeListener mockUseCaseListener;
+
+    @Before
+    public void setup() {
+        mockUseCaseListener = Mockito.mock(BaseUseCase.StateChangeListener.class);
+    }
+
+    @Test
+    public void getAttachedCamera() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        SessionConfiguration sessionToAttach = new SessionConfiguration.Builder().build();
+        testUseCase.attachToCamera("Camera", sessionToAttach);
+
+        Set<String> attachedCameras = testUseCase.getAttachedCameraIds();
+
+        assertThat(attachedCameras).contains("Camera");
+    }
+
+    @Test
+    public void getAttachedSessionConfiguration() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        SessionConfiguration sessionToAttach = new SessionConfiguration.Builder().build();
+        testUseCase.attachToCamera("Camera", sessionToAttach);
+
+        SessionConfiguration attachedSession = testUseCase.getSessionConfiguration("Camera");
+
+        assertThat(attachedSession).isEqualTo(sessionToAttach);
+    }
+
+    @Test
+    public void removeListener() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mockUseCaseListener);
+        testUseCase.removeStateChangeListener(mockUseCaseListener);
+
+        testUseCase.activate();
+
+        verify(mockUseCaseListener, never()).onUseCaseActive(any());
+    }
+
+    @Test
+    public void clearListeners() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mockUseCaseListener);
+        testUseCase.clear();
+
+        testUseCase.activate();
+        verify(mockUseCaseListener, never()).onUseCaseActive(any());
+    }
+
+    @Test
+    public void notifyActiveState() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mockUseCaseListener);
+
+        testUseCase.activate();
+        verify(mockUseCaseListener, times(1)).onUseCaseActive(testUseCase);
+    }
+
+    @Test
+    public void notifyInactiveState() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mockUseCaseListener);
+
+        testUseCase.deactivate();
+        verify(mockUseCaseListener, times(1)).onUseCaseInactive(testUseCase);
+    }
+
+    @Test
+    public void notifyUpdatedSettings() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mockUseCaseListener);
+
+        testUseCase.update();
+        verify(mockUseCaseListener, times(1)).onUseCaseUpdated(testUseCase);
+    }
+
+    @Test
+    public void notifyResetUseCase() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mockUseCaseListener);
+
+        testUseCase.notifyReset();
+        verify(mockUseCaseListener, times(1)).onUseCaseReset(testUseCase);
+    }
+
+    @Test
+    public void notifySingleCapture() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mockUseCaseListener);
+        CaptureRequestConfiguration captureRequestConfiguration =
+                new CaptureRequestConfiguration.Builder().build();
+
+        testUseCase.notifySingleCapture(captureRequestConfiguration);
+        verify(mockUseCaseListener, times(1))
+                .onUseCaseSingleRequest(testUseCase, captureRequestConfiguration);
+    }
+
+    @Test
+    public void useCaseConfiguration_canBeUpdated() {
+        String originalName = "UseCase";
+        FakeUseCaseConfiguration.Builder configurationBuilder =
+                new FakeUseCaseConfiguration.Builder().setTargetName(originalName);
+
+        TestUseCase testUseCase = new TestUseCase(configurationBuilder.build());
+        String originalRetrievedName = testUseCase.getUseCaseConfiguration().getTargetName();
+
+        // NOTE: Updating the use case name is probably a very bad idea in most cases. However,
+        // we'll do
+        // it here for the sake of this test.
+        String newName = "UseCase-New";
+        configurationBuilder.setTargetName(newName);
+        testUseCase.updateUseCaseConfiguration(configurationBuilder.build());
+        String newRetrievedName = testUseCase.getUseCaseConfiguration().getTargetName();
+
+        assertThat(originalRetrievedName).isEqualTo(originalName);
+        assertThat(newRetrievedName).isEqualTo(newName);
+    }
+
+    static class TestUseCase extends FakeUseCase {
+        TestUseCase(FakeUseCaseConfiguration configuration) {
+            super(configuration);
+        }
+
+        void activate() {
+            notifyActive();
+        }
+
+        void deactivate() {
+            notifyInactive();
+        }
+
+        void update() {
+            notifyUpdated();
+        }
+
+        @Override
+        protected void updateUseCaseConfiguration(UseCaseConfiguration<?> useCaseConfiguration) {
+            super.updateUseCaseConfiguration(useCaseConfiguration);
+        }
+
+        @Override
+        protected Map<String, Size> onSuggestedResolutionUpdated(
+                Map<String, Size> suggestedResolutionMap) {
+            return suggestedResolutionMap;
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksAndroidTest.java
new file mode 100644
index 0000000..7877984
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksAndroidTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraCaptureCallbacksAndroidTest {
+
+    @Test
+    public void comboCallbackInvokesConstituentCallbacks() {
+        CameraCaptureCallback callback0 = Mockito.mock(CameraCaptureCallback.class);
+        CameraCaptureCallback callback1 = Mockito.mock(CameraCaptureCallback.class);
+        CameraCaptureCallback comboCallback =
+                CameraCaptureCallbacks.createComboCallback(callback0, callback1);
+        CameraCaptureResult result = Mockito.mock(CameraCaptureResult.class);
+        CameraCaptureFailure failure = new CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR);
+
+        comboCallback.onCaptureCompleted(result);
+        verify(callback0, times(1)).onCaptureCompleted(result);
+        verify(callback1, times(1)).onCaptureCompleted(result);
+
+        comboCallback.onCaptureFailed(failure);
+        verify(callback0, times(1)).onCaptureFailed(failure);
+        verify(callback1, times(1)).onCaptureFailed(failure);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureAndroidTest.java
new file mode 100644
index 0000000..105c2d1
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureAndroidTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.core.CameraCaptureFailure.Reason;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CameraCaptureFailureAndroidTest {
+
+    @Test
+    public void getReason() {
+        CameraCaptureFailure failure = new CameraCaptureFailure(Reason.ERROR);
+        assertThat(failure.getReason()).isEqualTo(Reason.ERROR);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksAndroidTest.java
new file mode 100644
index 0000000..e67da4a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksAndroidTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.os.Build;
+import android.view.Surface;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraCaptureSessionStateCallbacksAndroidTest {
+
+    @Test
+    public void comboCallbackInvokesConstituentCallbacks() {
+        CameraCaptureSession.StateCallback callback0 =
+                Mockito.mock(CameraCaptureSession.StateCallback.class);
+        CameraCaptureSession.StateCallback callback1 =
+                Mockito.mock(CameraCaptureSession.StateCallback.class);
+        CameraCaptureSession.StateCallback comboCallback =
+                CameraCaptureSessionStateCallbacks.createComboCallback(callback0, callback1);
+        CameraCaptureSession session = Mockito.mock(CameraCaptureSession.class);
+        Surface surface = Mockito.mock(Surface.class);
+
+        comboCallback.onConfigured(session);
+        verify(callback0, times(1)).onConfigured(session);
+        verify(callback1, times(1)).onConfigured(session);
+
+        comboCallback.onActive(session);
+        verify(callback0, times(1)).onActive(session);
+        verify(callback1, times(1)).onActive(session);
+
+        comboCallback.onClosed(session);
+        verify(callback0, times(1)).onClosed(session);
+        verify(callback1, times(1)).onClosed(session);
+
+        comboCallback.onReady(session);
+        verify(callback0, times(1)).onReady(session);
+        verify(callback1, times(1)).onReady(session);
+
+        if (Build.VERSION.SDK_INT >= 26) {
+            comboCallback.onCaptureQueueEmpty(session);
+            verify(callback0, times(1)).onCaptureQueueEmpty(session);
+            verify(callback1, times(1)).onCaptureQueueEmpty(session);
+        }
+
+        if (Build.VERSION.SDK_INT >= 23) {
+            comboCallback.onSurfacePrepared(session, surface);
+            verify(callback0, times(1)).onSurfacePrepared(session, surface);
+            verify(callback1, times(1)).onSurfacePrepared(session, surface);
+        }
+
+        comboCallback.onConfigureFailed(session);
+        verify(callback0, times(1)).onConfigureFailed(session);
+        verify(callback1, times(1)).onConfigureFailed(session);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksAndroidTest.java
new file mode 100644
index 0000000..b1d22cc
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksAndroidTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraDevice;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class CameraDeviceStateCallbacksAndroidTest {
+
+    @Test
+    public void comboCallbackInvokesConstituentCallbacks() {
+        CameraDevice.StateCallback callback0 = Mockito.mock(CameraDevice.StateCallback.class);
+        CameraDevice.StateCallback callback1 = Mockito.mock(CameraDevice.StateCallback.class);
+        CameraDevice.StateCallback comboCallback =
+                CameraDeviceStateCallbacks.createComboCallback(callback0, callback1);
+        CameraDevice device = Mockito.mock(CameraDevice.class);
+
+        comboCallback.onOpened(device);
+        verify(callback0, times(1)).onOpened(device);
+        verify(callback1, times(1)).onOpened(device);
+
+        comboCallback.onClosed(device);
+        verify(callback0, times(1)).onClosed(device);
+        verify(callback1, times(1)).onClosed(device);
+
+        comboCallback.onDisconnected(device);
+        verify(callback0, times(1)).onDisconnected(device);
+        verify(callback1, times(1)).onDisconnected(device);
+
+        final int error = 1;
+        comboCallback.onError(device, error);
+        verify(callback0, times(1)).onError(device, error);
+        verify(callback1, times(1)).onError(device, error);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryAndroidTest.java
new file mode 100644
index 0000000..29793d6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryAndroidTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public final class CameraRepositoryAndroidTest {
+
+    private CameraRepository cameraRepository;
+
+    @Before
+    public void setUp() {
+        cameraRepository = new CameraRepository();
+        cameraRepository.init(new FakeCameraFactory());
+    }
+
+    @Test
+    public void cameraIdsCanBeAcquired() {
+        Set<String> cameraIds = cameraRepository.getCameraIds();
+
+        assertThat(cameraIds).isNotEmpty();
+    }
+
+    @Test
+    public void cameraCanBeObtainedWithValidId() {
+        for (String cameraId : cameraRepository.getCameraIds()) {
+            BaseCamera camera = cameraRepository.getCamera(cameraId);
+
+            assertThat(camera).isNotNull();
+        }
+    }
+
+    @Test
+    public void cameraCannotBeObtainedWithInvalidId() {
+        assertThrows(
+                IllegalArgumentException.class, () -> cameraRepository.getCamera("no_such_id"));
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraUtil.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraUtil.java
new file mode 100644
index 0000000..645812a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraUtil.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Utility functions for obtaining instances of camera2 classes. */
+public final class CameraUtil {
+    /** Amount of time to wait before timing out when trying to open a {@link CameraDevice}. */
+    private static final int CAMERA_OPEN_TIMEOUT_SECONDS = 2;
+
+    /**
+     * Gets a new instance of a {@link CameraDevice}.
+     *
+     * <p>This method attempts to open up a new camera. Since the camera api is asynchronous it
+     * needs to wait for camera open
+     *
+     * <p>After the camera is no longer needed {@link #releaseCameraDevice(CameraDevice)} should be
+     * called to clean up resources.
+     *
+     * @throws CameraAccessException if the device is unable to access the camera
+     * @throws InterruptedException  if a {@link CameraDevice} can not be retrieved within a set
+     * time
+     */
+    public static CameraDevice getCameraDevice()
+            throws CameraAccessException, InterruptedException {
+        // Setup threading required for callback on openCamera()
+        HandlerThread handlerThread = new HandlerThread("handler thread");
+        handlerThread.start();
+        Handler handler = new Handler(handlerThread.getLooper());
+
+        CameraManager cameraManager = getCameraManager();
+
+        // Use the first camera available.
+        String[] cameraIds = cameraManager.getCameraIdList();
+        if (cameraIds.length <= 0) {
+            throw new CameraAccessException(
+                    CameraAccessException.CAMERA_ERROR, "Device contains no cameras.");
+        }
+        String cameraName = cameraIds[0];
+
+        // Use an AtomicReference to store the CameraDevice because it is initialized in a lambda.
+        // This
+        // way the AtomicReference itself is effectively final.
+        AtomicReference<CameraDevice> cameraDeviceHolder = new AtomicReference<>();
+
+        // Open the camera using the CameraManager which returns a valid and open CameraDevice only
+        // when
+        // onOpened() is called.
+        CountDownLatch latch = new CountDownLatch(1);
+        cameraManager.openCamera(
+                cameraName,
+                new StateCallback() {
+                    @Override
+                    public void onOpened(CameraDevice camera) {
+                        cameraDeviceHolder.set(camera);
+                        latch.countDown();
+                    }
+
+                    @Override
+                    public void onClosed(CameraDevice cameraDevice) {
+                        handlerThread.quit();
+                    }
+
+                    @Override
+                    public void onDisconnected(CameraDevice camera) {
+                    }
+
+                    @Override
+                    public void onError(CameraDevice camera, int error) {
+                    }
+                },
+                handler);
+
+        // Wait for the callback to initialize the CameraDevice
+        latch.await(CAMERA_OPEN_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        return cameraDeviceHolder.get();
+    }
+
+    /**
+     * Cleans up resources that need to be kept around while the camera device is active.
+     *
+     * @param cameraDevice camera that was obtained via {@link #getCameraDevice()}
+     */
+    public static void releaseCameraDevice(CameraDevice cameraDevice) {
+        cameraDevice.close();
+    }
+
+    public static CameraManager getCameraManager() {
+        return (CameraManager)
+                ApplicationProvider.getApplicationContext()
+                        .getSystemService(Context.CAMERA_SERVICE);
+    }
+
+    /**
+     * Opens a camera and associates the camera with multiple use cases.
+     *
+     * <p>Sets the use case to be online and active, so that the use case is in a state to issue
+     * capture requests to the camera. The caller is responsible for making the use case inactive
+     * and offline and for closing the camera afterwards.
+     *
+     * @param camera   to open
+     * @param useCases to associate with
+     */
+    public static void openCameraWithUseCase(BaseCamera camera, BaseUseCase... useCases) {
+        camera.addOnlineUseCase(Arrays.asList(useCases));
+        for (BaseUseCase useCase : useCases) {
+            camera.onUseCaseActive(useCase);
+        }
+    }
+
+    /**
+     * Detach multiple use cases from a camera.
+     *
+     * <p>Sets the use cases to be inactive and remove from the online list.
+     *
+     * @param camera   to detach from
+     * @param useCases to be detached
+     */
+    public static void detachUseCaseFromCamera(BaseCamera camera, BaseUseCase... useCases) {
+        for (BaseUseCase useCase : useCases) {
+            camera.onUseCaseInactive(useCase);
+        }
+        camera.removeOnlineUseCase(Arrays.asList(useCases));
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraXAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraXAndroidTest.java
new file mode 100644
index 0000000..6189396
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraXAndroidTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Size;
+
+import androidx.camera.core.CameraX.ErrorCode;
+import androidx.camera.core.CameraX.ErrorListener;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
+import androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(AndroidJUnit4.class)
+public final class CameraXAndroidTest {
+    static CameraFactory cameraFactory = new FakeCameraFactory();
+    String cameraId;
+    BaseCamera camera;
+    private FakeLifecycleOwner lifecycle;
+    private CountingErrorListener errorListener;
+    private CountDownLatch latch;
+    private HandlerThread handlerThread;
+    private Handler handler;
+
+    private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+        try {
+            return CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to get camera id for camera lens facing " + lensFacing, e);
+        }
+    }
+
+    @Before
+    public void setUp() {
+        Context context = ApplicationProvider.getApplicationContext();
+        CameraDeviceSurfaceManager surfaceManager = new FakeCameraDeviceSurfaceManager();
+        UseCaseConfigurationFactory defaultConfigFactory = new ExtendableUseCaseConfigFactory();
+        AppConfiguration.Builder appConfigBuilder =
+                new AppConfiguration.Builder()
+                        .setCameraFactory(cameraFactory)
+                        .setDeviceSurfaceManager(surfaceManager)
+                        .setUseCaseConfigFactory(defaultConfigFactory);
+
+        // CameraX.init will actually init just once across all test cases. However we need to get
+        // the real CameraFactory instance being injected into the init process.  So here we store
+        // the
+        // CameraFactory instance in static fields.
+        CameraX.init(context, appConfigBuilder.build());
+        lifecycle = new FakeLifecycleOwner();
+        cameraId = getCameraIdUnchecked(LensFacing.BACK);
+        camera = cameraFactory.getCamera(cameraId);
+        latch = new CountDownLatch(1);
+        errorListener = new CountingErrorListener(latch);
+        handlerThread = new HandlerThread("ErrorHandlerThread");
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        CameraX.unbindAll();
+        handlerThread.quitSafely();
+
+        // Wait some time for the cameras to close. We need the cameras to close to bring CameraX
+        // back
+        // to the initial state.
+        Thread.sleep(3000);
+    }
+
+    @Test
+    public void bind_createsNewUseCaseGroup() {
+        CameraX.bindToLifecycle(lifecycle, new FakeUseCase());
+
+        // One observer is the use case group. The other observer removes the use case upon the
+        // lifecycle's destruction.
+        assertThat(lifecycle.getObserverCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void bindMultipleUseCases() {
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+        FakeUseCase fakeUseCase = new FakeUseCase(configuration0);
+        FakeOtherUseCaseConfiguration configuration1 =
+                new FakeOtherUseCaseConfiguration.Builder().setTargetName("config1").build();
+        FakeOtherUseCase fakeOtherUseCase = new FakeOtherUseCase(configuration1);
+
+        CameraX.bindToLifecycle(lifecycle, fakeUseCase, fakeOtherUseCase);
+
+        assertThat(CameraX.isBound(fakeUseCase)).isTrue();
+        assertThat(CameraX.isBound(fakeOtherUseCase)).isTrue();
+    }
+
+    @Test
+    public void isNotBound_afterUnbind() {
+        FakeUseCase fakeUseCase = new FakeUseCase();
+        CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+        CameraX.unbind(fakeUseCase);
+        assertThat(CameraX.isBound(fakeUseCase)).isFalse();
+    }
+
+    @Test
+    public void bind_createsDifferentUseCaseGroups_forDifferentLifecycles() {
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+        CameraX.bindToLifecycle(lifecycle, new FakeUseCase(configuration0));
+
+        FakeUseCaseConfiguration configuration1 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config1").build();
+        FakeLifecycleOwner anotherLifecycle = new FakeLifecycleOwner();
+        CameraX.bindToLifecycle(anotherLifecycle, new FakeUseCase(configuration1));
+
+        // One observer is the use case group. The other observer removes the use case upon the
+        // lifecycle's destruction.
+        assertThat(lifecycle.getObserverCount()).isEqualTo(2);
+        assertThat(anotherLifecycle.getObserverCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void exception_withDestroyedLifecycle() {
+        FakeUseCase useCase = new FakeUseCase();
+
+        lifecycle.destroy();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    CameraX.bindToLifecycle(lifecycle, useCase);
+                });
+    }
+
+    @Test
+    public void errorListenerGetsCalled_whenErrorPosted() throws InterruptedException {
+        CameraX.setErrorListener(errorListener, handler);
+        CameraX.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+        latch.await(1, TimeUnit.SECONDS);
+
+        assertThat(errorListener.getCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void requestingDefaultConfiguration_returnsDefaultConfiguration() {
+        // Requesting a default configuration will throw if CameraX is not initialized.
+        FakeUseCaseConfiguration config =
+                CameraX.getDefaultUseCaseConfiguration(FakeUseCaseConfiguration.class);
+        assertThat(config).isNotNull();
+        assertThat(config.getTargetClass(null)).isEqualTo(FakeUseCase.class);
+    }
+
+    @Test
+    public void attachCameraControl_afterBindToLifecycle() {
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+        AttachCameraFakeCase fakeUseCase = new AttachCameraFakeCase(configuration0);
+
+        CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+        assertThat(fakeUseCase.getCameraControl(cameraId)).isEqualTo(camera.getCameraControl());
+    }
+
+    @Test
+    public void onCameraControlReadyIsCalled_afterBindToLifecycle() {
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+        AttachCameraFakeCase fakeUseCase = spy(new AttachCameraFakeCase(configuration0));
+
+        CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+        Mockito.verify(fakeUseCase).onCameraControlReady(cameraId);
+    }
+
+    @Test
+    public void detachCameraControl_afterUnbind() {
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+        AttachCameraFakeCase fakeUseCase = new AttachCameraFakeCase(configuration0);
+        CameraX.bindToLifecycle(lifecycle, fakeUseCase);
+
+        CameraX.unbind(fakeUseCase);
+
+        // after unbind, Camera's CameraControl should be detached from Usecase
+        assertThat(fakeUseCase.getCameraControl(cameraId)).isNotEqualTo(camera.getCameraControl());
+        // UseCase still gets a non-null default CameraControl that does nothing.
+        assertThat(fakeUseCase.getCameraControl(cameraId)).isNotNull();
+    }
+
+    @Test
+    public void canRetrieveCameraInfo() throws CameraInfoUnavailableException {
+        String cameraId = CameraX.getCameraWithLensFacing(LensFacing.BACK);
+        CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+        assertThat(cameraInfo).isNotNull();
+    }
+
+    private static class CountingErrorListener implements ErrorListener {
+        CountDownLatch latch;
+        AtomicInteger count = new AtomicInteger(0);
+
+        CountingErrorListener(CountDownLatch latch) {
+            this.latch = latch;
+        }
+
+        @Override
+        public void onError(ErrorCode errorCode, String message) {
+            count.getAndIncrement();
+            latch.countDown();
+        }
+
+        public int getCount() {
+            return count.get();
+        }
+    }
+
+    /** FakeUseCase that will call attachToCamera */
+    public static class AttachCameraFakeCase extends FakeUseCase {
+
+        public AttachCameraFakeCase(FakeUseCaseConfiguration configuration) {
+            super(configuration);
+        }
+
+        @Override
+        protected Map<String, Size> onSuggestedResolutionUpdated(
+                Map<String, Size> suggestedResolutionMap) {
+
+            SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+            CameraDeviceConfiguration configuration =
+                    (CameraDeviceConfiguration) getUseCaseConfiguration();
+            String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+            attachToCamera(cameraId, builder.build());
+            return suggestedResolutionMap;
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationAndroidTest.java
new file mode 100644
index 0000000..f883966
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationAndroidTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class CaptureRequestConfigurationAndroidTest {
+    private DeferrableSurface mockSurface0;
+
+    @Before
+    public void setup() {
+        mockSurface0 = Mockito.mock(DeferrableSurface.class);
+    }
+
+    @Test
+    public void buildCaptureRequestWithNullCameraDevice() throws CameraAccessException {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+        CameraDevice cameraDevice = null;
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        CaptureRequest.Builder captureRequestBuilder =
+                captureRequestConfiguration.buildCaptureRequest(cameraDevice);
+
+        assertThat(captureRequestBuilder).isNull();
+    }
+
+    @Test
+    public void builderSetTemplate() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        assertThat(captureRequestConfiguration.getTemplateType())
+                .isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+    }
+
+    @Test
+    public void builderAddSurface() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.addSurface(mockSurface0);
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        List<DeferrableSurface> surfaces = captureRequestConfiguration.getSurfaces();
+
+        assertThat(surfaces).hasSize(1);
+        assertThat(surfaces).contains(mockSurface0);
+    }
+
+    @Test
+    public void builderRemoveSurface() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.addSurface(mockSurface0);
+        builder.removeSurface(mockSurface0);
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        List<Surface> surfaces =
+                DeferrableSurfaces.surfaceList(captureRequestConfiguration.getSurfaces());
+        assertThat(surfaces).isEmpty();
+    }
+
+    @Test
+    public void builderClearSurface() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.addSurface(mockSurface0);
+        builder.clearSurfaces();
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        List<Surface> surfaces =
+                DeferrableSurfaces.surfaceList(captureRequestConfiguration.getSurfaces());
+        assertThat(surfaces.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void builderAddCharacteristic() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        Map<Key<?>, CaptureRequestParameter<?>> parameterMap =
+                captureRequestConfiguration.getCameraCharacteristics();
+
+        assertThat(parameterMap.containsKey(CaptureRequest.CONTROL_AF_MODE)).isTrue();
+        assertThat(parameterMap)
+                .containsEntry(
+                        CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequestParameter.create(
+                                CaptureRequest.CONTROL_AF_MODE,
+                                CaptureRequest.CONTROL_AF_MODE_AUTO));
+    }
+
+    @Test
+    public void builderSetUseTargetedSurface() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.setUseRepeatingSurface(true);
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        assertThat(captureRequestConfiguration.isUseRepeatingSurface()).isTrue();
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestParameterAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestParameterAndroidTest.java
new file mode 100644
index 0000000..da520917
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestParameterAndroidTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class CaptureRequestParameterAndroidTest {
+    private CameraDevice cameraDevice;
+
+    @Before
+    public void setup() throws CameraAccessException, InterruptedException {
+        cameraDevice = CameraUtil.getCameraDevice();
+    }
+
+    @After
+    public void teardown() {
+        CameraUtil.releaseCameraDevice(cameraDevice);
+    }
+
+    @Test
+    public void instanceCreation() {
+        CaptureRequestParameter<?> captureRequestParameter =
+                CaptureRequestParameter.create(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        assertThat(captureRequestParameter.getKey()).isEqualTo(CaptureRequest.CONTROL_AF_MODE);
+        assertThat(captureRequestParameter.getValue())
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+    }
+
+    @Test
+    public void applyParameter() throws CameraAccessException {
+        CaptureRequest.Builder builder =
+                cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+
+        assertThat(builder).isNotNull();
+
+        CaptureRequestParameter<?> captureRequestParameter =
+                CaptureRequestParameter.create(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        captureRequestParameter.apply(builder);
+
+        assertThat(builder.get(CaptureRequest.CONTROL_AF_MODE))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerAndroidTest.java
new file mode 100644
index 0000000..d750b5c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerAndroidTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.camera.core.CameraX.ErrorCode;
+import androidx.camera.core.CameraX.ErrorListener;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(AndroidJUnit4.class)
+public class ErrorHandlerAndroidTest {
+    private ErrorHandler errorHandler;
+    private CountingErrorListener errorListener0;
+    private CountingErrorListener errorListener1;
+    private HandlerThread handlerThread;
+    private Handler handler;
+    private CountDownLatch latch;
+
+    @Before
+    public void setup() {
+        errorHandler = new ErrorHandler();
+        latch = new CountDownLatch(1);
+        errorListener0 = new CountingErrorListener(latch);
+        errorListener1 = new CountingErrorListener(latch);
+
+        handlerThread = new HandlerThread("ErrorHandlerThread");
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+    }
+
+    @Test
+    public void errorListenerCalled_whenSet() throws InterruptedException {
+        errorHandler.setErrorListener(errorListener0, handler);
+
+        errorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+        latch.await(1, TimeUnit.SECONDS);
+
+        assertThat(errorListener0.getCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void errorListenerRemoved_whenNullSet() throws InterruptedException {
+        errorHandler.setErrorListener(errorListener0, handler);
+        errorHandler.setErrorListener(null, handler);
+
+        errorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+        assertThat(latch.await(1, TimeUnit.SECONDS)).isFalse();
+    }
+
+    @Test
+    public void errorListenerReplaced() throws InterruptedException {
+        errorHandler.setErrorListener(errorListener0, handler);
+        errorHandler.setErrorListener(errorListener1, handler);
+
+        errorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+        latch.await(1, TimeUnit.SECONDS);
+
+        assertThat(errorListener0.getCount()).isEqualTo(0);
+        assertThat(errorListener1.getCount()).isEqualTo(1);
+    }
+
+    private static class CountingErrorListener implements ErrorListener {
+        CountDownLatch latch;
+        AtomicInteger count = new AtomicInteger(0);
+
+        CountingErrorListener(CountDownLatch latch) {
+            this.latch = latch;
+        }
+
+        @Override
+        public void onError(ErrorCode errorCode, String message) {
+            count.getAndIncrement();
+            latch.countDown();
+        }
+
+        public int getCount() {
+            return count.get();
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeActivity.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeActivity.java
new file mode 100644
index 0000000..1293807
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/** A fake {@link Activity} that checks properties of the CameraX library. */
+public class FakeActivity extends Activity {
+    private volatile boolean isCameraXInitializedAtOnCreate = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstance) {
+        super.onCreate(savedInstance);
+        isCameraXInitializedAtOnCreate = CameraX.isInitialized();
+    }
+
+    /** Returns true if CameraX is initialized when {@link #onCreate(Bundle)} is called. */
+    public boolean isCameraXInitializedAtOnCreate() {
+        return isCameraXInitializedAtOnCreate;
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
new file mode 100644
index 0000000..e6b0ed5
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Size;
+
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Map;
+
+/**
+ * A second fake {@link BaseUseCase}.
+ *
+ * <p>This is used to complement the {@link FakeUseCase} for testing instances where a use case of
+ * different type is created.
+ */
+class FakeOtherUseCase extends BaseUseCase {
+    private volatile boolean isCleared = false;
+
+    /** Creates a new instance of a {@link FakeOtherUseCase} with a given configuration. */
+    FakeOtherUseCase(FakeOtherUseCaseConfiguration configuration) {
+        super(configuration);
+    }
+
+    /** Creates a new instance of a {@link FakeOtherUseCase} with a default configuration. */
+    FakeOtherUseCase() {
+        this(new FakeOtherUseCaseConfiguration.Builder().build());
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        isCleared = true;
+    }
+
+    @Override
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        return new FakeOtherUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK);
+    }
+
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        return suggestedResolutionMap;
+    }
+
+    /** Returns true if {@link #clear()} has been called previously. */
+    public boolean isCleared() {
+        return isCleared;
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfiguration.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfiguration.java
new file mode 100644
index 0000000..acb0044
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfiguration.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/** A fake configuration for {@link FakeOtherUseCase}. */
+public class FakeOtherUseCaseConfiguration
+        implements UseCaseConfiguration<FakeOtherUseCase>, CameraDeviceConfiguration {
+
+    private final Configuration config;
+
+    private FakeOtherUseCaseConfiguration(Configuration config) {
+        this.config = config;
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** Builder for an empty Configuration */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<
+            FakeOtherUseCase, FakeOtherUseCaseConfiguration, Builder>,
+            CameraDeviceConfiguration.Builder<FakeOtherUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle optionsBundle;
+
+        public Builder() {
+            optionsBundle = MutableOptionsBundle.create();
+            setTargetClass(FakeOtherUseCase.class);
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return optionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public FakeOtherUseCaseConfiguration build() {
+            return new FakeOtherUseCaseConfiguration(OptionsBundle.from(optionsBundle));
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCase.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCase.java
new file mode 100644
index 0000000..4be3a63
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCase.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Map;
+
+/** A fake {@link BaseUseCase}. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class FakeUseCase extends BaseUseCase {
+    private volatile boolean isCleared = false;
+
+    /** Creates a new instance of a {@link FakeUseCase} with a given configuration. */
+    protected FakeUseCase(FakeUseCaseConfiguration configuration) {
+        super(configuration);
+    }
+
+    /** Creates a new instance of a {@link FakeUseCase} with a default configuration. */
+    protected FakeUseCase() {
+        this(new FakeUseCaseConfiguration.Builder().build());
+    }
+
+    @Override
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        return new FakeUseCaseConfiguration.Builder()
+                .setLensFacing(LensFacing.BACK)
+                .setOptionUnpacker((useCaseConfig, sessionConfigBuilder) -> {
+                });
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        isCleared = true;
+    }
+
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        return suggestedResolutionMap;
+    }
+
+    /** Returns true if {@link #clear()} has been called previously. */
+    public boolean isCleared() {
+        return isCleared;
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCaseConfiguration.java b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCaseConfiguration.java
new file mode 100644
index 0000000..270a194
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeUseCaseConfiguration.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/** A fake configuration for {@link FakeUseCase}. */
+public class FakeUseCaseConfiguration
+        implements UseCaseConfiguration<FakeUseCase>, CameraDeviceConfiguration {
+
+    private final Configuration config;
+
+    private FakeUseCaseConfiguration(Configuration config) {
+        this.config = config;
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** Builder for an empty Configuration */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<FakeUseCase, FakeUseCaseConfiguration, Builder>,
+            CameraDeviceConfiguration.Builder<FakeUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle optionsBundle;
+
+        public Builder() {
+            optionsBundle = MutableOptionsBundle.create();
+            setTargetClass(FakeUseCase.class);
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return optionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public FakeUseCaseConfiguration build() {
+            return new FakeUseCaseConfiguration(OptionsBundle.from(optionsBundle));
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyAndroidTest.java
new file mode 100644
index 0000000..0dca471
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyAndroidTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+public final class ForwardingImageProxyAndroidTest {
+
+    private final ImageProxy baseImageProxy = mock(ImageProxy.class);
+    private final ImageProxy.PlaneProxy yPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy uPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy vPlane = mock(ImageProxy.PlaneProxy.class);
+    private ForwardingImageProxy imageProxy;
+
+    @Before
+    public void setUp() {
+        imageProxy = new ConcreteImageProxy(baseImageProxy);
+    }
+
+    @Test
+    public void close_closesWrappedImage() {
+        imageProxy.close();
+
+        verify(baseImageProxy).close();
+    }
+
+    @Test(timeout = 2000)
+    public void close_notifiesOnImageCloseListener_afterSetOnImageCloseListener()
+            throws InterruptedException {
+        Semaphore closedImageSemaphore = new Semaphore(/*permits=*/ 0);
+        AtomicReference<ImageProxy> closedImage = new AtomicReference<>();
+        imageProxy.addOnImageCloseListener(
+                image -> {
+                    closedImage.set(image);
+                    closedImageSemaphore.release();
+                });
+
+        imageProxy.close();
+
+        closedImageSemaphore.acquire();
+        assertThat(closedImage.get()).isSameAs(imageProxy);
+    }
+
+    @Test
+    public void getCropRect_returnsCropRectForWrappedImage() {
+        when(baseImageProxy.getCropRect()).thenReturn(new Rect(0, 0, 20, 20));
+
+        assertThat(imageProxy.getCropRect()).isEqualTo(new Rect(0, 0, 20, 20));
+    }
+
+    @Test
+    public void setCropRect_setsCropRectForWrappedImage() {
+        imageProxy.setCropRect(new Rect(0, 0, 40, 40));
+
+        verify(baseImageProxy).setCropRect(new Rect(0, 0, 40, 40));
+    }
+
+    @Test
+    public void getFormat_returnsFormatForWrappedImage() {
+        when(baseImageProxy.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+        assertThat(imageProxy.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
+    }
+
+    @Test
+    public void getHeight_returnsHeightForWrappedImage() {
+        when(baseImageProxy.getHeight()).thenReturn(480);
+
+        assertThat(imageProxy.getHeight()).isEqualTo(480);
+    }
+
+    @Test
+    public void getWidth_returnsWidthForWrappedImage() {
+        when(baseImageProxy.getWidth()).thenReturn(640);
+
+        assertThat(imageProxy.getWidth()).isEqualTo(640);
+    }
+
+    @Test
+    public void getTimestamp_returnsTimestampForWrappedImage() {
+        when(baseImageProxy.getTimestamp()).thenReturn(138990020L);
+
+        assertThat(imageProxy.getTimestamp()).isEqualTo(138990020L);
+    }
+
+    @Test
+    public void setTimestamp_setsTimestampForWrappedImage() {
+        imageProxy.setTimestamp(138990020L);
+
+        verify(baseImageProxy).setTimestamp(138990020L);
+    }
+
+    @Test
+    public void getPlanes_returnsPlanesForWrappedImage() {
+        when(baseImageProxy.getPlanes())
+                .thenReturn(new ImageProxy.PlaneProxy[]{yPlane, uPlane, vPlane});
+
+        ImageProxy.PlaneProxy[] planes = imageProxy.getPlanes();
+        assertThat(planes.length).isEqualTo(3);
+        assertThat(planes[0]).isEqualTo(yPlane);
+        assertThat(planes[1]).isEqualTo(uPlane);
+        assertThat(planes[2]).isEqualTo(vPlane);
+    }
+
+    private static final class ConcreteImageProxy extends ForwardingImageProxy {
+        private ConcreteImageProxy(ImageProxy baseImageProxy) {
+            super(baseImageProxy);
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerAndroidTest.java
new file mode 100644
index 0000000..6c098aa
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerAndroidTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class ForwardingImageReaderListenerAndroidTest {
+    private static final int IMAGE_WIDTH = 640;
+    private static final int IMAGE_HEIGHT = 480;
+    private static final int IMAGE_FORMAT = ImageFormat.YUV_420_888;
+    private static final int MAX_IMAGES = 10;
+
+    private final ImageReader imageReader = mock(ImageReader.class);
+    private final Surface surface = mock(Surface.class);
+    private HandlerThread handlerThread;
+    private Handler handler;
+    private List<QueuedImageReaderProxy> imageReaderProxys;
+    private ForwardingImageReaderListener forwardingListener;
+
+    private static Image createMockImage() {
+        Image image = mock(Image.class);
+        when(image.getWidth()).thenReturn(IMAGE_WIDTH);
+        when(image.getHeight()).thenReturn(IMAGE_HEIGHT);
+        when(image.getFormat()).thenReturn(IMAGE_FORMAT);
+        return image;
+    }
+
+    private static ImageReaderProxy.OnImageAvailableListener createMockListener() {
+        return mock(ImageReaderProxy.OnImageAvailableListener.class);
+    }
+
+    /**
+     * Returns a listener which immediately acquires the next image, closes the image, and releases
+     * a semaphore.
+     */
+    private static ImageReaderProxy.OnImageAvailableListener
+    createSemaphoreReleasingClosingListener(Semaphore semaphore) {
+        return imageReaderProxy -> {
+            imageReaderProxy.acquireNextImage().close();
+            semaphore.release();
+        };
+    }
+
+    @Before
+    public void setUp() {
+        handlerThread = new HandlerThread("listener");
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+        imageReaderProxys = new ArrayList<>(3);
+        for (int i = 0; i < 3; ++i) {
+            imageReaderProxys.add(
+                    new QueuedImageReaderProxy(
+                            IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_FORMAT, MAX_IMAGES, surface));
+        }
+        forwardingListener = new ForwardingImageReaderListener(imageReaderProxys);
+    }
+
+    @After
+    public void tearDown() {
+        handlerThread.quitSafely();
+    }
+
+    @Test
+    public void newImageIsForwardedToAllListeners() {
+        Image baseImage = createMockImage();
+        when(imageReader.acquireNextImage()).thenReturn(baseImage);
+        List<ImageReaderProxy.OnImageAvailableListener> listeners = new ArrayList<>();
+        for (ImageReaderProxy imageReaderProxy : imageReaderProxys) {
+            ImageReaderProxy.OnImageAvailableListener listener = createMockListener();
+            imageReaderProxy.setOnImageAvailableListener(listener, handler);
+            listeners.add(listener);
+        }
+
+        final int availableImages = 5;
+        for (int i = 0; i < availableImages; ++i) {
+            forwardingListener.onImageAvailable(imageReader);
+        }
+
+        for (int i = 0; i < imageReaderProxys.size(); ++i) {
+            // Listener should be notified about every available image.
+            verify(listeners.get(i), timeout(2000).times(availableImages))
+                    .onImageAvailable(imageReaderProxys.get(i));
+        }
+    }
+
+    @Test(timeout = 2000)
+    public void baseImageIsClosed_allQueuesAreCleared_whenAllForwardedCopiesAreClosed()
+            throws InterruptedException {
+        Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+        Image baseImage = createMockImage();
+        when(imageReader.acquireNextImage()).thenReturn(baseImage);
+        for (ImageReaderProxy imageReaderProxy : imageReaderProxys) {
+            // Close the image for every listener.
+            imageReaderProxy.setOnImageAvailableListener(
+                    createSemaphoreReleasingClosingListener(onCloseSemaphore), handler);
+        }
+
+        final int availableImages = 5;
+        for (int i = 0; i < availableImages; ++i) {
+            forwardingListener.onImageAvailable(imageReader);
+        }
+        onCloseSemaphore.acquire(availableImages * imageReaderProxys.size());
+
+        // Base image should be closed every time.
+        verify(baseImage, times(availableImages)).close();
+        // All queues should be cleared.
+        for (QueuedImageReaderProxy imageReaderProxy : imageReaderProxys) {
+            assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(0);
+        }
+    }
+
+    @Test(timeout = 2000)
+    public void baseImageIsNotClosed_someQueuesAreCleared_whenNotAllForwardedCopiesAreClosed()
+            throws InterruptedException {
+        Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+        Image baseImage = createMockImage();
+        when(imageReader.acquireNextImage()).thenReturn(baseImage);
+        // Don't close the image for the first listener.
+        imageReaderProxys.get(0).setOnImageAvailableListener(createMockListener(), handler);
+        // Close the image for the other listeners.
+        imageReaderProxys
+                .get(1)
+                .setOnImageAvailableListener(
+                        createSemaphoreReleasingClosingListener(onCloseSemaphore), handler);
+        imageReaderProxys
+                .get(2)
+                .setOnImageAvailableListener(
+                        createSemaphoreReleasingClosingListener(onCloseSemaphore), handler);
+
+        final int availableImages = 5;
+        for (int i = 0; i < availableImages; ++i) {
+            forwardingListener.onImageAvailable(imageReader);
+        }
+        onCloseSemaphore.acquire(availableImages * (imageReaderProxys.size() - 1));
+
+        // Base image should not be closed every time.
+        verify(baseImage, never()).close();
+        // First reader's queue should not be cleared.
+        assertThat(imageReaderProxys.get(0).getCurrentImages()).isEqualTo(availableImages);
+        // Other readers' queues should be cleared.
+        assertThat(imageReaderProxys.get(1).getCurrentImages()).isEqualTo(0);
+        assertThat(imageReaderProxys.get(2).getCurrentImages()).isEqualTo(0);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerAndroidTest.java
new file mode 100644
index 0000000..87abf93
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerAndroidTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImageProxyDownsamplerAndroidTest {
+    private static final int WIDTH = 8;
+    private static final int HEIGHT = 8;
+
+    private static ImageProxy createYuv420Image(int uvPixelStride) {
+        ImageProxy image = mock(ImageProxy.class);
+        ImageProxy.PlaneProxy[] planes = new ImageProxy.PlaneProxy[3];
+
+        when(image.getWidth()).thenReturn(WIDTH);
+        when(image.getHeight()).thenReturn(HEIGHT);
+        when(image.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+        when(image.getPlanes()).thenReturn(planes);
+
+        planes[0] =
+                createPlaneWithRampPattern(WIDTH, HEIGHT, /*pixelStride=*/ 1, /*initialValue=*/ 0);
+        planes[1] =
+                createPlaneWithRampPattern(
+                        WIDTH / 2, HEIGHT / 2, uvPixelStride, /*initialValue=*/ 1);
+        planes[2] =
+                createPlaneWithRampPattern(
+                        WIDTH / 2, HEIGHT / 2, uvPixelStride, /*initialValue=*/ 2);
+
+        return image;
+    }
+
+    private static ImageProxy.PlaneProxy createPlaneWithRampPattern(
+            int width, int height, int pixelStride, int initialValue) {
+        return new ImageProxy.PlaneProxy() {
+            final ByteBuffer buffer =
+                    createBufferWithRampPattern(width, height, pixelStride, initialValue);
+
+            @Override
+            public int getRowStride() {
+                return width * pixelStride;
+            }
+
+            @Override
+            public int getPixelStride() {
+                return pixelStride;
+            }
+
+            @Override
+            public ByteBuffer getBuffer() {
+                return buffer;
+            }
+        };
+    }
+
+    private static ByteBuffer createBufferWithRampPattern(
+            int width, int height, int pixelStride, int initialValue) {
+        int rowStride = width * pixelStride;
+        ByteBuffer buffer = ByteBuffer.allocateDirect(rowStride * height);
+        int value = initialValue;
+        for (int y = 0; y < height; ++y) {
+            for (int x = 0; x < width; ++x) {
+                buffer.position(y * rowStride + x * pixelStride);
+                buffer.put((byte) (value++ & 0xFF));
+            }
+        }
+        return buffer;
+    }
+
+    private static void checkOutputIsNearestNeighborDownsampledInput(
+            ImageProxy inputImage, ImageProxy outputImage, int downsamplingFactor) {
+        ImageProxy.PlaneProxy[] inputPlanes = inputImage.getPlanes();
+        ImageProxy.PlaneProxy[] outputPlanes = outputImage.getPlanes();
+        for (int c = 0; c < 3; ++c) {
+            ByteBuffer inputBuffer = inputPlanes[c].getBuffer();
+            ByteBuffer outputBuffer = outputPlanes[c].getBuffer();
+            inputBuffer.rewind();
+            outputBuffer.rewind();
+            int divisor = (c == 0) ? 1 : 2;
+            int inputRowStride = inputPlanes[c].getRowStride();
+            int inputPixelStride = inputPlanes[c].getPixelStride();
+            int outputRowStride = outputPlanes[c].getRowStride();
+            int outputPixelStride = outputPlanes[c].getPixelStride();
+            for (int y = 0; y < outputImage.getHeight() / divisor; ++y) {
+                for (int x = 0; x < outputImage.getWidth() / divisor; ++x) {
+                    byte inputPixel =
+                            inputBuffer.get(
+                                    y * downsamplingFactor * inputRowStride
+                                            + x * downsamplingFactor * inputPixelStride);
+                    byte outputPixel =
+                            outputBuffer.get(y * outputRowStride + x * outputPixelStride);
+                    assertThat(outputPixel).isEqualTo(inputPixel);
+                }
+            }
+        }
+    }
+
+    private static void checkOutputIsAveragingDownsampledInput(
+            ImageProxy inputImage, ImageProxy outputImage, int downsamplingFactor) {
+        ImageProxy.PlaneProxy[] inputPlanes = inputImage.getPlanes();
+        ImageProxy.PlaneProxy[] outputPlanes = outputImage.getPlanes();
+        for (int c = 0; c < 3; ++c) {
+            ByteBuffer inputBuffer = inputPlanes[c].getBuffer();
+            ByteBuffer outputBuffer = outputPlanes[c].getBuffer();
+            inputBuffer.rewind();
+            outputBuffer.rewind();
+            int divisor = (c == 0) ? 1 : 2;
+            int inputRowStride = inputPlanes[c].getRowStride();
+            int inputPixelStride = inputPlanes[c].getPixelStride();
+            int outputRowStride = outputPlanes[c].getRowStride();
+            int outputPixelStride = outputPlanes[c].getPixelStride();
+            for (int y = 0; y < outputImage.getHeight() / divisor; ++y) {
+                for (int x = 0; x < outputImage.getWidth() / divisor; ++x) {
+                    byte inputPixelA =
+                            inputBuffer.get(
+                                    y * downsamplingFactor * inputRowStride
+                                            + x * downsamplingFactor * inputPixelStride);
+                    byte inputPixelB =
+                            inputBuffer.get(
+                                    y * downsamplingFactor * inputRowStride
+                                            + (x * downsamplingFactor + 1) * inputPixelStride);
+                    byte inputPixelC =
+                            inputBuffer.get(
+                                    (y * downsamplingFactor + 1) * inputRowStride
+                                            + x * downsamplingFactor * inputPixelStride);
+                    byte inputPixelD =
+                            inputBuffer.get(
+                                    (y * downsamplingFactor + 1) * inputRowStride
+                                            + (x * downsamplingFactor + 1) * inputPixelStride);
+                    byte averaged =
+                            (byte)
+                                    ((((inputPixelA & 0xFF)
+                                            + (inputPixelB & 0xFF)
+                                            + (inputPixelC & 0xFF)
+                                            + (inputPixelD & 0xFF))
+                                            / 4)
+                                            & 0xFF);
+                    byte outputPixel =
+                            outputBuffer.get(y * outputRowStride + x * outputPixelStride);
+                    assertThat(outputPixel).isEqualTo(averaged);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void nearestNeighborDownsamplingBy2X_whenUVPlanesHavePixelStride1() {
+        ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 1);
+        int downsamplingFactor = 2;
+        ImageProxy outputImage =
+                ImageProxyDownsampler.downsample(
+                        inputImage,
+                        WIDTH / downsamplingFactor,
+                        HEIGHT / downsamplingFactor,
+                        ImageProxyDownsampler.DownsamplingMethod.NEAREST_NEIGHBOR);
+
+        checkOutputIsNearestNeighborDownsampledInput(inputImage, outputImage, downsamplingFactor);
+    }
+
+    @Test
+    public void nearestNeighborDownsamplingBy2X_whenUVPlanesHavePixelStride2() {
+        ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 2);
+        int downsamplingFactor = 2;
+        ImageProxy outputImage =
+                ImageProxyDownsampler.downsample(
+                        inputImage,
+                        WIDTH / downsamplingFactor,
+                        HEIGHT / downsamplingFactor,
+                        ImageProxyDownsampler.DownsamplingMethod.NEAREST_NEIGHBOR);
+
+        checkOutputIsNearestNeighborDownsampledInput(inputImage, outputImage, downsamplingFactor);
+    }
+
+    @Test
+    public void averagingDownsamplingBy2X_whenUVPlanesHavePixelStride1() {
+        ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 1);
+        int downsamplingFactor = 2;
+        ImageProxy outputImage =
+                ImageProxyDownsampler.downsample(
+                        inputImage,
+                        WIDTH / downsamplingFactor,
+                        HEIGHT / downsamplingFactor,
+                        ImageProxyDownsampler.DownsamplingMethod.AVERAGING);
+
+        checkOutputIsAveragingDownsampledInput(inputImage, outputImage, downsamplingFactor);
+    }
+
+    @Test
+    public void averagingDownsamplingBy2X_whenUVPlanesHavePixelStride2() {
+        ImageProxy inputImage = createYuv420Image(/*uvPixelStride=*/ 2);
+        int downsamplingFactor = 2;
+        ImageProxy outputImage =
+                ImageProxyDownsampler.downsample(
+                        inputImage,
+                        WIDTH / downsamplingFactor,
+                        HEIGHT / downsamplingFactor,
+                        ImageProxyDownsampler.DownsamplingMethod.AVERAGING);
+
+        checkOutputIsAveragingDownsampledInput(inputImage, outputImage, downsamplingFactor);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverAndroidTest.java
new file mode 100644
index 0000000..85a7ba8
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverAndroidTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Base64;
+import android.util.Rational;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageSaver.OnImageSavedListener;
+import androidx.camera.core.ImageSaver.SaveError;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public class ImageSaverAndroidTest {
+
+    private static final int WIDTH = 160;
+    private static final int HEIGHT = 120;
+    private static final int Y_PIXEL_STRIDE = 1;
+    private static final int Y_ROW_STRIDE = WIDTH;
+    private static final int UV_PIXEL_STRIDE = 1;
+    private static final int UV_ROW_STRIDE = WIDTH / 2;
+
+    // The image used here has a YUV_420_888 format.
+    @Mock
+    private final ImageProxy mockYuvImage = mock(ImageProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy yPlane = mock(ImageProxy.PlaneProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy uPlane = mock(ImageProxy.PlaneProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy vPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ByteBuffer yBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
+    private final ByteBuffer uBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+    private final ByteBuffer vBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+
+    @Mock
+    private final ImageProxy mockJpegImage = mock(ImageProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy jpegDataPlane = mock(ImageProxy.PlaneProxy.class);
+    private final String jpegImageDataBase64 =
+            "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
+                    + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEB"
+                    + "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAB4AKADASIA"
+                    + "AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA"
+                    + "AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3"
+                    + "ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm"
+                    + "p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA"
+                    + "AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx"
+                    + "BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK"
+                    + "U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3"
+                    + "uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD/AD/6"
+                    + "KKK/8/8AP/P/AAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii"
+                    + "gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA"
+                    + "CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK"
+                    + "KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAoo"
+                    + "ooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiii"
+                    + "gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9k=";
+    private final ByteBuffer jpegDataBuffer =
+            ByteBuffer.wrap(Base64.decode(jpegImageDataBase64, Base64.DEFAULT));
+
+    private final Semaphore semaphore = new Semaphore(0);
+    private final ImageSaver.OnImageSavedListener mockListener =
+            mock(ImageSaver.OnImageSavedListener.class);
+    private final ImageSaver.OnImageSavedListener syncListener =
+            new OnImageSavedListener() {
+                @Override
+                public void onImageSaved(File file) {
+                    mockListener.onImageSaved(file);
+                    semaphore.release();
+                }
+
+                @Override
+                public void onError(
+                        SaveError saveError, String message, @Nullable Throwable cause) {
+                    mockListener.onError(saveError, message, cause);
+                    semaphore.release();
+                }
+            };
+
+    private HandlerThread backgroundThread;
+    private Handler backgroundHandler;
+
+    @Before
+    public void setup() {
+        // The YUV image's behavior.
+        when(mockYuvImage.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+        when(mockYuvImage.getWidth()).thenReturn(WIDTH);
+        when(mockYuvImage.getHeight()).thenReturn(HEIGHT);
+
+        when(yPlane.getBuffer()).thenReturn(yBuffer);
+        when(yPlane.getPixelStride()).thenReturn(Y_PIXEL_STRIDE);
+        when(yPlane.getRowStride()).thenReturn(Y_ROW_STRIDE);
+
+        when(uPlane.getBuffer()).thenReturn(uBuffer);
+        when(uPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
+        when(uPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
+
+        when(vPlane.getBuffer()).thenReturn(vBuffer);
+        when(vPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
+        when(vPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
+        when(mockYuvImage.getPlanes())
+                .thenReturn(new ImageProxy.PlaneProxy[]{yPlane, uPlane, vPlane});
+
+        // The JPEG image's behavior
+        when(mockJpegImage.getFormat()).thenReturn(ImageFormat.JPEG);
+        when(mockJpegImage.getWidth()).thenReturn(WIDTH);
+        when(mockJpegImage.getHeight()).thenReturn(HEIGHT);
+
+        when(jpegDataPlane.getBuffer()).thenReturn(jpegDataBuffer);
+        when(mockJpegImage.getPlanes()).thenReturn(new ImageProxy.PlaneProxy[]{jpegDataPlane});
+
+        // Set up a background thread/handler for callbacks
+        backgroundThread = new HandlerThread("CallbackThread");
+        backgroundThread.start();
+        backgroundHandler = new Handler(backgroundThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        backgroundThread.quitSafely();
+    }
+
+    private ImageSaver getDefaultImageSaver(ImageProxy image, File file) {
+        return new ImageSaver(
+                image,
+                file,
+                /*orientation=*/ 0,
+                /*reversedHorizontal=*/ false,
+                /*reversedVertical=*/ false,
+                /*location=*/ null,
+                /*cropAspectRatio=*/ null,
+                syncListener,
+                backgroundHandler);
+    }
+
+    @Test
+    public void canSaveYuvImage() throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        ImageSaver imageSaver = getDefaultImageSaver(mockYuvImage, saveLocation);
+
+        imageSaver.run();
+
+        semaphore.acquire();
+
+        verify(mockListener).onImageSaved(anyObject());
+    }
+
+    @Test
+    public void canSaveJpegImage() throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        semaphore.acquire();
+
+        verify(mockListener).onImageSaved(anyObject());
+    }
+
+    @Test
+    public void errorCallbackWillBeCalledOnInvalidPath() throws InterruptedException, IOException {
+        // Invalid filename should cause error
+        File saveLocation = new File("/not/a/real/path.jpg");
+
+        ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        semaphore.acquire();
+
+        verify(mockListener).onError(eq(SaveError.FILE_IO_FAILED), anyString(), anyObject());
+    }
+
+    @Test
+    public void imageIsClosedOnSuccess() throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        semaphore.acquire();
+
+        verify(mockJpegImage).close();
+    }
+
+    @Test
+    public void imageIsClosedOnError() throws InterruptedException, IOException {
+        // Invalid filename should cause error
+        File saveLocation = new File("/not/a/real/path.jpg");
+
+        ImageSaver imageSaver = getDefaultImageSaver(mockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        semaphore.acquire();
+
+        verify(mockJpegImage).close();
+    }
+
+    private void imageCanBeCropped(ImageProxy image) throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        Rational viewRatio = new Rational(1, 1);
+
+        ImageSaver imageSaver =
+                new ImageSaver(
+                        image,
+                        saveLocation,
+                        /*orientation=*/ 0,
+                        /*reversedHorizontal=*/ false,
+                        /*reversedVertical=*/ false,
+                        /*location=*/ null,
+                        /*cropAspectRatio=*/ viewRatio,
+                        syncListener,
+                        backgroundHandler);
+        imageSaver.run();
+
+        semaphore.acquire();
+
+        Bitmap bitmap = BitmapFactory.decodeFile(saveLocation.getPath());
+        assertThat(bitmap.getWidth()).isEqualTo(bitmap.getHeight());
+    }
+
+    @Test
+    public void jpegImageCanBeCropped() throws InterruptedException, IOException {
+        imageCanBeCropped(mockJpegImage);
+    }
+
+    @Test
+    public void yuvImageCanBeCropped() throws InterruptedException, IOException {
+        imageCanBeCropped(mockYuvImage);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceAndroidTest.java
new file mode 100644
index 0000000..0ae4238
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceAndroidTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.concurrent.ExecutionException;
+
+@RunWith(AndroidJUnit4.class)
+public final class ImmediateSurfaceAndroidTest {
+    Surface mockSurface = Mockito.mock(Surface.class);
+
+    @Test
+    public void getSurface_returnsInstance() throws ExecutionException, InterruptedException {
+        ImmediateSurface immediateSurface = new ImmediateSurface(mockSurface);
+
+        ListenableFuture<Surface> surfaceListenableFuture = immediateSurface.getSurface();
+
+        assertThat(surfaceListenableFuture.get()).isSameAs(mockSurface);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorAndroidTest.java
new file mode 100644
index 0000000..5ed3e6d
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorAndroidTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.GuardedBy;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@RunWith(AndroidJUnit4.class)
+public final class IoExecutorAndroidTest {
+
+    private Executor ioExecutor;
+    private Lock lock = new ReentrantLock();
+    private Condition condition = lock.newCondition();
+    @GuardedBy("lock")
+    private RunnableState state = RunnableState.CLEAR;
+    private final Runnable runnable1 =
+            () -> {
+                lock.lock();
+                try {
+                    state = RunnableState.RUNNABLE1_WAITING;
+                    condition.signalAll();
+                    while (state != RunnableState.CLEAR) {
+                        condition.await();
+                    }
+
+                    state = RunnableState.RUNNABLE1_FINISHED;
+                    condition.signalAll();
+                } catch (InterruptedException e) {
+                    throw new RuntimeException("Thread interrupted unexpectedly", e);
+                } finally {
+                    lock.unlock();
+                }
+            };
+    private final Runnable runnable2 =
+            () -> {
+                lock.lock();
+                try {
+                    while (state != RunnableState.RUNNABLE1_WAITING) {
+                        condition.await();
+                    }
+
+                    state = RunnableState.RUNNABLE2_FINISHED;
+                    condition.signalAll();
+                } catch (InterruptedException e) {
+                    throw new RuntimeException("Thread interrupted unexpectedly", e);
+                } finally {
+                    lock.unlock();
+                }
+            };
+    private final Runnable simpleRunnable1 =
+            () -> {
+                lock.lock();
+                try {
+                    state = RunnableState.RUNNABLE1_FINISHED;
+                    condition.signalAll();
+                } finally {
+                    lock.unlock();
+                }
+            };
+
+    @Before
+    public void setup() {
+        lock.lock();
+        try {
+            state = RunnableState.CLEAR;
+        } finally {
+            lock.unlock();
+        }
+        ioExecutor = IoExecutor.getInstance();
+    }
+
+    @Test(timeout = 2000)
+    public void canRunRunnable() throws InterruptedException {
+        ioExecutor.execute(simpleRunnable1);
+        lock.lock();
+        try {
+            while (state != RunnableState.RUNNABLE1_FINISHED) {
+                condition.await();
+            }
+        } finally {
+            lock.unlock();
+        }
+
+        // No need to check anything here. Completing this method should signal success.
+    }
+
+    @Test(timeout = 2000)
+    public void canRunMultipleRunnableInParallel() throws InterruptedException {
+        ioExecutor.execute(runnable1);
+        ioExecutor.execute(runnable2);
+
+        lock.lock();
+        try {
+            // runnable2 cannot finish until runnable1 has started
+            while (state != RunnableState.RUNNABLE2_FINISHED) {
+                condition.await();
+            }
+
+            // Allow runnable1 to finish
+            state = RunnableState.CLEAR;
+            condition.signalAll();
+
+            while (state != RunnableState.RUNNABLE1_FINISHED) {
+                condition.await();
+            }
+        } finally {
+            lock.unlock();
+        }
+
+        // No need to check anything here. Completing this method should signal success.
+    }
+
+    private enum RunnableState {
+        CLEAR,
+        RUNNABLE1_WAITING,
+        RUNNABLE1_FINISHED,
+        RUNNABLE2_FINISHED
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyAndroidTest.java
new file mode 100644
index 0000000..8500148
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyAndroidTest.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+@RunWith(AndroidJUnit4.class)
+public final class QueuedImageReaderProxyAndroidTest {
+    private static final int IMAGE_WIDTH = 640;
+    private static final int IMAGE_HEIGHT = 480;
+    private static final int IMAGE_FORMAT = ImageFormat.YUV_420_888;
+    private static final int MAX_IMAGES = 10;
+
+    private final Surface surface = mock(Surface.class);
+    private HandlerThread handlerThread;
+    private Handler handler;
+    private QueuedImageReaderProxy imageReaderProxy;
+
+    private static ImageProxy createMockImageProxy() {
+        ImageProxy image = mock(ImageProxy.class);
+        when(image.getWidth()).thenReturn(IMAGE_WIDTH);
+        when(image.getHeight()).thenReturn(IMAGE_HEIGHT);
+        when(image.getFormat()).thenReturn(IMAGE_FORMAT);
+        return image;
+    }
+
+    private static ConcreteImageProxy createSemaphoreReleasingOnCloseImageProxy(
+            Semaphore semaphore) {
+        ConcreteImageProxy image = createForwardingImageProxy();
+        image.addOnImageCloseListener(
+                closedImage -> {
+                    semaphore.release();
+                });
+        return image;
+    }
+
+    private static ConcreteImageProxy createForwardingImageProxy() {
+        return new ConcreteImageProxy(createMockImageProxy());
+    }
+
+    @Before
+    public void setUp() {
+        handlerThread = new HandlerThread("background");
+        handlerThread.start();
+        handler = new Handler(handlerThread.getLooper());
+        imageReaderProxy =
+                new QueuedImageReaderProxy(
+                        IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_FORMAT, MAX_IMAGES, surface);
+    }
+
+    @After
+    public void tearDown() {
+        handlerThread.quitSafely();
+    }
+
+    @Test
+    public void enqueueImage_incrementsQueueSize() {
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+
+        assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(2);
+    }
+
+    @Test
+    public void enqueueImage_doesNotIncreaseSizeBeyondMaxImages() {
+        // Exceed the queue's capacity by 2.
+        for (int i = 0; i < MAX_IMAGES + 2; ++i) {
+            imageReaderProxy.enqueueImage(createForwardingImageProxy());
+        }
+
+        assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(MAX_IMAGES);
+    }
+
+    @Test
+    public void enqueueImage_closesImagesWhichAreNotEnqueued_doesNotCloseOtherImages() {
+        // Exceed the queue's capacity by 2.
+        List<ConcreteImageProxy> images = new ArrayList<>(MAX_IMAGES + 2);
+        for (int i = 0; i < MAX_IMAGES + 2; ++i) {
+            images.add(createForwardingImageProxy());
+            imageReaderProxy.enqueueImage(images.get(i));
+        }
+
+        // Last two images should not be enqueued and should be closed.
+        assertThat(images.get(MAX_IMAGES).isClosed()).isTrue();
+        assertThat(images.get(MAX_IMAGES + 1).isClosed()).isTrue();
+        // All other images should be enqueued and open.
+        for (int i = 0; i < MAX_IMAGES; ++i) {
+            assertThat(images.get(i).isClosed()).isFalse();
+        }
+    }
+
+    @Test(timeout = 2000)
+    public void closedImages_reduceQueueSize() throws InterruptedException {
+        // Fill up to the queue's capacity.
+        Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+        for (int i = 0; i < MAX_IMAGES; ++i) {
+            ForwardingImageProxy image =
+                    createSemaphoreReleasingOnCloseImageProxy(onCloseSemaphore);
+            imageReaderProxy.enqueueImage(image);
+        }
+
+        imageReaderProxy.acquireNextImage().close();
+        imageReaderProxy.acquireNextImage().close();
+        onCloseSemaphore.acquire(/*permits=*/ 2);
+
+        assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(MAX_IMAGES - 2);
+    }
+
+    @Test(timeout = 2000)
+    public void closedImage_allowsNewImageToBeEnqueued() throws InterruptedException {
+        // Fill up to the queue's capacity.
+        Semaphore onCloseSemaphore = new Semaphore(/*permits=*/ 0);
+        for (int i = 0; i < MAX_IMAGES; ++i) {
+            ForwardingImageProxy image =
+                    createSemaphoreReleasingOnCloseImageProxy(onCloseSemaphore);
+            imageReaderProxy.enqueueImage(image);
+        }
+
+        imageReaderProxy.acquireNextImage().close();
+        onCloseSemaphore.acquire();
+
+        ConcreteImageProxy lastImageProxy = createForwardingImageProxy();
+        imageReaderProxy.enqueueImage(lastImageProxy);
+
+        // Last image should be enqueued and open.
+        assertThat(lastImageProxy.isClosed()).isFalse();
+    }
+
+    @Test
+    public void enqueueImage_invokesListenerCallback() {
+        ImageReaderProxy.OnImageAvailableListener listener =
+                mock(ImageReaderProxy.OnImageAvailableListener.class);
+        imageReaderProxy.setOnImageAvailableListener(listener, handler);
+
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+
+        verify(listener, timeout(2000).times(2)).onImageAvailable(imageReaderProxy);
+    }
+
+    @Test
+    public void acquireLatestImage_returnsNull_whenQueueIsEmpty() {
+        assertThat(imageReaderProxy.acquireLatestImage()).isNull();
+    }
+
+    @Test
+    public void acquireLatestImage_returnsLastImage_reducesQueueSizeToOne() {
+        final int availableImages = 5;
+        List<ForwardingImageProxy> images = new ArrayList<>(availableImages);
+        for (int i = 0; i < availableImages; ++i) {
+            images.add(createForwardingImageProxy());
+            imageReaderProxy.enqueueImage(images.get(i));
+        }
+
+        ImageProxy lastImage = images.get(availableImages - 1);
+        assertThat(imageReaderProxy.acquireLatestImage()).isEqualTo(lastImage);
+        assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(1);
+    }
+
+    @Test
+    public void acquireLatestImage_throwsException_whenAllImagesWerePreviouslyAcquired() {
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+        imageReaderProxy.acquireNextImage();
+
+        assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireLatestImage());
+    }
+
+    @Test
+    public void acquireNextImage_returnsNull_whenQueueIsEmpty() {
+        assertThat(imageReaderProxy.acquireNextImage()).isNull();
+    }
+
+    @Test
+    public void acquireNextImage_returnsNextImage_doesNotChangeQueueSize() {
+        final int availableImages = 5;
+        List<ForwardingImageProxy> images = new ArrayList<>(availableImages);
+        for (int i = 0; i < availableImages; ++i) {
+            images.add(createForwardingImageProxy());
+            imageReaderProxy.enqueueImage(images.get(i));
+        }
+
+        for (int i = 0; i < availableImages; ++i) {
+            assertThat(imageReaderProxy.acquireNextImage()).isEqualTo(images.get(i));
+        }
+        assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(availableImages);
+    }
+
+    @Test
+    public void acquireNextImage_throwsException_whenAllImagesWerePreviouslyAcquired() {
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+        imageReaderProxy.acquireNextImage();
+
+        assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireNextImage());
+    }
+
+    @Test
+    public void close_closesAnyImagesStillInQueue() {
+        ConcreteImageProxy image0 = createForwardingImageProxy();
+        ConcreteImageProxy image1 = createForwardingImageProxy();
+        imageReaderProxy.enqueueImage(image0);
+        imageReaderProxy.enqueueImage(image1);
+
+        imageReaderProxy.close();
+
+        assertThat(image0.isClosed()).isTrue();
+        assertThat(image1.isClosed()).isTrue();
+    }
+
+    @Test
+    public void close_notifiesOnCloseListeners() {
+        QueuedImageReaderProxy.OnReaderCloseListener listenerA =
+                mock(QueuedImageReaderProxy.OnReaderCloseListener.class);
+        QueuedImageReaderProxy.OnReaderCloseListener listenerB =
+                mock(QueuedImageReaderProxy.OnReaderCloseListener.class);
+        imageReaderProxy.addOnReaderCloseListener(listenerA);
+        imageReaderProxy.addOnReaderCloseListener(listenerB);
+
+        imageReaderProxy.close();
+
+        verify(listenerA, times(1)).onReaderClose(imageReaderProxy);
+        verify(listenerB, times(1)).onReaderClose(imageReaderProxy);
+    }
+
+    @Test
+    public void acquireLatestImage_throwsException_afterReaderIsClosed() {
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+        imageReaderProxy.close();
+
+        assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireLatestImage());
+    }
+
+    @Test
+    public void acquireNextImage_throwsException_afterReaderIsClosed() {
+        imageReaderProxy.enqueueImage(createForwardingImageProxy());
+        imageReaderProxy.close();
+
+        assertThrows(IllegalStateException.class, () -> imageReaderProxy.acquireNextImage());
+    }
+
+    @Test
+    public void getHeight_returnsFixedHeight() {
+        assertThat(imageReaderProxy.getHeight()).isEqualTo(IMAGE_HEIGHT);
+    }
+
+    @Test
+    public void getWidth_returnsFixedWidth() {
+        assertThat(imageReaderProxy.getWidth()).isEqualTo(IMAGE_WIDTH);
+    }
+
+    @Test
+    public void getImageFormat_returnsFixedFormat() {
+        assertThat(imageReaderProxy.getImageFormat()).isEqualTo(IMAGE_FORMAT);
+    }
+
+    @Test
+    public void getMaxImages_returnsFixedCapacity() {
+        assertThat(imageReaderProxy.getMaxImages()).isEqualTo(MAX_IMAGES);
+    }
+
+    private static final class ConcreteImageProxy extends ForwardingImageProxy {
+        private boolean isClosed = false;
+
+        ConcreteImageProxy(ImageProxy image) {
+            super(image);
+        }
+
+        @Override
+        public synchronized void close() {
+            super.close();
+            isClosed = true;
+        }
+
+        public synchronized boolean isClosed() {
+            return isClosed;
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyAndroidTest.java
new file mode 100644
index 0000000..74ae7b6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyAndroidTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+public class ReferenceCountedImageProxyAndroidTest {
+    private static final int WIDTH = 640;
+    private static final int HEIGHT = 480;
+
+    // Assume the image has YUV_420_888 format.
+    private final ImageProxy image = mock(ImageProxy.class);
+    private final ImageProxy.PlaneProxy yPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy uPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy vPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ByteBuffer yBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
+    private final ByteBuffer uBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+    private final ByteBuffer vBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+    private ReferenceCountedImageProxy imageProxy;
+
+    @Before
+    public void setUp() {
+        when(image.getWidth()).thenReturn(WIDTH);
+        when(image.getHeight()).thenReturn(HEIGHT);
+        when(yPlane.getBuffer()).thenReturn(yBuffer);
+        when(uPlane.getBuffer()).thenReturn(uBuffer);
+        when(vPlane.getBuffer()).thenReturn(vBuffer);
+        when(image.getPlanes()).thenReturn(new ImageProxy.PlaneProxy[]{yPlane, uPlane, vPlane});
+        imageProxy = new ReferenceCountedImageProxy(image);
+    }
+
+    @Test
+    public void getReferenceCount_returnsOne_afterConstruction() {
+        assertThat(imageProxy.getReferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void fork_incrementsReferenceCount() {
+        imageProxy.fork();
+        imageProxy.fork();
+
+        assertThat(imageProxy.getReferenceCount()).isEqualTo(3);
+    }
+
+    @Test
+    public void close_decrementsReferenceCount() {
+        ImageProxy forkedImage0 = imageProxy.fork();
+        ImageProxy forkedImage1 = imageProxy.fork();
+
+        forkedImage0.close();
+        forkedImage1.close();
+
+        assertThat(imageProxy.getReferenceCount()).isEqualTo(1);
+        verify(image, never()).close();
+    }
+
+    @Test
+    public void close_closesBaseImage_whenReferenceCountHitsZero() {
+        ImageProxy forkedImage0 = imageProxy.fork();
+        ImageProxy forkedImage1 = imageProxy.fork();
+
+        forkedImage0.close();
+        forkedImage1.close();
+        imageProxy.close();
+
+        assertThat(imageProxy.getReferenceCount()).isEqualTo(0);
+        verify(image, times(1)).close();
+    }
+
+    @Test
+    public void close_decrementsReferenceCountOnlyOnce() {
+        ImageProxy forkedImage = imageProxy.fork();
+
+        forkedImage.close();
+        forkedImage.close();
+
+        assertThat(imageProxy.getReferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void fork_returnsNull_whenBaseImageIsClosed() {
+        imageProxy.close();
+
+        ImageProxy forkedImage = imageProxy.fork();
+
+        assertThat(forkedImage).isNull();
+    }
+
+    @Test
+    public void concurrentAccessForTwoForkedImagesOnTwoThreads() throws InterruptedException {
+        final ImageProxy forkedImage0 = imageProxy.fork();
+        final ImageProxy forkedImage1 = imageProxy.fork();
+
+        Thread thread0 =
+                new Thread() {
+                    @Override
+                    public void run() {
+                        forkedImage0.getWidth();
+                        forkedImage0.getHeight();
+                        ImageProxy.PlaneProxy[] planes = forkedImage0.getPlanes();
+                        for (ImageProxy.PlaneProxy plane : planes) {
+                            ByteBuffer buffer = plane.getBuffer();
+                            for (int i = 0; i < buffer.capacity(); ++i) {
+                                buffer.get(i);
+                            }
+                        }
+                    }
+                };
+        Thread thread1 =
+                new Thread() {
+                    @Override
+                    public void run() {
+                        forkedImage1.getWidth();
+                        forkedImage1.getHeight();
+                        ImageProxy.PlaneProxy[] planes = forkedImage1.getPlanes();
+                        for (ImageProxy.PlaneProxy plane : planes) {
+                            ByteBuffer buffer = plane.getBuffer();
+                            for (int i = 0; i < buffer.capacity(); ++i) {
+                                buffer.get(i);
+                            }
+                        }
+                    }
+                };
+
+        thread0.start();
+        thread1.start();
+        thread0.join();
+        thread1.join();
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationAndroidTest.java
new file mode 100644
index 0000000..c6eca95
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationAndroidTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.view.Surface;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class SessionConfigurationAndroidTest {
+    private DeferrableSurface mockSurface0;
+    private DeferrableSurface mockSurface1;
+
+    @Before
+    public void setup() {
+        mockSurface0 = new ImmediateSurface(Mockito.mock(Surface.class));
+        mockSurface1 = new ImmediateSurface(Mockito.mock(Surface.class));
+    }
+
+    @Test
+    public void builderSetTemplate() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        assertThat(sessionConfiguration.getTemplateType()).isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+    }
+
+    @Test
+    public void builderAddSurface() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addSurface(mockSurface0);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+
+        assertThat(surfaces).hasSize(1);
+        assertThat(surfaces).contains(mockSurface0);
+    }
+
+    @Test
+    public void builderAddNonRepeatingSurface() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addNonRepeatingSurface(mockSurface0);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+        List<DeferrableSurface> repeatingSurfaces =
+                sessionConfiguration.getCaptureRequestConfiguration().getSurfaces();
+
+        assertThat(surfaces).hasSize(1);
+        assertThat(surfaces).contains(mockSurface0);
+        assertThat(repeatingSurfaces).isEmpty();
+        assertThat(repeatingSurfaces).doesNotContain(mockSurface0);
+    }
+
+    @Test
+    public void builderAddSurfaceContainsRepeatingSurface() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addSurface(mockSurface0);
+        builder.addNonRepeatingSurface(mockSurface1);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        List<Surface> surfaces = DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces());
+        List<Surface> repeatingSurfaces =
+                DeferrableSurfaces.surfaceList(
+                        sessionConfiguration.getCaptureRequestConfiguration().getSurfaces());
+
+        assertThat(surfaces.size()).isAtLeast(repeatingSurfaces.size());
+        assertThat(surfaces).containsAllIn(repeatingSurfaces);
+    }
+
+    @Test
+    public void builderRemoveSurface() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addSurface(mockSurface0);
+        builder.removeSurface(mockSurface0);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        List<Surface> surfaces = DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces());
+        assertThat(surfaces).isEmpty();
+    }
+
+    @Test
+    public void builderClearSurface() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addSurface(mockSurface0);
+        builder.clearSurfaces();
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        List<Surface> surfaces = DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces());
+        assertThat(surfaces.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void builderAddCharacteristic() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        Map<Key<?>, CaptureRequestParameter<?>> parameterMap =
+                sessionConfiguration.getCameraCharacteristics();
+
+        assertThat(parameterMap.containsKey(CaptureRequest.CONTROL_AF_MODE)).isTrue();
+        assertThat(parameterMap)
+                .containsEntry(
+                        CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequestParameter.create(
+                                CaptureRequest.CONTROL_AF_MODE,
+                                CaptureRequest.CONTROL_AF_MODE_AUTO));
+    }
+
+    @Test
+    public void conflictingTemplate() {
+        SessionConfiguration.Builder builderPreview = new SessionConfiguration.Builder();
+        builderPreview.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        SessionConfiguration sessionConfigurationPreview = builderPreview.build();
+        SessionConfiguration.Builder builderZsl = new SessionConfiguration.Builder();
+        builderZsl.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
+        SessionConfiguration sessionConfigurationZsl = builderZsl.build();
+
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+
+        validatingBuilder.add(sessionConfigurationPreview);
+        validatingBuilder.add(sessionConfigurationZsl);
+
+        assertThat(validatingBuilder.isValid()).isFalse();
+    }
+
+    @Test
+    public void conflictingCharacteristics() {
+        SessionConfiguration.Builder builderAfAuto = new SessionConfiguration.Builder();
+        builderAfAuto.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+        SessionConfiguration sessionConfigurationAfAuto = builderAfAuto.build();
+        SessionConfiguration.Builder builderAfOff = new SessionConfiguration.Builder();
+        builderAfOff.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
+        SessionConfiguration sessionConfigurationAfOff = builderAfOff.build();
+
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+
+        validatingBuilder.add(sessionConfigurationAfAuto);
+        validatingBuilder.add(sessionConfigurationAfOff);
+
+        assertThat(validatingBuilder.isValid()).isFalse();
+    }
+
+    @Test
+    public void combineTwoSessionsValid() {
+        SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+        builder0.addSurface(mockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mockSurface1);
+        builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder1.addCharacteristic(
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+        validatingBuilder.add(builder0.build());
+        validatingBuilder.add(builder1.build());
+
+        assertThat(validatingBuilder.isValid()).isTrue();
+    }
+
+    @Test
+    public void combineTwoSessionsTemplate() {
+        SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+        builder0.addSurface(mockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mockSurface1);
+        builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder1.addCharacteristic(
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+        validatingBuilder.add(builder0.build());
+        validatingBuilder.add(builder1.build());
+
+        SessionConfiguration sessionConfiguration = validatingBuilder.build();
+
+        assertThat(sessionConfiguration.getTemplateType()).isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+    }
+
+    @Test
+    public void combineTwoSessionsSurfaces() {
+        SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+        builder0.addSurface(mockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mockSurface1);
+        builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder1.addCharacteristic(
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+        validatingBuilder.add(builder0.build());
+        validatingBuilder.add(builder1.build());
+
+        SessionConfiguration sessionConfiguration = validatingBuilder.build();
+
+        List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+        assertThat(surfaces).containsExactly(mockSurface0, mockSurface1);
+    }
+
+    @Test
+    public void combineTwoSessionsCharacteristics() {
+        SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+        builder0.addSurface(mockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mockSurface1);
+        builder1.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder1.addCharacteristic(
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
+
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+        validatingBuilder.add(builder0.build());
+        validatingBuilder.add(builder1.build());
+
+        SessionConfiguration sessionConfiguration = validatingBuilder.build();
+
+        Map<Key<?>, CaptureRequestParameter<?>> parameterMap =
+                sessionConfiguration.getCameraCharacteristics();
+        assertThat(parameterMap)
+                .containsExactly(
+                        CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequestParameter.create(
+                                CaptureRequest.CONTROL_AF_MODE,
+                                CaptureRequest.CONTROL_AF_MODE_AUTO),
+                        CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+                        CaptureRequestParameter.create(
+                                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
+                                CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO));
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyAndroidTest.java
new file mode 100644
index 0000000..09f3175
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyAndroidTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public final class SingleCloseImageProxyAndroidTest {
+
+    private final ImageProxy imageProxy = mock(ImageProxy.class);
+    private SingleCloseImageProxy singleCloseImageProxy;
+
+    @Before
+    public void setUp() {
+        singleCloseImageProxy = new SingleCloseImageProxy(imageProxy);
+    }
+
+    @Test
+    public void wrappedImageIsClosedOnce_whenWrappingImageIsClosedOnce() {
+        singleCloseImageProxy.close();
+
+        verify(imageProxy, times(1)).close();
+    }
+
+    @Test
+    public void wrappedImageIsClosedOnce_whenWrappingImageIsClosedTwice() {
+        singleCloseImageProxy.close();
+        singleCloseImageProxy.close();
+
+        verify(imageProxy, times(1)).close();
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/StreamConfigurationMapUtil.java b/camera/core/src/androidTest/java/androidx/camera/core/StreamConfigurationMapUtil.java
new file mode 100644
index 0000000..0bfd39f
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/StreamConfigurationMapUtil.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.util.Size;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+/** Utility functions to obtain fake {@link StreamConfigurationMap} for testing */
+public final class StreamConfigurationMapUtil {
+    /**
+     * Generates fake StreamConfigurationMap for testing usage.
+     *
+     * @return a fake {@link StreamConfigurationMap} object
+     */
+    public static StreamConfigurationMap generateFakeStreamConfigurationMap() {
+        /**
+         * Defined in StreamConfigurationMap.java: 0x21 is internal defined legal format
+         * corresponding to ImageFormat.JPEG. 0x22 is internal defined legal format
+         * IMPLEMENTATION_DEFINED and at least one stream configuration for
+         * IMPLEMENTATION_DEFINED(0x22) must exist, otherwise, there will be AssertionError threw.
+         * 0x22 is also mapped to ImageFormat.PRIVATE after Android level 23.
+         */
+        int[] supportedFormats =
+                new int[]{
+                        ImageFormat.YUV_420_888,
+                        ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+                        ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+                };
+        Size[] supportedSizes =
+                new Size[]{
+                        new Size(4032, 3024),
+                        new Size(3840, 2160),
+                        new Size(1920, 1080),
+                        new Size(640, 480),
+                        new Size(320, 240),
+                        new Size(320, 180)
+                };
+
+        return generateFakeStreamConfigurationMap(supportedFormats, supportedSizes);
+    }
+
+    /**
+     * Generates fake StreamConfigurationMap for testing usage.
+     *
+     * @param supportedFormats The supported {@link ImageFormat} list to be added
+     * @param supportedSizes   The supported sizes to be added
+     * @return a fake {@link StreamConfigurationMap} object
+     */
+    public static StreamConfigurationMap generateFakeStreamConfigurationMap(
+            int[] supportedFormats, Size[] supportedSizes) {
+        StreamConfigurationMap map;
+
+        // TODO(b/123938482): Remove usage of reflection in this class
+        Class<?> streamConfigurationClass;
+        Class<?> streamConfigurationDurationClass;
+        Class<?> highSpeedVideoConfigurationClass;
+        Class<?> reprocessFormatsMapClass;
+
+        try {
+            streamConfigurationClass =
+                    Class.forName("android.hardware.camera2.params.StreamConfiguration");
+            streamConfigurationDurationClass =
+                    Class.forName("android.hardware.camera2.params.StreamConfigurationDuration");
+            highSpeedVideoConfigurationClass =
+                    Class.forName("android.hardware.camera2.params.HighSpeedVideoConfiguration");
+            reprocessFormatsMapClass =
+                    Class.forName("android.hardware.camera2.params.ReprocessFormatsMap");
+        } catch (ClassNotFoundException e) {
+            throw new AssertionError(
+                    "Class can not be found when trying to generate a StreamConfigurationMap "
+                            + "object.",
+                    e);
+        }
+
+        Constructor<?> streamConfigurationMapConstructor;
+        Constructor<?> streamConfigurationConstructor;
+        Constructor<?> streamConfigurationDurationConstructor;
+
+        try {
+            if (Build.VERSION.SDK_INT >= 23) {
+                streamConfigurationMapConstructor =
+                        StreamConfigurationMap.class.getDeclaredConstructor(
+                                Array.newInstance(streamConfigurationClass, 1).getClass(),
+                                Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+                                Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+                                Array.newInstance(streamConfigurationClass, 1).getClass(),
+                                Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+                                Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+                                Array.newInstance(highSpeedVideoConfigurationClass, 1).getClass(),
+                                reprocessFormatsMapClass,
+                                boolean.class);
+            } else {
+                streamConfigurationMapConstructor =
+                        StreamConfigurationMap.class.getDeclaredConstructor(
+                                Array.newInstance(streamConfigurationClass, 1).getClass(),
+                                Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+                                Array.newInstance(streamConfigurationDurationClass, 1).getClass(),
+                                Array.newInstance(highSpeedVideoConfigurationClass, 1).getClass());
+            }
+
+            streamConfigurationConstructor =
+                    streamConfigurationClass.getDeclaredConstructor(
+                            int.class, int.class, int.class, boolean.class);
+
+            streamConfigurationDurationConstructor =
+                    streamConfigurationDurationClass.getDeclaredConstructor(
+                            int.class, int.class, int.class, long.class);
+        } catch (NoSuchMethodException e) {
+            throw new AssertionError(
+                    "Constructor can not be found when trying to generate a "
+                            + "StreamConfigurationMap object.",
+                    e);
+        }
+
+        Object configurationArray =
+                Array.newInstance(
+                        streamConfigurationClass, supportedFormats.length * supportedSizes.length);
+        Object minFrameDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+        Object stallDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+        Object depthConfigurationArray = Array.newInstance(streamConfigurationClass, 1);
+        Object depthMinFrameDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+        Object depthStallDurationArray = Array.newInstance(streamConfigurationDurationClass, 1);
+
+        try {
+            for (int i = 0; i < supportedFormats.length; i++) {
+                for (int j = 0; j < supportedSizes.length; j++) {
+                    Array.set(
+                            configurationArray,
+                            i * supportedSizes.length + j,
+                            streamConfigurationConstructor.newInstance(
+                                    supportedFormats[i],
+                                    supportedSizes[j].getWidth(),
+                                    supportedSizes[j].getHeight(),
+                                    false));
+                }
+            }
+
+            Array.set(
+                    minFrameDurationArray,
+                    0,
+                    streamConfigurationDurationConstructor.newInstance(
+                            ImageFormat.YUV_420_888, 1920, 1080, 0));
+
+            Array.set(
+                    stallDurationArray,
+                    0,
+                    streamConfigurationDurationConstructor.newInstance(
+                            ImageFormat.YUV_420_888, 1920, 1080, 0));
+
+            // Need depth configuration to create the object successfully
+            // 0x24 is internal format type of HAL_PIXEL_FORMAT_RAW_OPAQUE
+            Array.set(
+                    depthConfigurationArray,
+                    0,
+                    streamConfigurationConstructor.newInstance(0x24, 1920, 1080, false));
+
+            Array.set(
+                    depthMinFrameDurationArray,
+                    0,
+                    streamConfigurationDurationConstructor.newInstance(0x24, 1920, 1080, 0));
+
+            Array.set(
+                    depthStallDurationArray,
+                    0,
+                    streamConfigurationDurationConstructor.newInstance(0x24, 1920, 1080, 0));
+
+            if (Build.VERSION.SDK_INT >= 23) {
+                map =
+                        (StreamConfigurationMap)
+                                streamConfigurationMapConstructor.newInstance(
+                                        configurationArray,
+                                        minFrameDurationArray,
+                                        stallDurationArray,
+                                        depthConfigurationArray,
+                                        depthMinFrameDurationArray,
+                                        depthStallDurationArray,
+                                        null,
+                                        null,
+                                        false);
+            } else {
+                map =
+                        (StreamConfigurationMap)
+                                streamConfigurationMapConstructor.newInstance(
+                                        configurationArray,
+                                        minFrameDurationArray,
+                                        stallDurationArray,
+                                        null);
+            }
+
+        } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
+            throw new AssertionError(
+                    "Failed to create new instance when trying to generate a "
+                            + "StreamConfigurationMap object.",
+                    e);
+        }
+
+        return map;
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateAndroidTest.java
new file mode 100644
index 0000000..a8d9ef0
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateAndroidTest.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.testing.fakes.FakeAppConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class UseCaseAttachStateAndroidTest {
+    private final LensFacing cameraLensFacing0 = LensFacing.BACK;
+    private final LensFacing cameraLensFacing1 = LensFacing.FRONT;
+    private final CameraDevice mockCameraDevice = Mockito.mock(CameraDevice.class);
+    private final CameraCaptureSession mockCameraCaptureSession =
+            Mockito.mock(CameraCaptureSession.class);
+
+    private String cameraId;
+
+    @Before
+    public void setUp() {
+        AppConfiguration appConfiguration = FakeAppConfiguration.create();
+        CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            cameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+        }
+        CameraX.init(ApplicationProvider.getApplicationContext(), appConfiguration);
+    }
+
+    @Test
+    public void setSingleUseCaseOnline() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+                .containsExactly(fakeUseCase.surface);
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+        verify(fakeUseCase.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+        verify(fakeUseCase.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setTwoUseCasesOnline() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase0 = new TestUseCase(configuration0, cameraId);
+        FakeUseCaseConfiguration configuration1 =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase1 = new TestUseCase(configuration1, cameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase0);
+        useCaseAttachState.setUseCaseOnline(fakeUseCase1);
+
+        SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+                .containsExactly(fakeUseCase0.surface, fakeUseCase1.surface);
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+        verify(fakeUseCase0.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+        verify(fakeUseCase1.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+        verify(fakeUseCase0.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+        verify(fakeUseCase1.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase0.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+        verify(fakeUseCase1.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseActiveOnly() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder =
+                useCaseAttachState.getActiveAndOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+        verify(fakeUseCase.deviceStateCallback, never()).onOpened(mockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+        verify(fakeUseCase.sessionStateCallback, never()).onConfigured(mockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.cameraCaptureCallback, never()).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseActiveAndOnline() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+        useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder =
+                useCaseAttachState.getActiveAndOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+                .containsExactly(fakeUseCase.surface);
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+        verify(fakeUseCase.deviceStateCallback, times(1)).onOpened(mockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+        verify(fakeUseCase.sessionStateCallback, times(1)).onConfigured(mockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.cameraCaptureCallback, times(1)).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseOffline() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+        useCaseAttachState.setUseCaseOffline(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+        verify(fakeUseCase.deviceStateCallback, never()).onOpened(mockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+        verify(fakeUseCase.sessionStateCallback, never()).onConfigured(mockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.cameraCaptureCallback, never()).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseInactive() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+        useCaseAttachState.setUseCaseActive(fakeUseCase);
+        useCaseAttachState.setUseCaseInactive(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder =
+                useCaseAttachState.getActiveAndOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mockCameraDevice);
+        verify(fakeUseCase.deviceStateCallback, never()).onOpened(mockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mockCameraCaptureSession);
+        verify(fakeUseCase.sessionStateCallback, never()).onConfigured(mockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.cameraCaptureCallback, never()).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void updateUseCase() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+        useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+        // The original template should be PREVIEW.
+        SessionConfiguration firstSessionConfiguration =
+                useCaseAttachState.getActiveAndOnlineBuilder().build();
+        assertThat(firstSessionConfiguration.getTemplateType())
+                .isEqualTo(CameraDevice.TEMPLATE_PREVIEW);
+
+        // Change the template to STILL_CAPTURE.
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+        builder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+        fakeUseCase.attachToCamera(cameraId, builder.build());
+
+        useCaseAttachState.updateUseCase(fakeUseCase);
+
+        // The new template should be STILL_CAPTURE.
+        SessionConfiguration secondSessionConfiguration =
+                useCaseAttachState.getActiveAndOnlineBuilder().build();
+        assertThat(secondSessionConfiguration.getTemplateType())
+                .isEqualTo(CameraDevice.TEMPLATE_STILL_CAPTURE);
+    }
+
+    @Test
+    public void setUseCaseOnlineWithWrongCamera() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing1)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> useCaseAttachState.setUseCaseOnline(fakeUseCase));
+    }
+
+    @Test
+    public void setUseCaseActiveWithWrongCamera() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(cameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(cameraLensFacing1)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, cameraId);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> useCaseAttachState.setUseCaseActive(fakeUseCase));
+    }
+
+    private static class TestUseCase extends FakeUseCase {
+        private final Surface surface = Mockito.mock(Surface.class);
+        private final CameraDevice.StateCallback deviceStateCallback =
+                Mockito.mock(CameraDevice.StateCallback.class);
+        private final CameraCaptureSession.StateCallback sessionStateCallback =
+                Mockito.mock(CameraCaptureSession.StateCallback.class);
+        private final CameraCaptureCallback cameraCaptureCallback =
+                Mockito.mock(CameraCaptureCallback.class);
+
+        TestUseCase(FakeUseCaseConfiguration configuration, String cameraId) {
+            super(configuration);
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            suggestedResolutionMap.put(cameraId, new Size(640, 480));
+            updateSuggestedResolution(suggestedResolutionMap);
+        }
+
+        @Override
+        protected Map<String, Size> onSuggestedResolutionUpdated(
+                Map<String, Size> suggestedResolutionMap) {
+            SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            builder.addSurface(new ImmediateSurface(surface));
+            builder.setDeviceStateCallback(deviceStateCallback);
+            builder.setSessionStateCallback(sessionStateCallback);
+            builder.setCameraCaptureCallback(cameraCaptureCallback);
+
+            LensFacing lensFacing =
+                    ((CameraDeviceConfiguration) getUseCaseConfiguration()).getLensFacing();
+            try {
+                String cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+                attachToCamera(cameraId, builder.build());
+            } catch (Exception e) {
+                throw new IllegalArgumentException(
+                        "Unable to attach to camera with LensFacing " + lensFacing, e);
+            }
+            return suggestedResolutionMap;
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupAndroidTest.java
new file mode 100644
index 0000000..c218c18
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupAndroidTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseGroupAndroidTest {
+    private final UseCaseGroup.StateChangeListener mockListener =
+            Mockito.mock(UseCaseGroup.StateChangeListener.class);
+    private FakeUseCaseConfiguration fakeUseCaseConfiguration;
+    private FakeOtherUseCaseConfiguration fakeOtherUseCaseConfiguration;
+    private UseCaseGroup useCaseGroup;
+    private FakeUseCase fakeUseCase;
+    private FakeOtherUseCase fakeOtherUseCase;
+
+    @Before
+    public void setUp() {
+        fakeUseCaseConfiguration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("fakeUseCaseConfiguration")
+                        .build();
+        fakeOtherUseCaseConfiguration =
+                new FakeOtherUseCaseConfiguration.Builder()
+                        .setTargetName("fakeOtherUseCaseConfiguration")
+                        .build();
+        useCaseGroup = new UseCaseGroup();
+        fakeUseCase = new FakeUseCase(fakeUseCaseConfiguration);
+        fakeOtherUseCase = new FakeOtherUseCase(fakeOtherUseCaseConfiguration);
+    }
+
+    @Test
+    public void groupStartsEmpty() {
+        assertThat(useCaseGroup.getUseCases()).isEmpty();
+    }
+
+    @Test
+    public void newUseCaseIsAdded_whenNoneExistsInGroup() {
+        assertThat(useCaseGroup.addUseCase(fakeUseCase)).isTrue();
+        assertThat(useCaseGroup.getUseCases()).containsExactly(fakeUseCase);
+    }
+
+    @Test
+    public void multipleUseCases_canBeAdded() {
+        assertThat(useCaseGroup.addUseCase(fakeUseCase)).isTrue();
+        assertThat(useCaseGroup.addUseCase(fakeOtherUseCase)).isTrue();
+
+        assertThat(useCaseGroup.getUseCases()).containsExactly(fakeUseCase, fakeOtherUseCase);
+    }
+
+    @Test
+    public void groupBecomesEmpty_afterGroupIsCleared() {
+        useCaseGroup.addUseCase(fakeUseCase);
+        useCaseGroup.clear();
+
+        assertThat(useCaseGroup.getUseCases()).isEmpty();
+    }
+
+    @Test
+    public void useCaseIsCleared_afterGroupIsCleared() {
+        useCaseGroup.addUseCase(fakeUseCase);
+        assertThat(fakeUseCase.isCleared()).isFalse();
+
+        useCaseGroup.clear();
+
+        assertThat(fakeUseCase.isCleared()).isTrue();
+    }
+
+    @Test
+    public void useCaseRemoved_afterRemovedCalled() {
+        useCaseGroup.addUseCase(fakeUseCase);
+
+        useCaseGroup.removeUseCase(fakeUseCase);
+
+        assertThat(useCaseGroup.getUseCases()).isEmpty();
+    }
+
+    @Test
+    public void listenerOnGroupActive_ifUseCaseGroupStarted() {
+        useCaseGroup.setListener(mockListener);
+        useCaseGroup.start();
+
+        verify(mockListener, times(1)).onGroupActive(useCaseGroup);
+    }
+
+    @Test
+    public void listenerOnGroupInactive_ifUseCaseGroupStopped() {
+        useCaseGroup.setListener(mockListener);
+        useCaseGroup.stop();
+
+        verify(mockListener, times(1)).onGroupInactive(useCaseGroup);
+    }
+
+    @Test
+    public void setListener_replacesPreviousListener() {
+        useCaseGroup.setListener(mockListener);
+        useCaseGroup.setListener(null);
+
+        useCaseGroup.start();
+        verify(mockListener, never()).onGroupActive(useCaseGroup);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerAndroidTest.java
new file mode 100644
index 0000000..065a53d
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerAndroidTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+public class UseCaseGroupLifecycleControllerAndroidTest {
+    private final UseCaseGroup.StateChangeListener mockListener =
+            Mockito.mock(UseCaseGroup.StateChangeListener.class);
+    private UseCaseGroupLifecycleController useCaseGroupLifecycleController;
+    private FakeLifecycleOwner lifecycleOwner;
+
+    @Before
+    public void setUp() {
+        lifecycleOwner = new FakeLifecycleOwner();
+    }
+
+    @Test
+    public void groupCanBeMadeObserverOfLifecycle() {
+        assertThat(lifecycleOwner.getObserverCount()).isEqualTo(0);
+
+        useCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        lifecycleOwner.getLifecycle(), new UseCaseGroup());
+
+        assertThat(lifecycleOwner.getObserverCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void groupCanStopObservingALifeCycle() {
+        useCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        lifecycleOwner.getLifecycle(), new UseCaseGroup());
+        assertThat(lifecycleOwner.getObserverCount()).isEqualTo(1);
+
+        useCaseGroupLifecycleController.release();
+
+        assertThat(lifecycleOwner.getObserverCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void groupCanBeReleasedMultipleTimes() {
+        useCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        lifecycleOwner.getLifecycle(), new UseCaseGroup());
+
+        useCaseGroupLifecycleController.release();
+        useCaseGroupLifecycleController.release();
+    }
+
+    @Test
+    public void lifecycleStart_triggersOnActive() {
+        useCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        lifecycleOwner.getLifecycle(), new UseCaseGroup());
+        useCaseGroupLifecycleController.getUseCaseGroup().setListener(mockListener);
+
+        lifecycleOwner.start();
+
+        verify(mockListener, times(1))
+                .onGroupActive(useCaseGroupLifecycleController.getUseCaseGroup());
+    }
+
+    @Test
+    public void lifecycleStop_triggersOnInactive() {
+        useCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        lifecycleOwner.getLifecycle(), new UseCaseGroup());
+        useCaseGroupLifecycleController.getUseCaseGroup().setListener(mockListener);
+        lifecycleOwner.start();
+
+        lifecycleOwner.stop();
+
+        verify(mockListener, times(1))
+                .onGroupInactive(useCaseGroupLifecycleController.getUseCaseGroup());
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryAndroidTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryAndroidTest.java
new file mode 100644
index 0000000..dc961da
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryAndroidTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseGroupRepositoryAndroidTest {
+
+    private FakeLifecycleOwner lifecycle;
+    private UseCaseGroupRepository repository;
+    private Map<LifecycleOwner, UseCaseGroupLifecycleController> useCasesMap;
+
+    @Before
+    public void setUp() {
+        lifecycle = new FakeLifecycleOwner();
+        repository = new UseCaseGroupRepository();
+        useCasesMap = repository.getUseCasesMap();
+    }
+
+    @Test
+    public void repositoryStartsEmpty() {
+        assertThat(useCasesMap).isEmpty();
+    }
+
+    @Test
+    public void newUseCaseGroupIsCreated_whenNoGroupExistsForLifecycleInRepository() {
+        UseCaseGroupLifecycleController group = repository.getOrCreateUseCaseGroup(lifecycle);
+
+        assertThat(useCasesMap).containsExactly(lifecycle, group);
+    }
+
+    @Test
+    public void existingUseCaseGroupIsReturned_whenGroupExistsForLifecycleInRepository() {
+        UseCaseGroupLifecycleController firstGroup = repository.getOrCreateUseCaseGroup(lifecycle);
+        UseCaseGroupLifecycleController secondGroup = repository.getOrCreateUseCaseGroup(lifecycle);
+
+        assertThat(firstGroup).isSameAs(secondGroup);
+        assertThat(useCasesMap).containsExactly(lifecycle, firstGroup);
+    }
+
+    @Test
+    public void differentUseCaseGroupsAreCreated_forDifferentLifecycles() {
+        UseCaseGroupLifecycleController firstGroup = repository.getOrCreateUseCaseGroup(lifecycle);
+        FakeLifecycleOwner secondLifecycle = new FakeLifecycleOwner();
+        UseCaseGroupLifecycleController secondGroup =
+                repository.getOrCreateUseCaseGroup(secondLifecycle);
+
+        assertThat(useCasesMap)
+                .containsExactly(lifecycle, firstGroup, secondLifecycle, secondGroup);
+    }
+
+    @Test
+    public void useCaseGroupObservesLifecycle() {
+        repository.getOrCreateUseCaseGroup(lifecycle);
+
+        // One observer is the use case group. The other observer removes the use case from the
+        // repository when the lifecycle is destroyed.
+        assertThat(lifecycle.getObserverCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void useCaseGroupIsRemovedFromRepository_whenLifecycleIsDestroyed() {
+        repository.getOrCreateUseCaseGroup(lifecycle);
+        lifecycle.destroy();
+
+        assertThat(useCasesMap).isEmpty();
+    }
+
+    @Test
+    public void useCaseIsCleared_whenLifecycleIsDestroyed() {
+        UseCaseGroupLifecycleController group = repository.getOrCreateUseCaseGroup(lifecycle);
+        FakeUseCase useCase = new FakeUseCase();
+        group.getUseCaseGroup().addUseCase(useCase);
+
+        assertThat(useCase.isCleared()).isFalse();
+
+        lifecycle.destroy();
+
+        assertThat(useCase.isCleared()).isTrue();
+    }
+
+    @Test
+    public void exception_whenCreatingWithDestroyedLifecycle() {
+        lifecycle.destroy();
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> repository.getOrCreateUseCaseGroup(lifecycle));
+    }
+}
diff --git a/camera/core/src/main/AndroidManifest.xml b/camera/core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b8f5fd2
--- /dev/null
+++ b/camera/core/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.core">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+</manifest>
diff --git a/camera/core/src/main/java/androidx/camera/core/AndroidImageProxy.java b/camera/core/src/main/java/androidx/camera/core/AndroidImageProxy.java
new file mode 100644
index 0000000..a538d1d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AndroidImageProxy.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.media.Image;
+import android.os.Build;
+
+import androidx.annotation.GuardedBy;
+
+import java.nio.ByteBuffer;
+
+/** An {@link ImageProxy} which wraps around an {@link Image}. */
+final class AndroidImageProxy implements ImageProxy {
+    /**
+     * Image.setTimestamp(long) was added in M. On lower API levels, we use our own timestamp field
+     * to provide a more consistent behavior across more devices.
+     */
+    // TODO(b/124267925): Remove @SuppressLint once we target API 21
+    @SuppressLint("ObsoleteSdkInt")
+    private static final boolean SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+
+    @GuardedBy("this")
+    private final Image image;
+
+    @GuardedBy("this")
+    private final PlaneProxy[] planes;
+
+    @GuardedBy("this")
+    private long timestamp;
+
+    /**
+     * Creates a new instance which wraps the given image.
+     *
+     * @param image to wrap
+     * @return new {@link AndroidImageProxy} instance
+     */
+    AndroidImageProxy(Image image) {
+        this.image = image;
+
+        Image.Plane[] originalPlanes = image.getPlanes();
+        if (originalPlanes != null) {
+            this.planes = new PlaneProxy[originalPlanes.length];
+            for (int i = 0; i < originalPlanes.length; ++i) {
+                this.planes[i] = new PlaneProxy(originalPlanes[i]);
+            }
+        } else {
+            this.planes = new PlaneProxy[0];
+        }
+
+        this.timestamp = image.getTimestamp();
+    }
+
+    @Override
+    public synchronized void close() {
+        image.close();
+    }
+
+    @Override
+    public synchronized Rect getCropRect() {
+        return image.getCropRect();
+    }
+
+    @Override
+    public synchronized void setCropRect(Rect rect) {
+        image.setCropRect(rect);
+    }
+
+    @Override
+    public synchronized int getFormat() {
+        return image.getFormat();
+    }
+
+    @Override
+    public synchronized int getHeight() {
+        return image.getHeight();
+    }
+
+    @Override
+    public synchronized int getWidth() {
+        return image.getWidth();
+    }
+
+    @Override
+    public synchronized long getTimestamp() {
+        if (SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK) {
+            return image.getTimestamp();
+        } else {
+            return timestamp;
+        }
+    }
+
+    @Override
+    public synchronized void setTimestamp(long timestamp) {
+        if (SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK) {
+            image.setTimestamp(timestamp);
+        } else {
+            this.timestamp = timestamp;
+        }
+    }
+
+    @Override
+    public synchronized ImageProxy.PlaneProxy[] getPlanes() {
+        return planes;
+    }
+
+    /** An {@link ImageProxy.PlaneProxy} which wraps around an {@link Image.Plane}. */
+    private static final class PlaneProxy implements ImageProxy.PlaneProxy {
+        @GuardedBy("this")
+        private final Image.Plane plane;
+
+        PlaneProxy(Image.Plane plane) {
+            this.plane = plane;
+        }
+
+        @Override
+        public synchronized int getRowStride() {
+            return plane.getRowStride();
+        }
+
+        @Override
+        public synchronized int getPixelStride() {
+            return plane.getPixelStride();
+        }
+
+        @Override
+        public synchronized ByteBuffer getBuffer() {
+            return plane.getBuffer();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java b/camera/core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
new file mode 100644
index 0000000..2a7b579f
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+
+/**
+ * An {@link ImageReaderProxy} which wraps around an {@link ImageReader}.
+ *
+ * <p>All methods map one-to-one between this {@link ImageReaderProxy} and the wrapped {@link
+ * ImageReader}.
+ */
+final class AndroidImageReaderProxy implements ImageReaderProxy {
+    @GuardedBy("this")
+    private final ImageReader imageReader;
+
+    /**
+     * Creates a new instance which wraps the given image reader.
+     *
+     * @param imageReader to wrap
+     * @return new {@link AndroidImageReaderProxy} instance
+     */
+    AndroidImageReaderProxy(ImageReader imageReader) {
+        this.imageReader = imageReader;
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireLatestImage() {
+        Image image = imageReader.acquireLatestImage();
+        if (image == null) {
+            return null;
+        }
+        return new AndroidImageProxy(image);
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireNextImage() {
+        Image image = imageReader.acquireNextImage();
+        if (image == null) {
+            return null;
+        }
+        return new AndroidImageProxy(image);
+    }
+
+    @Override
+    public synchronized void close() {
+        imageReader.close();
+    }
+
+    @Override
+    public synchronized int getHeight() {
+        return imageReader.getHeight();
+    }
+
+    @Override
+    public synchronized int getWidth() {
+        return imageReader.getWidth();
+    }
+
+    @Override
+    public synchronized int getImageFormat() {
+        return imageReader.getImageFormat();
+    }
+
+    @Override
+    public synchronized int getMaxImages() {
+        return imageReader.getMaxImages();
+    }
+
+    @Override
+    public synchronized Surface getSurface() {
+        return imageReader.getSurface();
+    }
+
+    @Override
+    public synchronized void setOnImageAvailableListener(
+            @Nullable ImageReaderProxy.OnImageAvailableListener listener,
+            @Nullable Handler handler) {
+        ImageReader.OnImageAvailableListener transformedListener =
+                reader -> {
+                    listener.onImageAvailable(AndroidImageReaderProxy.this);
+                };
+        imageReader.setOnImageAvailableListener(transformedListener, handler);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/AppConfiguration.java b/camera/core/src/main/java/androidx/camera/core/AppConfiguration.java
new file mode 100644
index 0000000..8847cc5
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AppConfiguration.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * Configuration for adding implementation and user-specific behavior to CameraX.
+ *
+ * <p>The AppConfiguration
+ *
+ * @hide
+ */
+public final class AppConfiguration implements TargetConfiguration<CameraX> {
+
+    static final Option<CameraFactory> OPTION_CAMERA_FACTORY =
+            Option.create("camerax.core.appConfig.cameraFactory", CameraFactory.class);
+    static final Option<CameraDeviceSurfaceManager> OPTION_DEVICE_SURFACE_MANAGER =
+            Option.create(
+                    "camerax.core.appConfig.deviceSurfaceManager",
+                    CameraDeviceSurfaceManager.class);
+    static final Option<UseCaseConfigurationFactory> OPTION_USECASE_CONFIG_FACTORY =
+            Option.create(
+                    "camerax.core.appConfig.useCaseConfigFactory",
+                    UseCaseConfigurationFactory.class);
+    private final OptionsBundle config;
+
+    AppConfiguration(OptionsBundle options) {
+        this.config = options;
+    }
+
+    /**
+     * Returns the {@link CameraFactory} implementation for the application.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraFactory getCameraFactory(@Nullable CameraFactory valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_CAMERA_FACTORY, valueIfMissing);
+    }
+
+    /**
+     * Returns the {@link CameraDeviceSurfaceManager} implementation for the application.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraDeviceSurfaceManager getDeviceSurfaceManager(
+            @Nullable CameraDeviceSurfaceManager valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_DEVICE_SURFACE_MANAGER, valueIfMissing);
+    }
+
+    // Option Declarations:
+    // ***********************************************************************************************
+
+    /**
+     * Returns the {@link UseCaseConfigurationFactory} implementation for the application.
+     *
+     * <p>This factory should produce all default configurations for the application's use cases.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public UseCaseConfigurationFactory getUseCaseConfigRepository(
+            @Nullable UseCaseConfigurationFactory valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_USECASE_CONFIG_FACTORY, valueIfMissing);
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** A builder for generating {@link AppConfiguration} objects. */
+    public static final class Builder
+            implements TargetConfiguration.Builder<CameraX, AppConfiguration, Builder> {
+
+        private final MutableOptionsBundle mutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            this.mutableConfig = mutableConfig;
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(CameraX.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + AppConfiguration.Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass(CameraX.class);
+        }
+
+        /**
+         * Generates a Builder from another Configuration object
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        public static Builder fromConfig(Configuration configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * Sets the {@link CameraFactory} implementation for the application.
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setCameraFactory(CameraFactory cameraFactory) {
+            getMutableConfiguration().insertOption(OPTION_CAMERA_FACTORY, cameraFactory);
+            return builder();
+        }
+
+        /**
+         * Sets the {@link CameraDeviceSurfaceManager} implementation for the application.
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setDeviceSurfaceManager(CameraDeviceSurfaceManager repository) {
+            getMutableConfiguration().insertOption(OPTION_DEVICE_SURFACE_MANAGER, repository);
+            return builder();
+        }
+
+        /**
+         * Sets the {@link UseCaseConfigurationFactory} implementation for the application.
+         *
+         * <p>This factory should produce all default configurations for the application's use
+         * cases.
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setUseCaseConfigFactory(UseCaseConfigurationFactory repository) {
+            getMutableConfiguration().insertOption(OPTION_USECASE_CONFIG_FACTORY, repository);
+            return builder();
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mutableConfig;
+        }
+
+        /** The solution for the unchecked cast warning. */
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public AppConfiguration build() {
+            return new AppConfiguration(OptionsBundle.from(mutableConfig));
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/BaseCamera.java b/camera/core/src/main/java/androidx/camera/core/BaseCamera.java
new file mode 100644
index 0000000..5aa5cc7
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/BaseCamera.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import java.util.Collection;
+
+/**
+ * The base camera interface. It is controlled by the change of state in use cases.
+ *
+ * @hide
+ */
+public interface BaseCamera extends BaseUseCase.StateChangeListener {
+    /**
+     * Open the camera asynchronously.
+     *
+     * <p>Once the camera has been opened use case state transitions can be used to control the
+     * camera pipeline.
+     */
+    void open();
+
+    /**
+     * Close the camera asynchronously.
+     *
+     * <p>Once the camera is closed the camera will no longer produce data. The camera must be
+     * reopened for it to produce data again.
+     */
+    void close();
+
+    /**
+     * Release the camera.
+     *
+     * <p>Once the camera is released it is permanently closed. A new instance must be created to
+     * access the camera.
+     */
+    void release();
+
+    /**
+     * Sets the use case to be in the state where the capture session will be configured to handle
+     * capture requests from the use cases.
+     */
+    void addOnlineUseCase(Collection<BaseUseCase> baseUseCases);
+
+    /**
+     * Removes the use case to be in the state where the capture session will be configured to
+     * handle capture requests from the use cases.
+     */
+    void removeOnlineUseCase(Collection<BaseUseCase> baseUseCases);
+
+    /** Returns the global CameraControl attached to this camera. */
+    default CameraControl getCameraControl() {
+        return CameraControl.defaultEmptyInstance();
+    }
+
+    /** Returns an interface to retrieve characteristics of the camera. */
+    default CameraInfo getCameraInfo() throws CameraInfoUnavailableException {
+        throw new CameraInfoUnavailableException("Camera info not implemented.");
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/BaseUseCase.java b/camera/core/src/main/java/androidx/camera/core/BaseUseCase.java
new file mode 100644
index 0000000..66f537c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/BaseUseCase.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Log;
+import android.util.Size;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration.Option;
+import androidx.camera.core.UseCaseConfiguration.Builder;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * The use case which all other use cases are built on top of.
+ *
+ * <p>A BaseUseCase provides functionality to map a {@link BaseCamera} to a {@link
+ * SessionConfiguration} and the communication of the active/inactive state to the Camera.
+ */
+public abstract class BaseUseCase {
+    private static final String TAG = "BaseUseCase";
+
+    /**
+     * The set of {@link StateChangeListener} that are currently listening state transitions of this
+     * use case.
+     */
+    private final Set<StateChangeListener> listeners = new HashSet<>();
+
+    /**
+     * A map of camera id and CameraControl. A CameraControl will be attached into the usecase after
+     * usecase is bound to lifecycle. It is used for controlling zoom/focus/flash/triggering Af or
+     * AE.
+     */
+    private final Map<String, CameraControl> attachedCameraControlMap = new HashMap<>();
+
+    /**
+     * A map of the names of the {@link android.hardware.camera2.CameraDevice} to the {@link
+     * SessionConfiguration} that have been attached to this BaseUseCase
+     */
+    private final Map<String, SessionConfiguration> attachedCameraIdToSessionConfigurationMap =
+            new HashMap<>();
+
+    /**
+     * A map of the names of the {@link android.hardware.camera2.CameraDevice} to the surface
+     * resolution that have been attached to this BaseUseCase
+     */
+    private final Map<String, Size> attachedSurfaceResolutionMap = new HashMap<>();
+
+    private State state = State.INACTIVE;
+
+    private UseCaseConfiguration<?> useCaseConfiguration;
+
+    /**
+     * Except for ImageFormat.JPEG or ImageFormat.YUV, other image formats like SurfaceTexture or
+     * MediaCodec classes will be mapped to internal format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED
+     * (0x22) in StreamConfigurationMap.java. 0x22 is also the code for ImageFormat.PRIVATE. But
+     * there is no ImageFormat.PRIVATE supported before Android level 23. There is same internal
+     * code 0x22 for internal corresponding format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED.
+     * Therefore, setting 0x22 as default image format.
+     */
+    private int imageFormat = ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
+
+    /**
+     * Creates a named instance of the use case.
+     *
+     * @param useCaseConfiguration the configuration object used for this use case
+     */
+    protected BaseUseCase(UseCaseConfiguration<?> useCaseConfiguration) {
+        updateUseCaseConfiguration(useCaseConfiguration);
+    }
+
+    /**
+     * Returns a {@link UseCaseConfiguration.Builder} pre-populated with default configuration
+     * options.
+     *
+     * <p>This is used to generate a final configuration by combining the user-supplied
+     * configuration with the default configuration. Subclasses can override this method to provide
+     * the pre-populated builder. If <code>null</code> is returned, then the user-supplied
+     * configuration will be used directly.
+     *
+     * @return A builder pre-populated with use case default options.
+     */
+    @Nullable
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        return null;
+    }
+
+    /**
+     * Updates the stored use case configuration.
+     *
+     * <p>This configuration will be combined with the default configuration that is contained in
+     * the pre-populated builder supplied by {@link #getDefaultBuilder()}, if it exists and the
+     * behavior of {@link #applyDefaults(UseCaseConfiguration, Builder)} is not overridden. Once
+     * this method returns, the combined use case configuration can be retrieved with {@link
+     * #getUseCaseConfiguration()}.
+     *
+     * <p>This method alone will not make any changes to the {@link SessionConfiguration}, it is up
+     * to the use case to decide when to modify the session configuration.
+     *
+     * @param useCaseConfiguration Configuration which will be applied on top of use case defaults,
+     *                             if a default builder is provided by {@link #getDefaultBuilder()}.
+     */
+    protected void updateUseCaseConfiguration(UseCaseConfiguration<?> useCaseConfiguration) {
+        UseCaseConfiguration.Builder<?, ?, ?> defaultBuilder = getDefaultBuilder();
+        if (defaultBuilder == null) {
+            Log.w(
+                    TAG,
+                    "No default configuration available. Relying solely on user-supplied options.");
+            this.useCaseConfiguration = useCaseConfiguration;
+        } else {
+            this.useCaseConfiguration = applyDefaults(useCaseConfiguration, defaultBuilder);
+        }
+    }
+
+    /**
+     * Combines user-supplied configuration with use case default configuration.
+     *
+     * <p>This is called during initialization of the class. Subclassess can override this method to
+     * modify the behavior of combining user-supplied values and default values.
+     *
+     * @param userConfiguration    The user-supplied configuration.
+     * @param defaultConfigBuilder A builder containing use-case default values.
+     * @return The configuration that will be used by this use case.
+     */
+    protected UseCaseConfiguration<?> applyDefaults(
+            UseCaseConfiguration<?> userConfiguration,
+            UseCaseConfiguration.Builder<?, ?, ?> defaultConfigBuilder) {
+
+        // If any options need special handling, this is the place to do it. For now we'll just copy
+        // over all options.
+        for (Option<?> opt : userConfiguration.listOptions()) {
+            @SuppressWarnings("unchecked") // Options/values are being copied directly
+                    Option<Object> objectOpt = (Option<Object>) opt;
+            defaultConfigBuilder.insertOption(
+                    objectOpt, userConfiguration.retrieveOption(objectOpt));
+        }
+
+        @SuppressWarnings(
+                "unchecked") // Since builder is a UseCaseConfiguration.Builder, it should produce a
+                // UseCaseConfiguration
+                UseCaseConfiguration<?> defaultConfig =
+                (UseCaseConfiguration<?>) defaultConfigBuilder.build();
+        return defaultConfig;
+    }
+
+    /**
+     * Get the names of the cameras which are attached to this use case.
+     *
+     * <p>The names will correspond to those of the camera as defined by {@link
+     * android.hardware.camera2.CameraManager}.
+     */
+    Set<String> getAttachedCameraIds() {
+        return attachedCameraIdToSessionConfigurationMap.keySet();
+    }
+
+    /**
+     * Attaches the BaseUseCase to a {@link android.hardware.camera2.CameraDevice} with the
+     * corresponding name.
+     *
+     * @param cameraId The name of the camera as defined by {@link
+     *                 android.hardware.camera2.CameraManager#getCameraIdList()}.
+     */
+    protected void attachToCamera(String cameraId, SessionConfiguration sessionConfiguration) {
+        attachedCameraIdToSessionConfigurationMap.put(cameraId, sessionConfiguration);
+    }
+
+    /**
+     * Add a {@link StateChangeListener}, which listens to this BaseUseCase's active and inactive
+     * transition events.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void addStateChangeListener(StateChangeListener listener) {
+        listeners.add(listener);
+    }
+
+    /**
+     * Attach a CameraControl to this use case.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public final void attachCameraControl(String cameraId, CameraControl cameraControl) {
+        attachedCameraControlMap.put(cameraId, cameraControl);
+        onCameraControlReady(cameraId);
+    }
+
+    /** Detach a CameraControl from this use case. */
+    final void detachCameraControl(String cameraId) {
+        attachedCameraControlMap.remove(cameraId);
+    }
+
+    /**
+     * Remove a {@link StateChangeListener} from listening to this BaseUseCase's active and inactive
+     * transition events.
+     *
+     * <p>If the listener isn't currently listening to the BaseUseCase then this call does nothing.
+     */
+    void removeStateChangeListener(StateChangeListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Get the {@link SessionConfiguration} for the specified camera id.
+     *
+     * @param cameraId the id of the camera as referred to be {@link
+     *                 android.hardware.camera2.CameraManager}
+     * @throws IllegalArgumentException if no camera with the specified cameraId is attached
+     */
+    public SessionConfiguration getSessionConfiguration(String cameraId) {
+        SessionConfiguration sessionConfiguration =
+                attachedCameraIdToSessionConfigurationMap.get(cameraId);
+        if (sessionConfiguration == null) {
+            throw new IllegalArgumentException("Invalid camera: " + cameraId);
+        } else {
+            return sessionConfiguration;
+        }
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that it has
+     * transitioned to an active state.
+     */
+    protected final void notifyActive() {
+        state = State.ACTIVE;
+        notifyState();
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that it has
+     * transitioned to an inactive state.
+     */
+    protected final void notifyInactive() {
+        state = State.INACTIVE;
+        notifyState();
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that it has a
+     * single capture request.
+     */
+    protected final void notifySingleCapture(CaptureRequestConfiguration requestConfiguration) {
+        for (StateChangeListener listener : listeners) {
+            listener.onUseCaseSingleRequest(this, requestConfiguration);
+        }
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that the
+     * settings have been updated.
+     */
+    protected final void notifyUpdated() {
+        for (StateChangeListener listener : listeners) {
+            listener.onUseCaseUpdated(this);
+        }
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that the use
+     * case needs to be completely reset.
+     */
+    protected final void notifyReset() {
+        for (StateChangeListener listener : listeners) {
+            listener.onUseCaseReset(this);
+        }
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase of its current
+     * state.
+     */
+    protected final void notifyState() {
+        switch (state) {
+            case INACTIVE:
+                for (StateChangeListener listener : listeners) {
+                    listener.onUseCaseInactive(this);
+                }
+                break;
+            case ACTIVE:
+                for (StateChangeListener listener : listeners) {
+                    listener.onUseCaseActive(this);
+                }
+                break;
+        }
+    }
+
+    /** Clear out all {@link StateChangeListener} from listening to this BaseUseCase. */
+    @CallSuper
+    protected void clear() {
+        listeners.clear();
+    }
+
+    public String getName() {
+        return useCaseConfiguration.getTargetName("<UnknownUseCase-" + this.hashCode() + ">");
+    }
+
+    /**
+     * Retrieves the configuration used by this use case.
+     *
+     * @return the configuration used by this use case.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public UseCaseConfiguration<?> getUseCaseConfiguration() {
+        return useCaseConfiguration;
+    }
+
+    /**
+     * Retrieves the currently attached surface resolution.
+     *
+     * @param cameraId the camera id for the desired surface.
+     * @return the currently attached surface resolution for the given camera id.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public Size getAttachedSurfaceResolution(String cameraId) {
+        return attachedSurfaceResolutionMap.get(cameraId);
+    }
+
+    /**
+     * Offers suggested resolutions.
+     *
+     * <p>The keys of suggestedResolutionMap should only be cameraIds that are valid for this use
+     * case.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void updateSuggestedResolution(Map<String, Size> suggestedResolutionMap) {
+        Map<String, Size> resolutionMap = onSuggestedResolutionUpdated(suggestedResolutionMap);
+
+        for (Entry<String, Size> entry : resolutionMap.entrySet()) {
+            attachedSurfaceResolutionMap.put(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * Called when binding new use cases via {@link CameraX#bindToLifecycle(LifecycleOwner,
+     * BaseUseCase...)}. Need to override this function to create {@link SessionConfiguration} or
+     * other necessary objects like {@link android.media.ImageReader} depending on the resolution.
+     *
+     * @param suggestedResolutionMap A map of the names of the {@link
+     *                               android.hardware.camera2.CameraDevice} to the suggested
+     *                               resolution that depends on camera
+     *                               device capability and what and how many use cases will be
+     *                               bound.
+     * @return The map with the resolutions that finally used to create the SessionConfiguration to
+     * attach to the camera device.
+     */
+    protected abstract Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap);
+
+    /**
+     * Called when CameraControl is attached into the UseCase. UseCase may need to override this
+     * method to configure the CameraControl here. Ex. Setting correct flash mode by
+     * CameraControl.setFlashMode to enable correct AE mode and flash state.
+     *
+     * @hide
+     */
+    protected void onCameraControlReady(String cameraId) {
+    }
+
+    /**
+     * Retrieves a previously attached {@link CameraControl}.
+     *
+     * @hide
+     */
+    protected CameraControl getCameraControl(String cameraId) {
+        CameraControl cameraControl = attachedCameraControlMap.get(cameraId);
+        if (cameraControl == null) {
+            return CameraControl.defaultEmptyInstance();
+        }
+        return cameraControl;
+    }
+
+    /**
+     * Get image format for the use case.
+     *
+     * @return image format for the use case
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getImageFormat() {
+        return imageFormat;
+    }
+
+    protected void setImageFormat(int imageFormat) {
+        this.imageFormat = imageFormat;
+    }
+
+    enum State {
+        /** Currently waiting for image data. */
+        ACTIVE,
+        /** Currently not waiting for image data. */
+        INACTIVE
+    }
+
+    /**
+     * Listener called when a {@link BaseUseCase} transitions between active/inactive states.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public interface StateChangeListener {
+        /**
+         * Called when a {@link BaseUseCase} becomes active.
+         *
+         * <p>When a BaseUseCase is active it expects that all data producers attached to itself
+         * should start producing data for it to consume. In addition the BaseUseCase will start
+         * producing data that other classes can be consumed.
+         */
+        void onUseCaseActive(BaseUseCase useCase);
+
+        /**
+         * Called when a {@link BaseUseCase} becomes inactive.
+         *
+         * <p>When a BaseUseCase is inactive it no longer expects data to be produced for it. In
+         * addition the BaseUseCase will stop producing data for other classes to consume.
+         */
+        void onUseCaseInactive(BaseUseCase useCase);
+
+        /**
+         * Called when a {@link BaseUseCase} has updated settings.
+         *
+         * <p>When a {@link BaseUseCase} has updated settings, it is expected that the listener will
+         * use these updated settings to reconfigure the listener's own state. A settings update is
+         * orthogonal to the active/inactive state change.
+         */
+        void onUseCaseUpdated(BaseUseCase useCase);
+
+        /**
+         * Called when a {@link BaseUseCase} has updated settings that require complete reset of the
+         * camera.
+         *
+         * <p>Updating certain parameters of the use case require a full reset of the camera. This
+         * includes updating the {@link android.view.Surface} used by the use case.
+         */
+        void onUseCaseReset(BaseUseCase useCase);
+
+        /**
+         * Called when a {@link BaseUseCase} need a single capture request
+         *
+         * @param captureRequestConfiguration used to construct the single capture request
+         */
+        void onUseCaseSingleRequest(
+                BaseUseCase useCase, CaptureRequestConfiguration captureRequestConfiguration);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallback.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallback.java
new file mode 100644
index 0000000..9adb0ca
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallback.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.NonNull;
+
+/**
+ * A callback object for tracking the progress of a capture request submitted to the camera device.
+ *
+ * @hide
+ */
+public abstract class CameraCaptureCallback {
+
+    /**
+     * This method is called when an image capture has fully completed and all the result metadata
+     * is available.
+     *
+     * @param cameraCaptureResult The output metadata from the capture.
+     */
+    public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
+    }
+
+    /**
+     * This method is called instead of {@link #onCaptureCompleted} when the camera device failed to
+     * produce a {@link CameraCaptureResult} for the request.
+     *
+     * @param failure The output failure from the capture, including the failure reason.
+     */
+    public void onCaptureFailed(@NonNull CameraCaptureFailure failure) {
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallbacks.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallbacks.java
new file mode 100644
index 0000000..fab0041
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallbacks.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraCaptureCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraCaptureCallbacks {
+    private CameraCaptureCallbacks() {
+    }
+
+    /** Returns a camera capture callback which does nothing. */
+    public static CameraCaptureCallback createNoOpCallback() {
+        return new NoOpCameraCaptureCallback();
+    }
+
+    /** Returns a camera capture callback which calls a list of other callbacks. */
+    static CameraCaptureCallback createComboCallback(List<CameraCaptureCallback> callbacks) {
+        return new ComboCameraCaptureCallback(callbacks);
+    }
+
+    /** Returns a camera capture callback which calls a list of other callbacks. */
+    public static CameraCaptureCallback createComboCallback(CameraCaptureCallback... callbacks) {
+        return createComboCallback(Arrays.asList(callbacks));
+    }
+
+    static final class NoOpCameraCaptureCallback extends CameraCaptureCallback {
+        @Override
+        public void onCaptureCompleted(CameraCaptureResult cameraCaptureResult) {
+        }
+
+        @Override
+        public void onCaptureFailed(CameraCaptureFailure failure) {
+        }
+    }
+
+    /**
+     * A CameraCaptureCallback which contains a list of CameraCaptureCallback and will propagate
+     * received callback to the list.
+     */
+    public static final class ComboCameraCaptureCallback extends CameraCaptureCallback {
+        private final List<CameraCaptureCallback> callbacks = new ArrayList<>();
+
+        ComboCameraCaptureCallback(List<CameraCaptureCallback> callbacks) {
+            for (CameraCaptureCallback callback : callbacks) {
+                // A no-op callback doesn't do anything, so avoid adding it to the final list.
+                if (!(callback instanceof NoOpCameraCaptureCallback)) {
+                    this.callbacks.add(callback);
+                }
+            }
+        }
+
+        @Override
+        public void onCaptureCompleted(CameraCaptureResult result) {
+            for (CameraCaptureCallback callback : callbacks) {
+                callback.onCaptureCompleted(result);
+            }
+        }
+
+        @Override
+        public void onCaptureFailed(CameraCaptureFailure failure) {
+            for (CameraCaptureCallback callback : callbacks) {
+                callback.onCaptureFailed(failure);
+            }
+        }
+
+        @NonNull
+        public List<CameraCaptureCallback> getCallbacks() {
+            return callbacks;
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureFailure.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureFailure.java
new file mode 100644
index 0000000..66018f6
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureFailure.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/**
+ * A report of failed capture for a single image capture.
+ *
+ * @hide
+ */
+public final class CameraCaptureFailure {
+
+    private final Reason reason;
+
+    /** @hide */
+    public CameraCaptureFailure(Reason reason) {
+        this.reason = reason;
+    }
+
+    /**
+     * Determine why the request was dropped, whether due to an error or to a user action.
+     *
+     * @return int The reason code.
+     * @see CameraCaptureFailure.Reason#ERROR
+     */
+    public Reason getReason() {
+        return reason;
+    }
+
+    /**
+     * The capture result has been dropped this frame only due to an error in the framework.
+     *
+     * @see #getReason()
+     */
+    public enum Reason {
+        ERROR,
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureMetaData.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureMetaData.java
new file mode 100644
index 0000000..490c568
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureMetaData.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/**
+ * This class defines the enumeration constants used for querying the camera capture mode and
+ * results.
+ *
+ * @hide
+ */
+public final class CameraCaptureMetaData {
+
+    /** Auto focus (AF) mode. */
+    public enum AfMode {
+
+        /** AF mode is currently unknown. */
+        UNKNOWN,
+
+        /** The AF routine does not control the lens. */
+        OFF,
+
+        /**
+         * AF is triggered on demand.
+         *
+         * <p>In this mode, the lens does not move unless the auto focus trigger action is called.
+         */
+        ON_MANUAL_AUTO,
+
+        /**
+         * AF is continually scanning.
+         *
+         * <p>In this mode, the AF algorithm modifies the lens position continually to attempt to
+         * provide a constantly-in-focus stream.
+         */
+        ON_CONTINUOUS_AUTO
+    }
+
+    /** Auto focus (AF) state. */
+    public enum AfState {
+
+        /** AF state is currently unknown. */
+        UNKNOWN,
+
+        /** AF is off or not yet has been triggered. */
+        INACTIVE,
+
+        /** AF is performing an AF scan. */
+        SCANNING,
+
+        /** AF currently believes it is in focus. */
+        FOCUSED,
+
+        /** AF believes it is focused correctly and has locked focus. */
+        LOCKED_FOCUSED,
+
+        /** AF has failed to focus and has locked focus. */
+        LOCKED_NOT_FOCUSED
+    }
+
+    /** Auto exposure (AE) state. */
+    public enum AeState {
+
+        /** AE state is currently unknown. */
+        UNKNOWN,
+
+        /** AE is off or has not yet been triggered. */
+        INACTIVE,
+
+        /** AE is performing an AE search. */
+        SEARCHING,
+
+        /**
+         * AE has a good set of control values, but flash needs to be fired for good quality still
+         * capture.
+         */
+        FLASH_REQUIRED,
+
+        /** AE has a good set of control values for the current scene. */
+        CONVERGED,
+
+        /** AE has been locked. */
+        LOCKED
+    }
+
+    /** Auto white balance (AWB) state. */
+    public enum AwbState {
+
+        /** AWB state is currently unknown. */
+        UNKNOWN,
+
+        /** AWB is not in auto mode, or has not yet started metering. */
+        INACTIVE,
+
+        /** AWB is performing AWB metering. */
+        METERING,
+
+        /** AWB has a good set of control values for the current scene. */
+        CONVERGED,
+
+        /** AWB has been locked. */
+        LOCKED
+    }
+
+    /** Flash state. */
+    public enum FlashState {
+
+        /** Flash state is unknown. */
+        UNKNOWN,
+
+        /** Flash is unavailable or not ready to fire. */
+        NONE,
+
+        /** Flash is ready to fire. */
+        READY,
+
+        /** Flash has been fired. */
+        FIRED
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureResult.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureResult.java
new file mode 100644
index 0000000..c31815c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureResult.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureMetaData.FlashState;
+
+/**
+ * The result of a single image capture.
+ *
+ * @hide
+ */
+public interface CameraCaptureResult {
+
+    @NonNull
+    AfMode getAfMode();
+
+    @NonNull
+    AfState getAfState();
+
+    @NonNull
+    AeState getAeState();
+
+    @NonNull
+    AwbState getAwbState();
+
+    @NonNull
+    FlashState getFlashState();
+
+    /** An implementation of CameraCaptureResult which always return default results. */
+    final class EmptyCameraCaptureResult implements CameraCaptureResult {
+
+        public static CameraCaptureResult create() {
+            return new EmptyCameraCaptureResult();
+        }
+
+        @NonNull
+        @Override
+        public AfMode getAfMode() {
+            return AfMode.UNKNOWN;
+        }
+
+        @NonNull
+        @Override
+        public AfState getAfState() {
+            return AfState.UNKNOWN;
+        }
+
+        @NonNull
+        @Override
+        public AeState getAeState() {
+            return AeState.UNKNOWN;
+        }
+
+        @NonNull
+        @Override
+        public AwbState getAwbState() {
+            return AwbState.UNKNOWN;
+        }
+
+        @NonNull
+        @Override
+        public FlashState getFlashState() {
+            return FlashState.UNKNOWN;
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureSessionStateCallbacks.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureSessionStateCallbacks.java
new file mode 100644
index 0000000..2a55ec1
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureSessionStateCallbacks.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraCaptureSession.StateCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraCaptureSessionStateCallbacks {
+    private CameraCaptureSessionStateCallbacks() {
+    }
+
+    /** Returns a session state callback which does nothing. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static CameraCaptureSession.StateCallback createNoOpCallback() {
+        return new NoOpSessionStateCallback();
+    }
+
+    /** Returns a session state callback which calls a list of other callbacks. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static CameraCaptureSession.StateCallback createComboCallback(
+            List<CameraCaptureSession.StateCallback> callbacks) {
+        return new ComboSessionStateCallback(callbacks);
+    }
+
+    /** Returns a session state callback which calls a list of other callbacks. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static CameraCaptureSession.StateCallback createComboCallback(
+            CameraCaptureSession.StateCallback... callbacks) {
+        return createComboCallback(Arrays.asList(callbacks));
+    }
+
+    static final class NoOpSessionStateCallback extends CameraCaptureSession.StateCallback {
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+        }
+
+        @Override
+        public void onActive(CameraCaptureSession session) {
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+        }
+
+        @Override
+        public void onCaptureQueueEmpty(CameraCaptureSession session) {
+        }
+
+        @Override
+        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+        }
+    }
+
+    private static final class ComboSessionStateCallback
+            extends CameraCaptureSession.StateCallback {
+        private final List<CameraCaptureSession.StateCallback> callbacks = new ArrayList<>();
+
+        ComboSessionStateCallback(List<CameraCaptureSession.StateCallback> callbacks) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                // A no-op callback doesn't do anything, so avoid adding it to the final list.
+                if (!(callback instanceof NoOpSessionStateCallback)) {
+                    this.callbacks.add(callback);
+                }
+            }
+        }
+
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                callback.onConfigured(session);
+            }
+        }
+
+        @Override
+        public void onActive(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                callback.onActive(session);
+            }
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                callback.onClosed(session);
+            }
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                callback.onReady(session);
+            }
+        }
+
+        @RequiresApi(api = Build.VERSION_CODES.O)
+        @Override
+        public void onCaptureQueueEmpty(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                callback.onCaptureQueueEmpty(session);
+            }
+        }
+
+        @Override
+        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                callback.onSurfacePrepared(session, surface);
+            }
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : callbacks) {
+                callback.onConfigureFailed(session);
+            }
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraControl.java b/camera/core/src/main/java/androidx/camera/core/CameraControl.java
new file mode 100644
index 0000000..1798de7
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraControl.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.Rect;
+import android.os.Handler;
+
+import androidx.annotation.Nullable;
+
+/**
+ * The CameraControl Interface.
+ *
+ * <p>CameraControl is used for global camera operations like zoom, focus, flash and triggering
+ * AF/AE. To control the global camera status across different UseCases,
+ * getSingleRequestImplOptions() is used to attach the common request parameter to all SINGLE
+ * CaptureRequests and getControlSessionConfiguration() is used to hook a {@link
+ * SessionConfiguration} alongside with other use cases to determine the final sessionConfiguration.
+ * A CameraControl implementation can use getControlSessionConfiguration to modify parameter for
+ * repeating request or add a listener to check the capture result.
+ *
+ * @hide
+ */
+public interface CameraControl {
+    static CameraControl defaultEmptyInstance() {
+        return new CameraControl() {
+            @Override
+            public void setCropRegion(Rect crop) {
+            }
+
+            @Override
+            public void focus(
+                    Rect focus,
+                    Rect metering,
+                    @Nullable OnFocusCompletedListener listener,
+                    @Nullable Handler handler) {
+            }
+
+            @Override
+            public FlashMode getFlashMode() {
+                return null;
+            }
+
+            @Override
+            public void setFlashMode(FlashMode flashMode) {
+            }
+
+            @Override
+            public void enableTorch(boolean torch) {
+            }
+
+            @Override
+            public boolean isTorchOn() {
+                return false;
+            }
+
+            @Override
+            public boolean isFocusLocked() {
+                return false;
+            }
+
+            @Override
+            public void triggerAf() {
+            }
+
+            @Override
+            public void triggerAePrecapture() {
+            }
+
+            @Override
+            public void cancelAfAeTrigger(
+                    boolean cancelAfTrigger, boolean cancelAePrecaptureTrigger) {
+            }
+
+            @Override
+            public SessionConfiguration getControlSessionConfiguration() {
+                return SessionConfiguration.defaultEmptySessionConfiguration();
+            }
+
+            @Override
+            public Configuration getSingleRequestImplOptions() {
+                return OptionsBundle.emptyBundle();
+            }
+        };
+    }
+
+    /**
+     * Set the desired crop region of the sensor to read out for all capture requests.
+     *
+     * <p>This crop region can be used to implement digital zoom. It is applied to every single and
+     * re peating requests.
+     *
+     * @param crop rectangle with dimensions in sensor pixel coordinate.
+     */
+    void setCropRegion(Rect crop);
+
+    /**
+     * Adjusts the camera output according to the properties in some local regions with a callback
+     * called once focus scan has completed.
+     *
+     * <p>The auto-focus (AF), auto-exposure (AE) and auto-whitebalance (AWB) properties will be
+     * recalculated from the local regions.
+     *
+     * @param focus    rectangle with dimensions in sensor coordinate frame for focus
+     * @param metering rectangle with dimensions in sensor coordinate frame for metering
+     * @param listener listener for when focus has completed
+     * @param handler  the handler where the listener will execute.
+     */
+    void focus(
+            Rect focus,
+            Rect metering,
+            @Nullable OnFocusCompletedListener listener,
+            @Nullable Handler handler);
+
+    default void focus(Rect focus, Rect metering) {
+        focus(focus, metering, null, null);
+    }
+
+    FlashMode getFlashMode();
+
+    /**
+     * Sets current flash mode
+     *
+     * @param flashMode the {@link FlashMode}.
+     */
+    void setFlashMode(FlashMode flashMode);
+
+    /**
+     * Enable the torch or disable the torch
+     *
+     * @param torch true to open the torch, false to close it.
+     */
+    void enableTorch(boolean torch);
+
+    /** Returns if current torch is enabled or not. */
+    boolean isTorchOn();
+
+    boolean isFocusLocked();
+
+    /** Performs a AF trigger. */
+    void triggerAf();
+
+    /** Performs a AE Precapture trigger. */
+    void triggerAePrecapture();
+
+    /** Cancel AF trigger AND/OR AE Precapture trigger.* */
+    void cancelAfAeTrigger(boolean cancelAfTrigger, boolean cancelAePrecaptureTrigger);
+
+    /**
+     * Hooks a SessionConfiguration into the final SessionConfiguration ValidatingBuilder.
+     * CameraControl can modify SessionConfiguration to add implementation options or add a listener
+     * to check the capture result.
+     */
+    SessionConfiguration getControlSessionConfiguration();
+
+    /** Attaches the common request implementation options to every SINGLE requests. */
+    Configuration getSingleRequestImplOptions();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraDeviceConfiguration.java b/camera/core/src/main/java/androidx/camera/core/CameraDeviceConfiguration.java
new file mode 100644
index 0000000..6895d2c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceConfiguration.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+
+/**
+ * Configuration containing options for configuring a Camera device.
+ *
+ * <p>This includes options for camera device intrinsics, such as the lens facing direction.
+ */
+public interface CameraDeviceConfiguration extends Configuration.Reader {
+
+    /**
+     * Option: camerax.core.camera.lensFacing
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<LensFacing> OPTION_LENS_FACING =
+            Option.create("camerax.core.camera.lensFacing", CameraX.LensFacing.class);
+
+    /**
+     * Retrieves the lens facing direction for the primary camera to be configured.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    default CameraX.LensFacing getLensFacing(@Nullable LensFacing valueIfMissing) {
+        return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the lens facing direction for the primary camera to be configured.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    default CameraX.LensFacing getLensFacing() {
+        return retrieveOption(OPTION_LENS_FACING);
+    }
+
+    // Option Declarations:
+    // ***********************************************************************************************
+
+    /**
+     * Builder for a {@link CameraDeviceConfiguration}.
+     *
+     * @param <C> The top level configuration which will be generated by {@link #build()}.
+     * @param <B> The top level builder type for which this builder is composed with.
+     */
+    interface Builder<C extends Configuration, B extends Builder<C, B>>
+            extends Configuration.Builder<C, B> {
+
+        /**
+         * Sets the primary camera to be configured based on the direction the lens is facing.
+         *
+         * <p>If multiple cameras exist with equivalent lens facing direction, the first ("primary")
+         * camera for that direction will be chosen.
+         *
+         * @param lensFacing The direction of the camera's lens.
+         * @return the current Builder.
+         */
+        default B setLensFacing(CameraX.LensFacing lensFacing) {
+            getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+            return builder();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraDeviceStateCallbacks.java b/camera/core/src/main/java/androidx/camera/core/CameraDeviceStateCallbacks.java
new file mode 100644
index 0000000..9831938
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceStateCallbacks.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CameraDevice;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Different implementations of {@link CameraDevice.StateCallback}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraDeviceStateCallbacks {
+    private CameraDeviceStateCallbacks() {
+    }
+
+    /** Returns a device state callback which does nothing. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static CameraDevice.StateCallback createNoOpCallback() {
+        return new NoOpDeviceStateCallback();
+    }
+
+    /** Returns a device state callback which calls a list of other callbacks. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static CameraDevice.StateCallback createComboCallback(
+            List<CameraDevice.StateCallback> callbacks) {
+        return new ComboDeviceStateCallback(callbacks);
+    }
+
+    /** Returns a device state callback which calls a list of other callbacks. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static CameraDevice.StateCallback createComboCallback(
+            CameraDevice.StateCallback... callbacks) {
+        return createComboCallback(Arrays.asList(callbacks));
+    }
+
+    static final class NoOpDeviceStateCallback extends CameraDevice.StateCallback {
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+        }
+
+        @Override
+        public void onClosed(CameraDevice cameraDevice) {
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+        }
+    }
+
+    private static final class ComboDeviceStateCallback extends CameraDevice.StateCallback {
+        private final List<CameraDevice.StateCallback> callbacks = new ArrayList<>();
+
+        ComboDeviceStateCallback(List<CameraDevice.StateCallback> callbacks) {
+            for (CameraDevice.StateCallback callback : callbacks) {
+                // A no-op callback doesn't do anything, so avoid adding it to the final list.
+                if (!(callback instanceof NoOpDeviceStateCallback)) {
+                    this.callbacks.add(callback);
+                }
+            }
+        }
+
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+            for (CameraDevice.StateCallback callback : callbacks) {
+                callback.onOpened(cameraDevice);
+            }
+        }
+
+        @Override
+        public void onClosed(CameraDevice cameraDevice) {
+            for (CameraDevice.StateCallback callback : callbacks) {
+                callback.onClosed(cameraDevice);
+            }
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            for (CameraDevice.StateCallback callback : callbacks) {
+                callback.onDisconnected(cameraDevice);
+            }
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+            for (CameraDevice.StateCallback callback : callbacks) {
+                callback.onError(cameraDevice, error);
+            }
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraDeviceSurfaceManager.java b/camera/core/src/main/java/androidx/camera/core/CameraDeviceSurfaceManager.java
new file mode 100644
index 0000000..ea2e8a0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceSurfaceManager.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Camera device manager to provide the guaranteed supported stream capabilities related info for
+ * all camera devices
+ *
+ * @hide
+ */
+public interface CameraDeviceSurfaceManager {
+    /**
+     * Check whether the input surface configuration list is under the capability of any combination
+     * of this object.
+     *
+     * @param cameraId                 the camera id of the camera device to be compared
+     * @param surfaceConfigurationList the surface configuration list to be compared
+     * @return the check result that whether it could be supported
+     */
+    boolean checkSupported(String cameraId, List<SurfaceConfiguration> surfaceConfigurationList);
+
+    /**
+     * Transform to a SurfaceConfiguration object with cameraId, image format and size info
+     *
+     * @param cameraId    the camera id of the camera device to transform the object
+     * @param imageFormat the image format info for the surface configuration object
+     * @param size        the size info for the surface configuration object
+     * @return new {@link SurfaceConfiguration} object
+     */
+    SurfaceConfiguration transformSurfaceConfiguration(String cameraId, int imageFormat, Size size);
+
+    /**
+     * Get max supported output size for specific camera device and image format
+     *
+     * @param cameraId    the camera Id
+     * @param imageFormat the image format info
+     * @return the max supported output size for the image format
+     */
+    @Nullable
+    Size getMaxOutputSize(String cameraId, int imageFormat);
+
+    /**
+     * Retrieves a map of suggested resolutions for the given list of use cases.
+     *
+     * @param cameraId         the camera id of the camera device used by the use cases
+     * @param originalUseCases list of use cases with existing surfaces
+     * @param newUseCases      list of new use cases
+     * @return map of suggested resolutions for given use cases
+     */
+    Map<BaseUseCase, Size> getSuggestedResolutions(
+            String cameraId, List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases);
+
+    /**
+     * Retrieves the preview size, choosing the smaller of the display size and 1080P.
+     *
+     * @return the size used for the on screen preview
+     */
+    Size getPreviewSize();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraFactory.java b/camera/core/src/main/java/androidx/camera/core/CameraFactory.java
new file mode 100644
index 0000000..21767af
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Set;
+
+/**
+ * The factory class that creates {@link BaseCamera} instances.
+ *
+ * @hide
+ */
+public interface CameraFactory {
+
+    BaseCamera getCamera(String cameraId);
+
+    Set<String> getAvailableCameraIds() throws CameraInfoUnavailableException;
+
+    @Nullable
+    String cameraIdForLensFacing(LensFacing lensFacing) throws CameraInfoUnavailableException;
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/core/src/main/java/androidx/camera/core/CameraInfo.java
new file mode 100644
index 0000000..f7f6a4d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/**
+ * An interface for retrieving camera information.
+ *
+ * <p>Contains methods for retrieving characteristics for a specific camera.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface CameraInfo {
+
+    /**
+     * Returns the LensFacing of this camera.
+     *
+     * @return One of {@link LensFacing#FRONT}, {@link LensFacing#BACK}, or <code>null</code> if the
+     * LensFacing does not fall into one of these two categories.
+     */
+    // TODO(b/122975195): Remove @Nullable and null return type once we have a LensFacing type which
+    // can be used to represent non-BACK or FRONT facing lenses.
+    @Nullable
+    LensFacing getLensFacing();
+
+    /**
+     * Returns the sensor rotation, in degrees, relative to the device's "natural" rotation.
+     *
+     * @return The sensor orientation in degrees.
+     * @see Surface#ROTATION_0, the natural orientation of the device.
+     */
+    default int getSensorRotationDegrees() {
+        return getSensorRotationDegrees(Surface.ROTATION_0);
+    }
+
+    /**
+     * Returns the sensor rotation, in degrees, relative to the given rotation value.
+     *
+     * <p>Valid values for the relative rotation are {@link Surface#ROTATION_0}, {@link
+     * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+     *
+     * @param relativeRotation The rotation relative to which the output will be calculated.
+     * @return The sensor orientation in degrees.
+     */
+    int getSensorRotationDegrees(@RotationValue int relativeRotation);
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraInfoUnavailableException.java b/camera/core/src/main/java/androidx/camera/core/CameraInfoUnavailableException.java
new file mode 100644
index 0000000..f3f4cc0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraInfoUnavailableException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** An exception thrown when unable to retrieve information about a camera. */
+public final class CameraInfoUnavailableException extends Exception {
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraInfoUnavailableException(String s, Throwable e) {
+        super(s, e);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraInfoUnavailableException(String s) {
+        super(s);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraOrientationUtil.java b/camera/core/src/main/java/androidx/camera/core/CameraOrientationUtil.java
new file mode 100644
index 0000000..2b54594
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraOrientationUtil.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/**
+ * Contains utility methods related to camera orientation.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraOrientationUtil {
+    private static final String TAG = "CameraOrientationUtil";
+    private static final boolean DEBUG = false;
+
+    // Do not allow instantiation
+    private CameraOrientationUtil() {
+    }
+
+    /**
+     * Calculates the delta between a source rotation and destination rotation.
+     *
+     * <p>A typical use of this method would be calculating the angular difference between the
+     * display orientation (destRotationDegrees) and camera sensor orientation
+     * (sourceRotationDegrees).
+     *
+     * @param destRotationDegrees   The destination rotation relative to the device's natural
+     *                              rotation.
+     * @param sourceRotationDegrees The source rotation relative to the device's natural rotation.
+     * @param isOppositeFacing      Whether the source and destination planes are facing opposite
+     *                              directions.
+     */
+    public static int getRelativeImageRotation(
+            int destRotationDegrees, int sourceRotationDegrees, boolean isOppositeFacing) {
+        int result;
+        if (isOppositeFacing) {
+            result = (sourceRotationDegrees - destRotationDegrees + 360) % 360;
+        } else {
+            result = (sourceRotationDegrees + destRotationDegrees) % 360;
+        }
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    String.format(
+                            "getRelativeImageRotation: destRotationDegrees=%s, "
+                                    + "sourceRotationDegrees=%s, isOppositeFacing=%s, "
+                                    + "result=%s",
+                            destRotationDegrees, sourceRotationDegrees, isOppositeFacing, result));
+        }
+        return result;
+    }
+
+    /**
+     * Converts rotation values enumerated in {@link Surface} to their equivalent in degrees.
+     *
+     * <p>Valid values for the relative rotation are {@link Surface#ROTATION_0}, {@link
+     * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+     *
+     * @param rotationEnum One of the enumerated rotation values from {@link Surface}.
+     * @return The equivalent rotation value in degrees.
+     * @throws IllegalArgumentException If the provided rotation enum is not one of those defined in
+     *                                  {@link Surface}.
+     */
+    public static int surfaceRotationToDegrees(@RotationValue int rotationEnum) {
+        int rotationDegrees;
+        switch (rotationEnum) {
+            case Surface.ROTATION_0:
+                rotationDegrees = 0;
+                break;
+            case Surface.ROTATION_90:
+                rotationDegrees = 90;
+                break;
+            case Surface.ROTATION_180:
+                rotationDegrees = 180;
+                break;
+            case Surface.ROTATION_270:
+                rotationDegrees = 270;
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported surface rotation: " + rotationEnum);
+        }
+
+        return rotationDegrees;
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraRepository.java b/camera/core/src/main/java/androidx/camera/core/CameraRepository.java
new file mode 100644
index 0000000..04ee948
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraRepository.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A collection of {@link BaseCamera} instances.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraRepository implements UseCaseGroup.StateChangeListener {
+    private static final String TAG = "CameraRepository";
+
+    private final Object camerasLock = new Object();
+
+    @GuardedBy("camerasLock")
+    private final Map<String, BaseCamera> cameras = new HashMap<>();
+
+    /**
+     * Initializes the repository from a {@link Context}.
+     *
+     * <p>All cameras queried from the {@link CameraFactory} will be added to the repository.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void init(CameraFactory cameraFactory) {
+        synchronized (camerasLock) {
+            try {
+                Set<String> camerasList = cameraFactory.getAvailableCameraIds();
+                for (String id : camerasList) {
+                    Log.d(TAG, "Added camera: " + id);
+                    cameras.put(id, cameraFactory.getCamera(id));
+                }
+            } catch (Exception e) {
+                throw new IllegalStateException("Unable to enumerate cameras", e);
+            }
+        }
+    }
+
+    /**
+     * Gets a {@link BaseCamera} for the given id.
+     *
+     * @param cameraId id for the camera
+     * @return a {@link BaseCamera} paired to this id
+     * @throws IllegalArgumentException if there is no camera paired with the id
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public BaseCamera getCamera(String cameraId) {
+        synchronized (camerasLock) {
+            BaseCamera camera = cameras.get(cameraId);
+
+            if (camera == null) {
+                throw new IllegalArgumentException("Invalid camera: " + cameraId);
+            }
+
+            return camera;
+        }
+    }
+
+    /**
+     * Gets the set of all camera ids.
+     *
+     * @return set of all camera ids
+     */
+    Set<String> getCameraIds() {
+        synchronized (camerasLock) {
+            return Collections.unmodifiableSet(cameras.keySet());
+        }
+    }
+
+    /**
+     * Attaches all the use cases in the {@link UseCaseGroup} and opens all the associated cameras.
+     *
+     * <p>This will start streaming data to the uses cases which are also online.
+     */
+    @Override
+    public void onGroupActive(UseCaseGroup useCaseGroup) {
+        synchronized (camerasLock) {
+            Map<String, Set<BaseUseCase>> cameraIdToUseCaseMap =
+                    useCaseGroup.getCameraIdToUseCaseMap();
+            for (Map.Entry<String, Set<BaseUseCase>> cameraUseCaseEntry :
+                    cameraIdToUseCaseMap.entrySet()) {
+                BaseCamera camera = getCamera(cameraUseCaseEntry.getKey());
+                attachUseCasesToCamera(camera, cameraUseCaseEntry.getValue());
+            }
+        }
+    }
+
+    /** Attaches a set of use cases to a camera. */
+    @GuardedBy("camerasLock")
+    private void attachUseCasesToCamera(BaseCamera camera, Set<BaseUseCase> baseUseCases) {
+        camera.addOnlineUseCase(baseUseCases);
+    }
+
+    /**
+     * Detaches all the use cases in the {@link UseCaseGroup} and closes the camera with no attached
+     * use cases.
+     */
+    @Override
+    public void onGroupInactive(UseCaseGroup useCaseGroup) {
+        synchronized (camerasLock) {
+            Map<String, Set<BaseUseCase>> cameraIdToUseCaseMap =
+                    useCaseGroup.getCameraIdToUseCaseMap();
+            for (Map.Entry<String, Set<BaseUseCase>> cameraUseCaseEntry :
+                    cameraIdToUseCaseMap.entrySet()) {
+                BaseCamera camera = getCamera(cameraUseCaseEntry.getKey());
+                detachUseCasesFromCamera(camera, cameraUseCaseEntry.getValue());
+            }
+        }
+    }
+
+    /** Detaches a set of use cases from a camera. */
+    @GuardedBy("camerasLock")
+    private void detachUseCasesFromCamera(BaseCamera camera, Set<BaseUseCase> baseUseCases) {
+        camera.removeOnlineUseCase(baseUseCases);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraX.java b/camera/core/src/main/java/androidx/camera/core/CameraX.java
new file mode 100644
index 0000000..6004119
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraX.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Size;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Main interface for accessing CameraX library.
+ *
+ * <p>This is a singleton class that is responsible for managing the set of {@link BaseCamera}
+ * instances and {@link BaseUseCase} instances that exist. A {@link BaseUseCase} is bound to {@link
+ * LifecycleOwner} so that the lifecycle is used to control the use case. There are 3 distinct sets
+ * lifecycle states to be aware of.
+ *
+ * <p>When the lifecycle is in the STARTED or RESUMED states the cameras are opened asynchronously
+ * and made ready for capturing. Data capture starts when triggered by the bound {@link
+ * BaseUseCase}.
+ *
+ * <p>When the lifecycle is in the CREATED state any cameras with no {@link BaseUseCase} attached
+ * will be closed asynchronously.
+ *
+ * <p>When the lifecycle transitions to the DESTROYED state the {@link BaseUseCase} will be unbound.
+ * A {@link #bindToLifecycle(LifecycleOwner, BaseUseCase...)} when the lifecycle is already in the
+ * DESTROYED state will fail. A call to {@link #bindToLifecycle(LifecycleOwner, BaseUseCase...)}
+ * will need to be made with another lifecycle to rebind the {@link BaseUseCase} that has been
+ * unbound.
+ *
+ * <pre>{@code
+ * public void setup() {
+ *   // Initialize UseCase
+ *   useCase = ...;
+ *
+ *   // UseCase binding event
+ *   CameraX.bindToLifecycle(lifecycleOwner, useCase);
+ *
+ *   // Make calls on useCase
+ * }
+ *
+ * public void operateOnUseCase() {
+ *   if (CameraX.isBound(useCase)) {
+ *     // Make calls on useCase
+ *   }
+ * }
+ *
+ * public void prematureTearDown() {
+ *   // Not required, but only if we want to remove it before the lifecycle automatically removes
+ *   // the use case
+ *   CameraX.unbind(useCase);
+ * }
+ * }</pre>
+ *
+ * <p>All operations on a use case, including binding and unbinding, should be done on the main
+ * thread, because lifecycle events are triggered on main thread. By only accessing the use case on
+ * the main thread it is a guaranteed that the use case will not be unbound in the middle of a
+ * method call.
+ */
+@MainThread
+public final class CameraX {
+
+    private static final CameraX INSTANCE = new CameraX();
+    private final CameraRepository cameraRepository = new CameraRepository();
+    private final AtomicBoolean initialized = new AtomicBoolean(false);
+    private final UseCaseGroupRepository useCaseGroupRepository = new UseCaseGroupRepository();
+    private final ErrorHandler errorHandler = new ErrorHandler();
+    private CameraFactory cameraFactory;
+    private CameraDeviceSurfaceManager surfaceManager;
+    private UseCaseConfigurationFactory defaultConfigFactory;
+    /** Prevents construction. */
+    private CameraX() {
+    }
+
+    /**
+     * Binds the collection of {@link BaseUseCase} to a {@link LifecycleOwner}.
+     *
+     * <p>If the lifecycleOwner contains a {@link android.arch.lifecycle.Lifecycle} that is already
+     * in the STARTED state or greater than the created use cases will attach to the cameras and
+     * trigger the appropriate notifications. This will generally cause a temporary glitch in the
+     * camera as part of the reset process. This will also help to calculate suggested resolutions
+     * depending on the use cases bound to the {@link android.arch.lifecycle.Lifecycle}. If the use
+     * cases are bound separately, it will find the supported resolution with the priority depending
+     * on the binding sequence. If the use cases are bound with a single call, it will find the
+     * supported resolution with the priority in sequence of ImageCaptureUseCase,
+     * VideoCaptureUseCase, ViewFinderUseCase and then ImageAnalysisUseCase. What resolutions can be
+     * supported will depend on the camera device hardware level that there are some default
+     * guaranteed resolutions listed in {@link
+     * android.hardware.camera2.CameraDevice#createCaptureSession}.
+     *
+     * @param lifecycleOwner The lifecycleOwner which controls the lifecycle transitions of the use
+     *                       cases.
+     * @param useCases       The use cases to bind to a lifecycle.
+     * @throws IllegalArgumentException If the use case has already been bound to another lifecycle.
+     */
+    public static void bindToLifecycle(LifecycleOwner lifecycleOwner, BaseUseCase... useCases) {
+        UseCaseGroupLifecycleController useCaseGroupLifecycleController =
+                INSTANCE.getOrCreateUseCaseGroup(lifecycleOwner);
+        UseCaseGroup useCaseGroupToBind = useCaseGroupLifecycleController.getUseCaseGroup();
+
+        Collection<UseCaseGroupLifecycleController> controllers =
+                INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+        for (BaseUseCase useCase : useCases) {
+            for (UseCaseGroupLifecycleController controller : controllers) {
+                UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
+                if (useCaseGroup.contains(useCase) && useCaseGroup != useCaseGroupToBind) {
+                    throw new IllegalStateException(
+                            String.format(
+                                    "Use case %s already bound to a different lifecycle.",
+                                    useCase));
+                }
+            }
+        }
+
+        calculateSuggestedResolutions(useCases);
+
+        for (BaseUseCase useCase : useCases) {
+            useCaseGroupToBind.addUseCase(useCase);
+            for (String cameraId : useCase.getAttachedCameraIds()) {
+                attach(cameraId, useCase);
+            }
+        }
+
+        useCaseGroupLifecycleController.notifyState();
+    }
+
+    /**
+     * Returns true if the {@link BaseUseCase} is bound to a lifecycle. Otherwise returns false.
+     *
+     * <p>It is not strictly necessary to check if a use case is bound or not. As long as the
+     * lifecycle it was bound to has not entered a DESTROYED state or if it hasn't been unbound by
+     * {@link #unbind(BaseUseCase...)} or {@link #unbindAll()} then the use case will remain bound.
+     * A use case will not be unbound in the middle of a method call as long as it is running on the
+     * main thread. This is because a lifecycle events will only automatically triggered on the main
+     * thread.
+     */
+    public static boolean isBound(BaseUseCase useCase) {
+        Collection<UseCaseGroupLifecycleController> controllers =
+                INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+
+        for (UseCaseGroupLifecycleController controller : controllers) {
+            UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
+            if (useCaseGroup.contains(useCase)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Unbinds all specified use cases from the lifecycle and removes them from CameraX.
+     *
+     * <p>This will initiate a close of every open camera which has zero {@link BaseUseCase}
+     * associated with it at the end of this call.
+     *
+     * <p>If a use case in the argument list is not bound, then then it is simply ignored.
+     *
+     * @param useCases The collection of use cases to remove.
+     */
+    public static void unbind(BaseUseCase... useCases) {
+        Collection<UseCaseGroupLifecycleController> useCaseGroups =
+                INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+
+        for (BaseUseCase useCase : useCases) {
+            for (UseCaseGroupLifecycleController useCaseGroupLifecycleController : useCaseGroups) {
+                UseCaseGroup useCaseGroup = useCaseGroupLifecycleController.getUseCaseGroup();
+                if (useCaseGroup.removeUseCase(useCase)) {
+                    for (String cameraId : useCase.getAttachedCameraIds()) {
+                        detach(cameraId, useCase);
+                    }
+
+                    useCase.clear();
+                }
+            }
+        }
+    }
+
+    /**
+     * Unbinds all use cases from the lifecycle and removes them from CameraX.
+     *
+     * <p>This will initiate a close of every currently open camera.
+     */
+    public static void unbindAll() {
+        Collection<UseCaseGroupLifecycleController> useCaseGroups =
+                INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+
+        List<BaseUseCase> useCases = new ArrayList<>();
+        for (UseCaseGroupLifecycleController useCaseGroupLifecycleController : useCaseGroups) {
+            UseCaseGroup useCaseGroup = useCaseGroupLifecycleController.getUseCaseGroup();
+            useCases.addAll(useCaseGroup.getUseCases());
+        }
+
+        unbind(useCases.toArray(new BaseUseCase[0]));
+    }
+
+    /**
+     * Returns the camera id for a camera with the specified lens facing.
+     *
+     * <p>This only gives the first (primary) camera found with the specified facing.
+     *
+     * @param lensFacing the lens facing of the camera
+     * @return the cameraId if camera exists or {@code null} if no camera with specified facing
+     * exists
+     * @throws CameraInfoUnavailableException if unable to access cameras, perhaps due to
+     *                                        insufficient permissions.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public static String getCameraWithLensFacing(LensFacing lensFacing)
+            throws CameraInfoUnavailableException {
+        return INSTANCE.getCameraFactory().cameraIdForLensFacing(lensFacing);
+    }
+
+    /**
+     * Returns the camera info for the camera with the given camera id.
+     *
+     * @param cameraId the internal id of the camera
+     * @return the camera info if it can be retrieved for the given id.
+     * @throws CameraInfoUnavailableException if unable to access cameras, perhaps due to
+     *                                        insufficient permissions.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public static CameraInfo getCameraInfo(String cameraId) throws CameraInfoUnavailableException {
+        return INSTANCE.getCameraRepository().getCamera(cameraId).getCameraInfo();
+    }
+
+    /**
+     * Returns the {@link CameraDeviceSurfaceManager} which can be used to query for valid surface
+     * configurations.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static CameraDeviceSurfaceManager getSurfaceManager() {
+        return INSTANCE.getCameraDeviceSurfaceManager();
+    }
+
+    /**
+     * Returns the default configuration for the given use case configuration type.
+     *
+     * <p>The options contained in this configuration serve as fallbacks if they are not included in
+     * the user-provided configuration used to create a use case.
+     *
+     * @param configType the configuration type
+     * @return the default configuration for the given configuration type
+     * @throws IllegalStateException if Camerax has not yet been initialized.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public static <C extends UseCaseConfiguration<?>> C getDefaultUseCaseConfiguration(
+            Class<C> configType) {
+        return INSTANCE.getDefaultConfigFactory().getConfiguration(configType);
+    }
+
+    /**
+     * Sets an {@link ErrorListener} which will get called anytime a CameraX specific error is
+     * encountered.
+     *
+     * @param errorListener the listener which will get all the error messages. If this is set to
+     *                      {@code null} then the default error listener will be set.
+     * @param handler       the handler for the thread to run the error handling on. If this is
+     *                      set to
+     *                      {@code null} then it will default to run on the main thread.
+     */
+    public static void setErrorListener(ErrorListener errorListener, Handler handler) {
+        INSTANCE.errorHandler.setErrorListener(errorListener, handler);
+    }
+
+    /**
+     * Posts an error which can be handled by the {@link ErrorListener}.
+     *
+     * @param errorCode the type of error that occurred
+     * @param message   the associated message with more details of the error
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static void postError(ErrorCode errorCode, String message) {
+        INSTANCE.errorHandler.postError(errorCode, message);
+    }
+
+    /**
+     * Initializes CameraX with the given context and application configuration.
+     *
+     * <p>The context enables CameraX to obtain access to necessary services, including the camera
+     * service. For example, the context can be provided by the application.
+     *
+     * @param context          to attach
+     * @param appConfiguration configuration options for this application session.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static void init(Context context, AppConfiguration appConfiguration) {
+        INSTANCE.initInternal(context, appConfiguration);
+    }
+
+    /**
+     * Returns true if CameraX is initialized.
+     *
+     * <p>Any previous call to {@link #init(Context, AppConfiguration)} would have initialized
+     * CameraX.
+     */
+    static boolean isInitialized() {
+        return INSTANCE.initialized.get();
+    }
+
+    /**
+     * Registers the callbacks for the {@link BaseCamera} to the {@link BaseUseCase}.
+     *
+     * @param cameraId the id for the {@link BaseCamera}
+     * @param useCase  the use case to register the callback for
+     */
+    private static void attach(String cameraId, BaseUseCase useCase) {
+        BaseCamera camera = INSTANCE.getCameraRepository().getCamera(cameraId);
+        if (camera == null) {
+            throw new IllegalArgumentException("Invalid camera: " + cameraId);
+        } else {
+            useCase.addStateChangeListener(camera);
+            useCase.attachCameraControl(cameraId, camera.getCameraControl());
+        }
+    }
+
+    /**
+     * Removes the callbacks registered by the {@link BaseCamera} to the {@link BaseUseCase}.
+     *
+     * @param cameraId the id for the {@link BaseCamera}
+     * @param useCase  the use case to remove the callback from
+     */
+    private static void detach(String cameraId, BaseUseCase useCase) {
+        BaseCamera camera = INSTANCE.getCameraRepository().getCamera(cameraId);
+        if (camera == null) {
+            throw new IllegalArgumentException("Invalid camera: " + cameraId);
+        } else {
+            useCase.notifyInactive();
+            useCase.removeStateChangeListener(camera);
+            camera.removeOnlineUseCase(Collections.singletonList(useCase));
+            useCase.detachCameraControl(cameraId);
+        }
+    }
+
+    private static void calculateSuggestedResolutions(BaseUseCase... useCases) {
+        Collection<UseCaseGroupLifecycleController> controllers =
+                INSTANCE.useCaseGroupRepository.getUseCaseGroups();
+        Map<String, List<BaseUseCase>> originalCameraIdUseCaseMap = new HashMap<>();
+        Map<String, List<BaseUseCase>> newCameraIdUseCaseMap = new HashMap<>();
+
+        // Collect original use cases for different camera devices
+        for (UseCaseGroupLifecycleController controller : controllers) {
+            UseCaseGroup useCaseGroup = controller.getUseCaseGroup();
+            for (BaseUseCase useCase : useCaseGroup.getUseCases()) {
+                for (String cameraId : useCase.getAttachedCameraIds()) {
+                    List<BaseUseCase> useCaseList = originalCameraIdUseCaseMap.get(cameraId);
+                    if (useCaseList == null) {
+                        useCaseList = new ArrayList<>();
+                        originalCameraIdUseCaseMap.put(cameraId, useCaseList);
+                    }
+                    useCaseList.add(useCase);
+                }
+            }
+        }
+
+        // Collect new use cases for different camera devices
+        for (BaseUseCase useCase : useCases) {
+            String cameraId = null;
+            LensFacing lensFacing =
+                    useCase.getUseCaseConfiguration()
+                            .retrieveOption(CameraDeviceConfiguration.OPTION_LENS_FACING);
+            try {
+                cameraId = getCameraWithLensFacing(lensFacing);
+            } catch (Exception e) {
+                throw new IllegalArgumentException("Invalid camera lens facing: " + lensFacing, e);
+            }
+
+            List<BaseUseCase> useCaseList = newCameraIdUseCaseMap.get(cameraId);
+            if (useCaseList == null) {
+                useCaseList = new ArrayList<>();
+                newCameraIdUseCaseMap.put(cameraId, useCaseList);
+            }
+            useCaseList.add(useCase);
+        }
+
+        // Get suggested resolutions and update the use case session configuration
+        for (String cameraId : newCameraIdUseCaseMap.keySet()) {
+            Map<BaseUseCase, Size> suggestResolutionsMap =
+                    getSurfaceManager()
+                            .getSuggestedResolutions(
+                                    cameraId,
+                                    originalCameraIdUseCaseMap.get(cameraId),
+                                    newCameraIdUseCaseMap.get(cameraId));
+
+            for (BaseUseCase useCase : useCases) {
+                Size resolution = suggestResolutionsMap.get(useCase);
+                Map<String, Size> suggestedCameraSurfaceResolutionMap = new HashMap<>();
+                suggestedCameraSurfaceResolutionMap.put(cameraId, resolution);
+                useCase.updateSuggestedResolution(suggestedCameraSurfaceResolutionMap);
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link CameraFactory} instance.
+     *
+     * @throws IllegalStateException if the {@link CameraFactory} has not been set, due to being
+     *                               uninitialized.
+     */
+    private CameraFactory getCameraFactory() {
+        if (cameraFactory == null) {
+            throw new IllegalStateException("CameraX not initialized yet.");
+        }
+
+        return cameraFactory;
+    }
+
+    /**
+     * Returns the {@link CameraDeviceSurfaceManager} instance.
+     *
+     * @throws IllegalStateException if the {@link CameraDeviceSurfaceManager} has not been set, due
+     *                               to being uninitialized.
+     */
+    private CameraDeviceSurfaceManager getCameraDeviceSurfaceManager() {
+        if (surfaceManager == null) {
+            throw new IllegalStateException("CameraX not initialized yet.");
+        }
+
+        return surfaceManager;
+    }
+
+    private UseCaseConfigurationFactory getDefaultConfigFactory() {
+        if (defaultConfigFactory == null) {
+            throw new IllegalStateException("CameraX not initialized yet.");
+        }
+
+        return defaultConfigFactory;
+    }
+
+    @SuppressWarnings("unused") // Context will be used in a future change
+    private void initInternal(Context context, AppConfiguration appConfiguration) {
+        if (initialized.getAndSet(true)) {
+            return;
+        }
+
+        cameraFactory = appConfiguration.getCameraFactory(null);
+        if (cameraFactory == null) {
+            throw new IllegalStateException(
+                    "Invalid app configuration provided. Missing CameraFactory.");
+        }
+
+        surfaceManager = appConfiguration.getDeviceSurfaceManager(null);
+        if (surfaceManager == null) {
+            throw new IllegalStateException(
+                    "Invalid app configuration provided. Missing CameraDeviceSurfaceManager.");
+        }
+
+        defaultConfigFactory = appConfiguration.getUseCaseConfigRepository(null);
+        if (defaultConfigFactory == null) {
+            throw new IllegalStateException(
+                    "Invalid app configuration provided. Missing UseCaseConfigurationFactory.");
+        }
+
+        cameraRepository.init(cameraFactory);
+    }
+
+    private UseCaseGroupLifecycleController getOrCreateUseCaseGroup(LifecycleOwner lifecycleOwner) {
+        return useCaseGroupRepository.getOrCreateUseCaseGroup(
+                lifecycleOwner, useCaseGroup -> useCaseGroup.setListener(cameraRepository));
+    }
+
+    private CameraRepository getCameraRepository() {
+        return cameraRepository;
+    }
+
+    /** The types of error states that can occur. */
+    public enum ErrorCode {
+        /** The camera has moved into an unexpected state from which it can not recover from. */
+        CAMERA_STATE_INCONSISTENT,
+        /** A {@link BaseUseCase} has encountered an error from which it can not recover from. */
+        USE_CASE_ERROR
+    }
+
+    /** The direction the camera faces relative to device screen. */
+    public enum LensFacing {
+        /** A camera on the device facing the same direction as the device's screen. */
+        FRONT,
+        /** A camera on the device facing the opposite direction as the device's screen. */
+        BACK
+    }
+
+    /** Listener called whenever an error condition occurs within CameraX. */
+    public interface ErrorListener {
+
+        /**
+         * Called whenever an error occurs within CameraX.
+         *
+         * @param error   the type of error that occurred
+         * @param message detailed message of the error condition
+         */
+        void onError(ErrorCode error, String message);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraXThreads.java b/camera/core/src/main/java/androidx/camera/core/CameraXThreads.java
new file mode 100644
index 0000000..8ca60d8
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraXThreads.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * Static tag for creating CameraX threads. TODO(b/115747543): Remove this class when migration from
+ * threads to executors is complete.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraXThreads {
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final String TAG = "CameraX-";
+
+    private CameraXThreads() {
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureRequestConfiguration.java b/camera/core/src/main/java/androidx/camera/core/CaptureRequestConfiguration.java
new file mode 100644
index 0000000..7b9fe15
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureRequestConfiguration.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Configurations needed for a capture request.
+ *
+ * <p>The CaptureRequestConfiguration contains all the {@link android.hardware.camera2} parameters
+ * that are required to issue a {@link CaptureRequest}.
+ *
+ * @hide
+ */
+public final class CaptureRequestConfiguration {
+
+    /** The set of {@link Surface} that data from the camera will be put into. */
+    final List<DeferrableSurface> surfaces;
+
+    /** The parameters used to configure the {@link CaptureRequest}. */
+    final Map<Key<?>, CaptureRequestParameter<?>> captureRequestParameters;
+
+    final Configuration implementationOptions;
+
+    /**
+     * The templates used for configuring a {@link CaptureRequest}. This must match the constants
+     * defined by {@link CameraDevice}
+     */
+    final int templateType;
+
+    /** The camera capture callback for a {@link CameraCaptureSession}. */
+    final CameraCaptureCallback cameraCaptureCallback;
+
+    /** True if this capture request needs a repeating surface */
+    private final boolean useRepeatingSurface;
+
+    /**
+     * Private constructor for a CaptureRequestConfiguration.
+     *
+     * <p>In practice, the {@link CaptureRequestConfiguration.Builder} will be used to construct a
+     * CaptureRequestConfiguration.
+     *
+     * @param surfaces                 The set of {@link Surface} where data will be put into.
+     * @param captureRequestParameters The parameters used to configure the {@link CaptureRequest}.
+     * @param implementationOptions    The generic parameters to be passed to the {@link BaseCamera}
+     *                                 class.
+     * @param templateType             The template for parameters of the CaptureRequest. This
+     *                                 must match the
+     *                                 constants defined by {@link CameraDevice}.
+     * @param cameraCaptureCallback    The camera capture callback.
+     */
+    CaptureRequestConfiguration(
+            List<DeferrableSurface> surfaces,
+            Map<Key<?>, CaptureRequestParameter<?>> captureRequestParameters,
+            Configuration implementationOptions,
+            int templateType,
+            CameraCaptureCallback cameraCaptureCallback,
+            boolean useRepeatingSurface) {
+        this.surfaces = surfaces;
+        this.captureRequestParameters = captureRequestParameters;
+        this.implementationOptions = implementationOptions;
+        this.templateType = templateType;
+        this.cameraCaptureCallback = cameraCaptureCallback;
+        this.useRepeatingSurface = useRepeatingSurface;
+    }
+
+    public void addSurface(DeferrableSurface surface) {
+        surfaces.add(surface);
+    }
+
+    public List<DeferrableSurface> getSurfaces() {
+        return Collections.unmodifiableList(surfaces);
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public Map<Key<?>, CaptureRequestParameter<?>> getCameraCharacteristics() {
+        return Collections.unmodifiableMap(captureRequestParameters);
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public Configuration getImplementationOptions() {
+        return implementationOptions;
+    }
+
+    int getTemplateType() {
+        return templateType;
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public boolean isUseRepeatingSurface() {
+        return useRepeatingSurface;
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraCaptureCallback getCameraCaptureCallback() {
+        return cameraCaptureCallback;
+    }
+
+    /**
+     * Return the builder of a {@link CaptureRequest} which can be issued.
+     *
+     * <p>Returns {@code null} if a valid {@link CaptureRequest} can not be constructed.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    public CaptureRequest.Builder buildCaptureRequest(@Nullable CameraDevice device)
+            throws CameraAccessException {
+        if (device == null) {
+            return null;
+        }
+        CaptureRequest.Builder builder = device.createCaptureRequest(templateType);
+
+        for (CaptureRequestParameter<?> captureRequestParameter :
+                captureRequestParameters.values()) {
+            captureRequestParameter.apply(builder);
+        }
+
+        List<Surface> surfaceList = DeferrableSurfaces.surfaceList(surfaces);
+
+        if (surfaceList.isEmpty()) {
+            return null;
+        }
+
+        for (Surface surface : surfaceList) {
+            builder.addTarget(surface);
+        }
+
+        return builder;
+    }
+
+    /** Builder for easy modification/rebuilding of a {@link CaptureRequestConfiguration}. */
+    public static final class Builder {
+        private final Set<DeferrableSurface> surfaces = new HashSet<>();
+        private final Map<Key<?>, CaptureRequestParameter<?>> captureRequestParameters =
+                new HashMap<>();
+        private MutableConfiguration implementationOptions = MutableOptionsBundle.create();
+        private int templateType = -1;
+        private CameraCaptureCallback cameraCaptureCallback =
+                CameraCaptureCallbacks.createNoOpCallback();
+        private boolean useRepeatingSurface = false;
+
+        public Builder() {
+        }
+
+        private Builder(CaptureRequestConfiguration base) {
+            surfaces.addAll(base.surfaces);
+            captureRequestParameters.putAll(base.captureRequestParameters);
+            implementationOptions = MutableOptionsBundle.from(base.implementationOptions);
+            templateType = base.templateType;
+            cameraCaptureCallback = base.cameraCaptureCallback;
+            useRepeatingSurface = base.isUseRepeatingSurface();
+        }
+
+        /** Create a {@link Builder} from a {@link CaptureRequestConfiguration} */
+        public static Builder from(CaptureRequestConfiguration base) {
+            return new Builder(base);
+        }
+
+        int getTemplateType() {
+            return templateType;
+        }
+
+        /**
+         * Set the template characteristics of the CaptureRequestConfiguration.
+         *
+         * @param templateType Template constant that must match those defined by {@link
+         *                     CameraDevice}
+         */
+        public void setTemplateType(int templateType) {
+            this.templateType = templateType;
+        }
+
+        CameraCaptureCallback getCameraCaptureCallback() {
+            return cameraCaptureCallback;
+        }
+
+        public void setCameraCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+            this.cameraCaptureCallback = cameraCaptureCallback;
+        }
+
+        public void addSurface(DeferrableSurface surface) {
+            surfaces.add(surface);
+        }
+
+        public void removeSurface(DeferrableSurface surface) {
+            surfaces.remove(surface);
+        }
+
+        public void clearSurfaces() {
+            surfaces.clear();
+        }
+
+        Set<DeferrableSurface> getSurfaces() {
+            return surfaces;
+        }
+
+        public <T> void addCharacteristic(Key<T> key, T value) {
+            captureRequestParameters.put(key, CaptureRequestParameter.create(key, value));
+        }
+
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public void addCharacteristics(Map<Key<?>, CaptureRequestParameter<?>> characteristics) {
+            captureRequestParameters.putAll(characteristics);
+        }
+
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public void setImplementationOptions(Configuration config) {
+            implementationOptions = MutableOptionsBundle.from(config);
+        }
+
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public void addImplementationOptions(Configuration config) {
+            for (Option<?> option : config.listOptions()) {
+                @SuppressWarnings("unchecked") // Options/values are being copied directly
+                        Option<Object> objectOpt = (Option<Object>) option;
+                implementationOptions.insertOption(objectOpt, config.retrieveOption(objectOpt));
+            }
+        }
+
+        Map<Key<?>, CaptureRequestParameter<?>> getCharacteristic() {
+            return captureRequestParameters;
+        }
+
+        boolean isUseRepeatingSurface() {
+            return useRepeatingSurface;
+        }
+
+        public void setUseRepeatingSurface(boolean useRepeatingSurface) {
+            this.useRepeatingSurface = useRepeatingSurface;
+        }
+
+        /**
+         * Builds an instance of a CaptureRequestConfiguration that has all the combined parameters
+         * of the CaptureRequestConfiguration that have been added to the Builder.
+         */
+        public CaptureRequestConfiguration build() {
+            return new CaptureRequestConfiguration(
+                    new ArrayList<>(surfaces),
+                    new HashMap<>(captureRequestParameters),
+                    OptionsBundle.from(implementationOptions),
+                    templateType,
+                    cameraCaptureCallback,
+                    useRepeatingSurface);
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureRequestParameter.java b/camera/core/src/main/java/androidx/camera/core/CaptureRequestParameter.java
new file mode 100644
index 0000000..79e04bf
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureRequestParameter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A {@link CaptureRequest.Key}-value pair.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@AutoValue
+public abstract class CaptureRequestParameter<T> {
+    /** Prevent subclassing. */
+    CaptureRequestParameter() {
+    }
+
+    public static <T> CaptureRequestParameter<T> create(CaptureRequest.Key<T> key, T value) {
+        return new AutoValue_CaptureRequestParameter<>(key, value);
+    }
+
+    /**
+     * Apply the parameter to the {@link CaptureRequest.Builder}
+     *
+     * <p>This provides a type safe way of setting the key-value pair since the type of the key gets
+     * erased.
+     */
+    public final void apply(CaptureRequest.Builder builder) {
+        builder.set(getKey(), getValue());
+    }
+
+    public abstract CaptureRequest.Key<T> getKey();
+
+    public abstract T getValue();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CheckedSurfaceTexture.java b/camera/core/src/main/java/androidx/camera/core/CheckedSurfaceTexture.java
new file mode 100644
index 0000000..461e6e9
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CheckedSurfaceTexture.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.SurfaceTexture;
+import android.opengl.GLES20;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.nio.IntBuffer;
+
+/**
+ * A {@link DeferrableSurface} which verifies the {@link SurfaceTexture} that backs the {@link
+ * Surface} is unreleased before returning the Surface.
+ */
+final class CheckedSurfaceTexture implements DeferrableSurface {
+    private final OnTextureChangedListener outputChangedListener;
+    private final Handler mainThreadHandler;
+    @Nullable
+    private SurfaceTexture surfaceTexture;
+    @Nullable
+    private Surface surface;
+    @Nullable
+    private Size resolution;
+    CheckedSurfaceTexture(
+            OnTextureChangedListener outputChangedListener, Handler mainThreadHandler) {
+        this.outputChangedListener = outputChangedListener;
+        this.mainThreadHandler = mainThreadHandler;
+    }
+
+    private static SurfaceTexture createDetachedSurfaceTexture(Size resolution) {
+        IntBuffer buffer = IntBuffer.allocate(1);
+        GLES20.glGenTextures(1, buffer);
+        SurfaceTexture surfaceTexture = new FixedSizeSurfaceTexture(buffer.get(), resolution);
+        surfaceTexture.detachFromGLContext();
+        return surfaceTexture;
+    }
+
+    @UiThread
+    void setResolution(Size resolution) {
+        this.resolution = resolution;
+    }
+
+    @UiThread
+    void resetSurfaceTexture() {
+        if (resolution == null) {
+            throw new IllegalStateException(
+                    "setResolution() must be called before resetSurfaceTexture()");
+        }
+
+        release();
+        surfaceTexture = createDetachedSurfaceTexture(resolution);
+        surface = new Surface(surfaceTexture);
+        outputChangedListener.onTextureChanged(surfaceTexture, resolution);
+    }
+
+    private boolean surfaceTextureReleased(SurfaceTexture surfaceTexture) {
+        boolean released = false;
+
+        // TODO(b/121196683) Refactor workaround into a compatibility module
+        if (26 <= android.os.Build.VERSION.SDK_INT) {
+            released = surfaceTexture.isReleased();
+        } else {
+            // WARNING: This relies on some implementation details of the ViewFinderOutput native
+            // code.
+            // If the ViewFinderOutput is released, we should get a RuntimeException. If not, we
+            // should
+            // get an IllegalStateException since we are not in the same EGL context as the
+            // consumer.
+            Exception exception = null;
+            try {
+                // TODO(b/121198329) Make sure updateTexImage() isn't called on consumer EGL context
+                surfaceTexture.updateTexImage();
+            } catch (IllegalStateException e) {
+                exception = e;
+                released = false;
+            } catch (RuntimeException e) {
+                exception = e;
+                released = true;
+            }
+
+            if (!released && exception == null) {
+                throw new RuntimeException("Unable to determine if ViewFinderOutput is released");
+            }
+        }
+
+        return released;
+    }
+
+    /**
+     * Returns the {@link Surface} that is backed by a {@link SurfaceTexture}.
+     *
+     * <p>If the {@link SurfaceTexture} has already been released then the surface will be reset
+     * using a new {@link SurfaceTexture}.
+     */
+    @Override
+    public ListenableFuture<Surface> getSurface() {
+        SettableFuture<Surface> deferredSurface = SettableFuture.create();
+        Runnable checkAndSetRunnable =
+                () -> {
+                    if (surfaceTextureReleased(surfaceTexture)) {
+                        // Reset the surface texture and notify the listener
+                        resetSurfaceTexture();
+                    }
+
+                    deferredSurface.set(surface);
+                };
+
+        if (Looper.myLooper() == mainThreadHandler.getLooper()) {
+            checkAndSetRunnable.run();
+        } else {
+            mainThreadHandler.post(checkAndSetRunnable);
+        }
+
+        return deferredSurface;
+    }
+
+    void release() {
+        if (surface != null) {
+            surface.release();
+            surface = null;
+        }
+    }
+
+    interface OnTextureChangedListener {
+        void onTextureChanged(SurfaceTexture newOutput, Size newResolution);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/Configuration.java b/camera/core/src/main/java/androidx/camera/core/Configuration.java
new file mode 100644
index 0000000..c717b38
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/Configuration.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+
+import java.util.Set;
+
+/**
+ * A Configuration is a collection of {@link Option}s and values.
+ *
+ * <p>Configuration object hold pairs of Options/Values and offer methods for querying whether
+ * Options are contained in the configuration along with methods for retrieving the associated
+ * values for options.
+ */
+public interface Configuration {
+
+    /**
+     * Returns whether this configuration contains the supplied option.
+     *
+     * @param id The {@link Option} to search for in this configuration.
+     * @return <code>true</code> if this configuration contains the supplied option; <code>false
+     * </code> otherwise.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    boolean containsOption(Option<?> id);
+
+    /**
+     * Retrieves the value for the specified option if it exists in the configuration.
+     *
+     * <p>If the option does not exist, an exception will be thrown.
+     *
+     * @param id       The {@link Option} to search for in this configuration.
+     * @param <ValueT> The type for the value associated with the supplied {@link Option}.
+     * @return The value stored in this configuration, or <code>null</code> if it does not exist.
+     * @throws IllegalArgumentException if the given option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    <ValueT> ValueT retrieveOption(Option<ValueT> id);
+
+    /**
+     * Retrieves the value for the specified option if it exists in the configuration.
+     *
+     * <p>If the option does not exist, <code>valueIfMissing</code> will be returned.
+     *
+     * @param id             The {@link Option} to search for in this configuration.
+     * @param valueIfMissing The value to return if the specified {@link Option} does not exist in
+     *                       this configuration.
+     * @param <ValueT>       The type for the value associated with the supplied {@link Option}.
+     * @return The value stored in this configuration, or <code>null</code> if it does not exist.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing);
+
+    /**
+     * Search the configuration for {@link Option}s whose id match the supplied search string.
+     *
+     * @param idSearchString The id string to search for. This could be a fully qualified id such as
+     *                       \"<code>camerax.core.example.option</code>\" or the stem for an
+     *                       option such as \"<code>
+     *                       camerax.core.example</code>\".
+     * @param matcher        A callback used to receive results of the search. Results will be
+     *                       sent to
+     *                       {@link OptionMatcher#onOptionMatched(Option)} in the order in which
+     *                       they are found inside
+     *                       this configuration. Subsequent results will continue to be sent as
+     *                       long as {@link
+     *                       OptionMatcher#onOptionMatched(Option)} returns <code>true</code>.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    void findOptions(String idSearchString, OptionMatcher matcher);
+
+    /**
+     * Lists all options contained within this configuration.
+     *
+     * @return A {@link Set} of {@link Option}s contained within this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Set<Option<?>> listOptions();
+
+    /**
+     * A callback for retrieving results of a {@link Configuration.Option} search.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    interface OptionMatcher {
+        /**
+         * Receives results from {@link Configuration#findOptions(String, OptionMatcher)}.
+         *
+         * <p>When searching for a specific option in a {@link Configuration}, {@link Option}s will
+         * be sent to {@link #onOptionMatched(Option)} in the order in which they are found.
+         *
+         * @param option The matched option.
+         * @return <code>false</code> if no further results are needed; <code>true</code> otherwise.
+         */
+        boolean onOptionMatched(Option<?> option);
+    }
+
+    /**
+     * The Reader interface can be extended to create APIs for reading specific options.
+     *
+     * <p>Reader objects are also {@link Configuration} objects, so can be passed to any method that
+     * expects a {@link Configuration}.
+     */
+    interface Reader extends Configuration {
+
+        /**
+         * Returns the underlying immutable {@link Configuration} object.
+         *
+         * @return The underlying {@link Configuration} object.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        Configuration getConfiguration();
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        default boolean containsOption(Option<?> id) {
+            return getConfiguration().containsOption(id);
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        default <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+            return getConfiguration().retrieveOption(id);
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        default <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+            return getConfiguration().retrieveOption(id, valueIfMissing);
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        default void findOptions(String idStem, OptionMatcher matcher) {
+            getConfiguration().findOptions(idStem, matcher);
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        default Set<Option<?>> listOptions() {
+            return getConfiguration().listOptions();
+        }
+    }
+
+    /**
+     * Builders are used to generate immutable {@link Configuration} objects.
+     *
+     * @param <C> The top-level type of the {@link Configuration} being generated.
+     * @param <T> The top-level {@link Builder} type for this Builder.
+     */
+    interface Builder<C extends Configuration, T extends Builder<C, T>> {
+
+        /**
+         * Returns the underlying {@link MutableConfiguration} being modified by this builder.
+         *
+         * @return The underlying {@link MutableConfiguration}.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        MutableConfiguration getMutableConfiguration();
+
+        /**
+         * The solution for the unchecked cast warning.
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        T builder();
+
+        /**
+         * Inserts a Option/Value pair into the configuration.
+         *
+         * <p>If the option already exists in this configuration, it will be replaced.
+         *
+         * @param opt      The option to be added or modified
+         * @param value    The value to insert for this option.
+         * @param <ValueT> The type of the value being inserted.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        default <ValueT> T insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /**
+         * Removes an option from the configuration if it exists.
+         *
+         * @param opt      The option to remove from the configuration.
+         * @param <ValueT> The type of the value being removed.
+         * @return The value that previously existed for <code>opt</code>, or <code>null</code> if
+         * the option did not exist in this configuration.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Nullable
+        default <ValueT> T removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        /**
+         * Creates an immutable {@link Configuration} object from the current state of this builder.
+         *
+         * @return The {@link Configuration} generated from the current state.
+         */
+        C build();
+    }
+
+    /**
+     * An {@link Option} is used to set and retrieve values for settings defined in a {@link
+     * Configuration}.
+     *
+     * <p>{@link Option}s can be thought of as the key in a key/value pair that makes up a setting.
+     * As the name suggests, {@link Option}s are optional, and may or may not exist inside a {@link
+     * Configuration}.
+     *
+     * @param <T> The type of the value for this option.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @AutoValue
+    abstract class Option<T> {
+
+        /** Prevent subclassing */
+        Option() {
+        }
+
+        /**
+         * Creates an {@link Option} from an id and value class.
+         *
+         * @param id         A unique string identifier for this option. This generally follows
+         *                   the scheme
+         *                   <code>&lt;owner&gt;.[optional.subCategories.]&lt;optionId&gt;</code>.
+         * @param valueClass The class of the value stored by this option.
+         * @param <T>        The type of the value stored by this option.
+         * @return An {@link Option} object which can be used to store/retrieve values from a {@link
+         * Configuration}.
+         */
+        public static <T> Option<T> create(String id, Class<T> valueClass) {
+            TypeReference<T> valueType = TypeReference.createSpecializedTypeReference(valueClass);
+            return create(id, valueType, /*token=*/ null);
+        }
+
+        /**
+         * Creates an {@link Option} from an id, value class and token.
+         *
+         * @param id         A unique string identifier for this option. This generally follows
+         *                   the scheme
+         *                   <code>&lt;owner&gt;.[optional.subCategories.]&lt;optionId&gt;</code>.
+         * @param valueClass The class of the value stored by this option.
+         * @param <T>        The type of the value stored by this option.
+         * @param token      An optional, type-erased object for storing more context for this
+         *                   specific
+         *                   option. Generally this object should have static scope and be
+         *                   immutable.
+         * @return An {@link Option} object which can be used to store/retrieve values from a {@link
+         * Configuration}.
+         */
+        public static <T> Option<T> create(String id, Class<T> valueClass, @Nullable Object token) {
+            TypeReference<T> valueType = TypeReference.createSpecializedTypeReference(valueClass);
+            return create(id, valueType, token);
+        }
+
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public static <T> Option<T> create(String name, TypeReference<T> valueType) {
+            return create(name, valueType, /*token=*/ null);
+        }
+
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public static <T> Option<T> create(
+                String name, TypeReference<T> valueType, @Nullable Object token) {
+            return new AutoValue_Configuration_Option<>(name, valueType, token);
+        }
+
+        /**
+         * Returns the unique string identifier for this option.
+         *
+         * <p>This generally follows the scheme * <code>
+         * &lt;owner&gt;.[optional.subCategories.]&lt;optionId&gt;
+         * </code>.
+         *
+         * @return The identifier.
+         */
+        public abstract String getId();
+
+        abstract TypeReference<T> getTypeReference();
+
+        /**
+         * Returns the optional type-erased context object for this option.
+         *
+         * <p>Generally this object should have static scope and be immutable.
+         *
+         * @return The type-erased context object.
+         */
+        @Nullable
+        public abstract Object getToken();
+
+        /**
+         * Returns the class object associated with the value for this option.
+         *
+         * @return The class object for the value's type.
+         */
+        @Memoized
+        @SuppressWarnings("unchecked")
+        public Class<T> getValueClass() {
+            return (Class<T>) getTypeReference().getRawType();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ConfigurationProvider.java b/camera/core/src/main/java/androidx/camera/core/ConfigurationProvider.java
new file mode 100644
index 0000000..6b0dbef
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ConfigurationProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * A class which provides a {@link androidx.camera.core.Configuration} object.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface ConfigurationProvider<C extends Configuration> {
+
+    /** Retrieve the {@link androidx.camera.core.Configuration} object. */
+    C getConfiguration();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/DeferrableSurface.java b/camera/core/src/main/java/androidx/camera/core/DeferrableSurface.java
new file mode 100644
index 0000000..038bf61
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/DeferrableSurface.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * A reference to a {@link Surface} whose creation can be deferred to a later time.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface DeferrableSurface {
+    /** Returns a {@link Surface} that is wrapped in a {@link ListenableFuture}. */
+    @Nullable
+    ListenableFuture<Surface> getSurface();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/DeferrableSurfaces.java b/camera/core/src/main/java/androidx/camera/core/DeferrableSurfaces.java
new file mode 100644
index 0000000..ae0d86d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/DeferrableSurfaces.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Utility functions for manipulating {@link DeferrableSurface}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class DeferrableSurfaces {
+    private static final String TAG = "DeferrableSurfaces";
+
+    private DeferrableSurfaces() {
+    }
+
+    /**
+     * Returns a {@link Surface} list from a {@link DeferrableSurface} collection.
+     *
+     * <p>Any {@link DeferrableSurface} that can not be obtained will be missing from the list. This
+     * means that the returned list will only be guaranteed to be less than or equal to in size to
+     * the original collection.
+     */
+    public static List<Surface> surfaceList(Collection<DeferrableSurface> deferrableSurfaces) {
+        List<ListenableFuture<Surface>> listenableFutureSurfaces = new ArrayList<>();
+
+        for (DeferrableSurface deferrableSurface : deferrableSurfaces) {
+            listenableFutureSurfaces.add(deferrableSurface.getSurface());
+        }
+
+        try {
+            // Need to create a new list since the list returned by successfulAsList() is
+            // unmodifiable so
+            // it will throw an Exception
+            List<Surface> surfaces =
+                    new ArrayList<>(Futures.successfulAsList(listenableFutureSurfaces).get());
+            surfaces.removeAll(Collections.singleton(null));
+            return Collections.unmodifiableList(surfaces);
+        } catch (InterruptedException | ExecutionException e) {
+            return Collections.unmodifiableList(Collections.emptyList());
+        }
+    }
+
+    /**
+     * Returns a {@link Surface} set from a {@link DeferrableSurface} collection.
+     *
+     * <p>Any {@link DeferrableSurface} that can not be obtained will be missing from the set. This
+     * means that the returned set will only be guaranteed to be less than or equal to in size to
+     * the original collection.
+     */
+    public static Set<Surface> surfaceSet(Collection<DeferrableSurface> deferrableSurfaces) {
+        List<ListenableFuture<Surface>> listenableFutureSurfaces = new ArrayList<>();
+
+        for (DeferrableSurface deferrableSurface : deferrableSurfaces) {
+            listenableFutureSurfaces.add(deferrableSurface.getSurface());
+        }
+
+        try {
+            HashSet<Surface> surfaces =
+                    new HashSet<>(Futures.successfulAsList(listenableFutureSurfaces).get());
+            surfaces.removeAll(Collections.singleton(null));
+            return Collections.unmodifiableSet(surfaces);
+        } catch (InterruptedException | ExecutionException e) {
+            return Collections.unmodifiableSet(Collections.emptySet());
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/DeviceProperties.java b/camera/core/src/main/java/androidx/camera/core/DeviceProperties.java
new file mode 100644
index 0000000..d0383d7
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/DeviceProperties.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.os.Build;
+
+import com.google.auto.value.AutoValue;
+
+/** Container of the device properties. */
+@AutoValue
+abstract class DeviceProperties {
+    /** Creates an instance by querying the properties from {@link android.os.Build}. */
+    static DeviceProperties create() {
+        return create(Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT);
+    }
+
+    /** Creates an instance from the given properties. */
+    static DeviceProperties create(String manufacturer, String model, int sdkVersion) {
+        return new AutoValue_DeviceProperties(manufacturer, model, sdkVersion);
+    }
+
+    /** Returns the manufacturer of the device. */
+    abstract String manufacturer();
+
+    /** Returns the model of the device. */
+    abstract String model();
+
+    /** Returns the SDK version of the OS running on the device. */
+    abstract int sdkVersion();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ErrorHandler.java b/camera/core/src/main/java/androidx/camera/core/ErrorHandler.java
new file mode 100644
index 0000000..7685d03
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ErrorHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.ErrorCode;
+import androidx.camera.core.CameraX.ErrorListener;
+
+/**
+ * Handler for sending and receiving error messages.
+ *
+ * @hide Only internal classes should post error messages
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ErrorHandler {
+    private static final String TAG = "ErrorHandler";
+
+    private final Object errorLock = new Object();
+
+    @GuardedBy("errorLock")
+    private ErrorListener listener = new PrintingErrorListener();
+
+    @GuardedBy("errorLock")
+    private Handler handler = new Handler(Looper.getMainLooper());
+
+    /**
+     * Posts an error message.
+     *
+     * @param error   the type of error that occurred
+     * @param message detailed message of the error condition
+     */
+    void postError(ErrorCode error, String message) {
+        synchronized (errorLock) {
+            ErrorListener listenerReference = listener;
+            handler.post(() -> listenerReference.onError(error, message));
+        }
+    }
+
+    /**
+     * Sets the listener for the error.
+     *
+     * @param listener the listener which should handle the error condition
+     * @param handler  the handler on which to run the listener
+     */
+    void setErrorListener(ErrorListener listener, Handler handler) {
+        synchronized (errorLock) {
+            if (handler == null) {
+                this.handler = new Handler(Looper.getMainLooper());
+            } else {
+                this.handler = handler;
+            }
+            if (listener == null) {
+                this.listener = new PrintingErrorListener();
+            } else {
+                this.listener = listener;
+            }
+        }
+    }
+
+    /** An error listener which logs the error message and returns. */
+    static final class PrintingErrorListener implements ErrorListener {
+        @Override
+        public void onError(ErrorCode error, String message) {
+            Log.e(TAG, "ErrorHandler occurred: " + error + " with message: " + message);
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/Exif.java b/camera/core/src/main/java/androidx/camera/core/Exif.java
new file mode 100644
index 0000000..a74df30
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/Exif.java
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.location.Location;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.exifinterface.media.ExifInterface;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Utility class for modifying metadata on JPEG files.
+ *
+ * <p>Call {@link #save()} to persist changes to disc.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class Exif {
+
+    /** Timestamp value indicating a timestamp value that is either not set or not valid */
+    public static final long INVALID_TIMESTAMP = -1;
+
+    private static final String TAG = Exif.class.getSimpleName();
+
+    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
+            new ThreadLocal<SimpleDateFormat>() {
+                @Override
+                public SimpleDateFormat initialValue() {
+                    return new SimpleDateFormat("yyyy:MM:dd", Locale.US);
+                }
+            };
+    private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT =
+            new ThreadLocal<SimpleDateFormat>() {
+                @Override
+                public SimpleDateFormat initialValue() {
+                    return new SimpleDateFormat("HH:mm:ss", Locale.US);
+                }
+            };
+    private static final ThreadLocal<SimpleDateFormat> DATETIME_FORMAT =
+            new ThreadLocal<SimpleDateFormat>() {
+                @Override
+                public SimpleDateFormat initialValue() {
+                    return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
+                }
+            };
+
+    private static final String KILOMETERS_PER_HOUR = "K";
+    private static final String MILES_PER_HOUR = "M";
+    private static final String KNOTS = "N";
+
+    private final ExifInterface exifInterface;
+
+    // When true, avoid saving any time. This is a privacy issue.
+    private boolean removeTimestamp = false;
+
+    private Exif(ExifInterface exifInterface) {
+        this.exifInterface = exifInterface;
+    }
+
+    public static Exif createFromFile(File file) throws IOException {
+        return createFromFileString(file.toString());
+    }
+
+    public static Exif createFromFileString(String filePath) throws IOException {
+        return new Exif(new ExifInterface(filePath));
+    }
+
+    public static Exif createFromInputStream(InputStream is) throws IOException {
+        return new Exif(new ExifInterface(is));
+    }
+
+    private static String convertToExifDateTime(long timestamp) {
+        return DATETIME_FORMAT.get().format(new Date(timestamp));
+    }
+
+    private static Date convertFromExifDateTime(String dateTime) throws ParseException {
+        return DATETIME_FORMAT.get().parse(dateTime);
+    }
+
+    private static Date convertFromExifDate(String date) throws ParseException {
+        return DATE_FORMAT.get().parse(date);
+    }
+
+    private static Date convertFromExifTime(String time) throws ParseException {
+        return TIME_FORMAT.get().parse(time);
+    }
+
+    /** Persists changes to disc. */
+    public void save() throws IOException {
+        if (!removeTimestamp) {
+            attachLastModifiedTimestamp();
+        }
+        exifInterface.saveAttributes();
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.ENGLISH,
+                "Exif{width=%s, height=%s, rotation=%d, "
+                        + "isFlippedVertically=%s, isFlippedHorizontally=%s, location=%s, "
+                        + "timestamp=%s, description=%s}",
+                getWidth(),
+                getHeight(),
+                getRotation(),
+                isFlippedVertically(),
+                isFlippedHorizontally(),
+                getLocation(),
+                getTimestamp(),
+                getDescription());
+    }
+
+    private int getOrientation() {
+        return exifInterface.getAttributeInt(
+                ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
+    }
+
+    /** Returns the width of the photo in pixels. */
+    public int getWidth() {
+        return exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0);
+    }
+
+    /** Returns the height of the photo in pixels. */
+    public int getHeight() {
+        return exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0);
+    }
+
+    @Nullable
+    public String getDescription() {
+        return exifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION);
+    }
+
+    public void setDescription(@Nullable String description) {
+        exifInterface.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, description);
+    }
+
+    /** @return The degree of rotation (eg. 0, 90, 180, 270). */
+    public int getRotation() {
+        switch (getOrientation()) {
+            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+                return 0;
+            case ExifInterface.ORIENTATION_ROTATE_180:
+                return 180;
+            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                return 180;
+            case ExifInterface.ORIENTATION_TRANSPOSE:
+                return 270;
+            case ExifInterface.ORIENTATION_ROTATE_90:
+                return 90;
+            case ExifInterface.ORIENTATION_TRANSVERSE:
+                return 90;
+            case ExifInterface.ORIENTATION_ROTATE_270:
+                return 270;
+            case ExifInterface.ORIENTATION_NORMAL:
+                // Fall-through
+            case ExifInterface.ORIENTATION_UNDEFINED:
+                // Fall-through
+            default:
+                return 0;
+        }
+    }
+
+    /** @return True if the image is flipped vertically after rotation. */
+    public boolean isFlippedVertically() {
+        switch (getOrientation()) {
+            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+                return false;
+            case ExifInterface.ORIENTATION_ROTATE_180:
+                return false;
+            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                return true;
+            case ExifInterface.ORIENTATION_TRANSPOSE:
+                return true;
+            case ExifInterface.ORIENTATION_ROTATE_90:
+                return false;
+            case ExifInterface.ORIENTATION_TRANSVERSE:
+                return true;
+            case ExifInterface.ORIENTATION_ROTATE_270:
+                return false;
+            case ExifInterface.ORIENTATION_NORMAL:
+                // Fall-through
+            case ExifInterface.ORIENTATION_UNDEFINED:
+                // Fall-through
+            default:
+                return false;
+        }
+    }
+
+    /** @return True if the image is flipped horizontally after rotation. */
+    public boolean isFlippedHorizontally() {
+        switch (getOrientation()) {
+            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+                return true;
+            case ExifInterface.ORIENTATION_ROTATE_180:
+                return false;
+            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                return false;
+            case ExifInterface.ORIENTATION_TRANSPOSE:
+                return false;
+            case ExifInterface.ORIENTATION_ROTATE_90:
+                return false;
+            case ExifInterface.ORIENTATION_TRANSVERSE:
+                return false;
+            case ExifInterface.ORIENTATION_ROTATE_270:
+                return false;
+            case ExifInterface.ORIENTATION_NORMAL:
+                // Fall-through
+            case ExifInterface.ORIENTATION_UNDEFINED:
+                // Fall-through
+            default:
+                return false;
+        }
+    }
+
+    private void attachLastModifiedTimestamp() {
+        long now = System.currentTimeMillis();
+        String datetime = convertToExifDateTime(now);
+
+        exifInterface.setAttribute(ExifInterface.TAG_DATETIME, datetime);
+
+        try {
+            String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
+            exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, subsec);
+        } catch (ParseException e) {
+        }
+    }
+
+    /**
+     * @return The timestamp (in millis) that this picture was modified, or {@link
+     * #INVALID_TIMESTAMP} if no time is available.
+     */
+    public long getLastModifiedTimestamp() {
+        long timestamp = parseTimestamp(exifInterface.getAttribute(ExifInterface.TAG_DATETIME));
+        if (timestamp == INVALID_TIMESTAMP) {
+            return INVALID_TIMESTAMP;
+        }
+
+        String subSecs = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME);
+        if (subSecs != null) {
+            try {
+                long sub = Long.parseLong(subSecs);
+                while (sub > 1000) {
+                    sub /= 10;
+                }
+                timestamp += sub;
+            } catch (NumberFormatException e) {
+                // Ignored
+            }
+        }
+
+        return timestamp;
+    }
+
+    /**
+     * @return The timestamp (in millis) that this picture was taken, or {@link #INVALID_TIMESTAMP}
+     * if no time is available.
+     */
+    public long getTimestamp() {
+        long timestamp =
+                parseTimestamp(exifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+        if (timestamp == INVALID_TIMESTAMP) {
+            return INVALID_TIMESTAMP;
+        }
+
+        String subSecs = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL);
+        if (subSecs != null) {
+            try {
+                long sub = Long.parseLong(subSecs);
+                while (sub > 1000) {
+                    sub /= 10;
+                }
+                timestamp += sub;
+            } catch (NumberFormatException e) {
+                // Ignored
+            }
+        }
+
+        return timestamp;
+    }
+
+    /** @return The location this picture was taken, or null if no location is available. */
+    @Nullable
+    public Location getLocation() {
+        String provider = exifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
+        double[] latlng = exifInterface.getLatLong();
+        double altitude = exifInterface.getAltitude(0);
+        double speed = exifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0);
+        String speedRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_SPEED_REF);
+        speedRef = speedRef == null ? KILOMETERS_PER_HOUR : speedRef; // Ensure speedRef is not null
+        long timestamp =
+                parseTimestamp(
+                        exifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP),
+                        exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP));
+        if (latlng == null) {
+            return null;
+        }
+        if (provider == null) {
+            provider = TAG;
+        }
+
+        Location location = new Location(provider);
+        location.setLatitude(latlng[0]);
+        location.setLongitude(latlng[1]);
+        if (altitude != 0) {
+            location.setAltitude(altitude);
+        }
+        if (speed != 0) {
+            switch (speedRef) {
+                case MILES_PER_HOUR:
+                    speed = Speed.fromMilesPerHour(speed).toMetersPerSecond();
+                    break;
+                case KNOTS:
+                    speed = Speed.fromKnots(speed).toMetersPerSecond();
+                    break;
+                case KILOMETERS_PER_HOUR:
+                    // fall through
+                default:
+                    speed = Speed.fromKilometersPerHour(speed).toMetersPerSecond();
+                    break;
+            }
+
+            location.setSpeed((float) speed);
+        }
+        if (timestamp != INVALID_TIMESTAMP) {
+            location.setTime(timestamp);
+        }
+        return location;
+    }
+
+    /**
+     * Rotates the image by the given degrees. Can only rotate by right angles (eg. 90, 180, -90).
+     * Other increments will set the orientation to undefined.
+     */
+    public void rotate(int degrees) {
+        if (degrees % 90 != 0) {
+            Log.w(
+                    TAG,
+                    String.format(
+                            "Can only rotate in right angles (eg. 0, 90, 180, 270). %d is "
+                                    + "unsupported.",
+                            degrees));
+            exifInterface.setAttribute(
+                    ExifInterface.TAG_ORIENTATION,
+                    String.valueOf(ExifInterface.ORIENTATION_UNDEFINED));
+            return;
+        }
+
+        degrees %= 360;
+
+        int orientation = getOrientation();
+        while (degrees < 0) {
+            degrees += 90;
+
+            switch (orientation) {
+                case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+                    orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_180:
+                    orientation = ExifInterface.ORIENTATION_ROTATE_90;
+                    break;
+                case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                    orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+                    break;
+                case ExifInterface.ORIENTATION_TRANSPOSE:
+                    orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_90:
+                    orientation = ExifInterface.ORIENTATION_NORMAL;
+                    break;
+                case ExifInterface.ORIENTATION_TRANSVERSE:
+                    orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_270:
+                    orientation = ExifInterface.ORIENTATION_ROTATE_90;
+                    break;
+                case ExifInterface.ORIENTATION_NORMAL:
+                    // Fall-through
+                case ExifInterface.ORIENTATION_UNDEFINED:
+                    // Fall-through
+                default:
+                    orientation = ExifInterface.ORIENTATION_ROTATE_270;
+                    break;
+            }
+        }
+        while (degrees > 0) {
+            degrees -= 90;
+
+            switch (orientation) {
+                case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+                    orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_180:
+                    orientation = ExifInterface.ORIENTATION_ROTATE_270;
+                    break;
+                case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                    orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+                    break;
+                case ExifInterface.ORIENTATION_TRANSPOSE:
+                    orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_90:
+                    orientation = ExifInterface.ORIENTATION_ROTATE_180;
+                    break;
+                case ExifInterface.ORIENTATION_TRANSVERSE:
+                    orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+                    break;
+                case ExifInterface.ORIENTATION_ROTATE_270:
+                    orientation = ExifInterface.ORIENTATION_NORMAL;
+                    break;
+                case ExifInterface.ORIENTATION_NORMAL:
+                    // Fall-through
+                case ExifInterface.ORIENTATION_UNDEFINED:
+                    // Fall-through
+                default:
+                    orientation = ExifInterface.ORIENTATION_ROTATE_90;
+                    break;
+            }
+        }
+        exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
+    }
+
+    /**
+     * Sets attributes to represent a flip of the image over the horizon so that the top and bottom
+     * are reversed.
+     */
+    public void flipVertically() {
+        int orientation;
+        switch (getOrientation()) {
+            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+                orientation = ExifInterface.ORIENTATION_ROTATE_180;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_180:
+                orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+                break;
+            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                orientation = ExifInterface.ORIENTATION_NORMAL;
+                break;
+            case ExifInterface.ORIENTATION_TRANSPOSE:
+                orientation = ExifInterface.ORIENTATION_ROTATE_270;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_90:
+                orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+                break;
+            case ExifInterface.ORIENTATION_TRANSVERSE:
+                orientation = ExifInterface.ORIENTATION_ROTATE_90;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_270:
+                orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+                break;
+            case ExifInterface.ORIENTATION_NORMAL:
+                // Fall-through
+            case ExifInterface.ORIENTATION_UNDEFINED:
+                // Fall-through
+            default:
+                orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+                break;
+        }
+        exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
+    }
+
+    /**
+     * Sets attributes to represent a flip of the image over the vertical so that the left and right
+     * are reversed.
+     */
+    public void flipHorizontally() {
+        int orientation;
+        switch (getOrientation()) {
+            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
+                orientation = ExifInterface.ORIENTATION_NORMAL;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_180:
+                orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL;
+                break;
+            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
+                orientation = ExifInterface.ORIENTATION_ROTATE_180;
+                break;
+            case ExifInterface.ORIENTATION_TRANSPOSE:
+                orientation = ExifInterface.ORIENTATION_ROTATE_90;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_90:
+                orientation = ExifInterface.ORIENTATION_TRANSPOSE;
+                break;
+            case ExifInterface.ORIENTATION_TRANSVERSE:
+                orientation = ExifInterface.ORIENTATION_ROTATE_270;
+                break;
+            case ExifInterface.ORIENTATION_ROTATE_270:
+                orientation = ExifInterface.ORIENTATION_TRANSVERSE;
+                break;
+            case ExifInterface.ORIENTATION_NORMAL:
+                // Fall-through
+            case ExifInterface.ORIENTATION_UNDEFINED:
+                // Fall-through
+            default:
+                orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL;
+                break;
+        }
+        exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
+    }
+
+    /** Attaches the current timestamp to the file. */
+    public void attachTimestamp() {
+        long now = System.currentTimeMillis();
+        String datetime = convertToExifDateTime(now);
+
+        exifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, datetime);
+        exifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, datetime);
+
+        try {
+            String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
+            exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subsec);
+            exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subsec);
+        } catch (ParseException e) {
+        }
+
+        removeTimestamp = false;
+    }
+
+    /** Removes the timestamp from the file. */
+    public void removeTimestamp() {
+        exifInterface.setAttribute(ExifInterface.TAG_DATETIME, null);
+        exifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
+        exifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null);
+        exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null);
+        exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null);
+        exifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null);
+        removeTimestamp = true;
+    }
+
+    /** Attaches the given location to the file. */
+    public void attachLocation(Location location) {
+        exifInterface.setGpsInfo(location);
+    }
+
+    /** Removes the location from the file. */
+    public void removeLocation() {
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, null);
+        exifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null);
+    }
+
+    /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */
+    private long parseTimestamp(@Nullable String date, @Nullable String time) {
+        if (date == null && time == null) {
+            return INVALID_TIMESTAMP;
+        }
+        if (time == null) {
+            try {
+                return convertFromExifDate(date).getTime();
+            } catch (ParseException e) {
+                return INVALID_TIMESTAMP;
+            }
+        }
+        if (date == null) {
+            try {
+                return convertFromExifTime(time).getTime();
+            } catch (ParseException e) {
+                return INVALID_TIMESTAMP;
+            }
+        }
+        return parseTimestamp(date + " " + time);
+    }
+
+    /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */
+    private long parseTimestamp(@Nullable String datetime) {
+        if (datetime == null) {
+            return INVALID_TIMESTAMP;
+        }
+        try {
+            return convertFromExifDateTime(datetime).getTime();
+        } catch (ParseException e) {
+            return INVALID_TIMESTAMP;
+        }
+    }
+
+    private static final class Speed {
+        static Converter fromKilometersPerHour(double kph) {
+            return new Converter(kph * 0.621371);
+        }
+
+        static Converter fromMetersPerSecond(double mps) {
+            return new Converter(mps * 2.23694);
+        }
+
+        static Converter fromMilesPerHour(double mph) {
+            return new Converter(mph);
+        }
+
+        static Converter fromKnots(double knots) {
+            return new Converter(knots * 1.15078);
+        }
+
+        static final class Converter {
+            final double mph;
+
+            Converter(double mph) {
+                this.mph = mph;
+            }
+
+            double toKilometersPerHour() {
+                return mph / 0.621371;
+            }
+
+            double toMilesPerHour() {
+                return mph;
+            }
+
+            double toKnots() {
+                return mph / 1.15078;
+            }
+
+            double toMetersPerSecond() {
+                return mph / 2.23694;
+            }
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ExtendableUseCaseConfigFactory.java b/camera/core/src/main/java/androidx/camera/core/ExtendableUseCaseConfigFactory.java
new file mode 100644
index 0000000..f81e718
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ExtendableUseCaseConfigFactory.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link androidx.camera.core.UseCaseConfigurationFactory} that uses {@link
+ * ConfigurationProvider}s to provide configurations.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ExtendableUseCaseConfigFactory implements UseCaseConfigurationFactory {
+    private final Map<Class<?>, ConfigurationProvider<?>> defaultProviders = new HashMap<>();
+
+    /**
+     * Inserts or overrides the {@link androidx.camera.core.ConfigurationProvider} for the given
+     * config type.
+     */
+    public <C extends Configuration> void installDefaultProvider(
+            Class<C> configType, ConfigurationProvider<C> defaultProvider) {
+        defaultProviders.put(configType, defaultProvider);
+    }
+
+    @Nullable
+    @Override
+    public <C extends UseCaseConfiguration<?>> C getConfiguration(Class<C> configType) {
+        @SuppressWarnings("unchecked") // Providers only could have been inserted with
+                // installDefaultProvider(), so the class should return the correct type.
+                ConfigurationProvider<C> provider =
+                (ConfigurationProvider<C>) defaultProviders.get(configType);
+        if (provider != null) {
+            return provider.getConfiguration();
+        }
+        return null;
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/FixedSizeSurfaceTexture.java b/camera/core/src/main/java/androidx/camera/core/FixedSizeSurfaceTexture.java
new file mode 100644
index 0000000..e1a8227
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/FixedSizeSurfaceTexture.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.SurfaceTexture;
+import android.os.Build.VERSION_CODES;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * An implementation of {@link SurfaceTexture} with a fixed default buffer size.
+ *
+ * <p>The fixed default buffer size used at construction time cannot be changed through the {@link
+ * #setDefaultBufferSize(int, int)} method.
+ */
+final class FixedSizeSurfaceTexture extends SurfaceTexture {
+
+    /**
+     * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+     *
+     * @param texName   the OpenGL texture object name (e.g. generated via glGenTextures)
+     * @param fixedSize the fixed default buffer size
+     * @throws Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+     */
+    FixedSizeSurfaceTexture(int texName, Size fixedSize) {
+        super(texName);
+        super.setDefaultBufferSize(fixedSize.getWidth(), fixedSize.getHeight());
+    }
+
+    /**
+     * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+     *
+     * <p>In single buffered mode the application is responsible for serializing access to the image
+     * content buffer. Each time the image content is to be updated, the {@link #releaseTexImage()}
+     * method must be called before the image content producer takes ownership of the buffer. For
+     * example, when producing image content with the NDK ANativeWindow_lock and
+     * ANativeWindow_unlockAndPost functions, {@link #releaseTexImage()} must be called before each
+     * ANativeWindow_lock, or that call will fail. When producing image content with OpenGL ES,
+     * {@link #releaseTexImage()} must be called before the first OpenGL ES function call each
+     * frame.
+     *
+     * @param texName          the OpenGL texture object name (e.g. generated via glGenTextures)
+     * @param singleBufferMode whether the SurfaceTexture will be in single buffered mode.
+     * @param fixedSize        the fixed default buffer size
+     * @throws Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+     */
+    FixedSizeSurfaceTexture(int texName, boolean singleBufferMode, Size fixedSize) {
+        super(texName, singleBufferMode);
+        super.setDefaultBufferSize(fixedSize.getWidth(), fixedSize.getHeight());
+    }
+
+    /**
+     * Construct a new SurfaceTexture to stream images to a given OpenGL texture.
+     *
+     * <p>In single buffered mode the application is responsible for serializing access to the image
+     * content buffer. Each time the image content is to be updated, the {@link #releaseTexImage()}
+     * method must be called before the image content producer takes ownership of the buffer. For
+     * example, when producing image content with the NDK ANativeWindow_lock and
+     * ANativeWindow_unlockAndPost functions, {@link #releaseTexImage()} must be called before each
+     * ANativeWindow_lock, or that call will fail. When producing image content with OpenGL ES,
+     * {@link #releaseTexImage()} must be called before the first OpenGL ES function call each
+     * frame.
+     *
+     * <p>Unlike {@link SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name,
+     * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed
+     * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing
+     * image content using OpenGL ES.
+     *
+     * @param singleBufferMode whether the SurfaceTexture will be in single buffered mode.
+     * @param fixedSize        the fixed default buffer size
+     * @throws Surface.OutOfResourcesException If the SurfaceTexture cannot be created.
+     */
+    @RequiresApi(api = VERSION_CODES.O)
+    FixedSizeSurfaceTexture(boolean singleBufferMode, Size fixedSize) {
+        super(singleBufferMode);
+        super.setDefaultBufferSize(fixedSize.getWidth(), fixedSize.getHeight());
+    }
+
+    /**
+     * This method has no effect.
+     *
+     * <p>Unlike {@link SurfaceTexture}, this method does not affect the default buffer size. The
+     * default buffer size will remain what it was set to during construction.
+     *
+     * @param width  ignored width
+     * @param height ignored height
+     */
+    @Override
+    public void setDefaultBufferSize(int width, int height) {
+        // No-op
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/FlashMode.java b/camera/core/src/main/java/androidx/camera/core/FlashMode.java
new file mode 100644
index 0000000..3b1f277
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/FlashMode.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/** The flash mode options when taking a picture using ImageCaptureUseCase. */
+public enum FlashMode {
+    /**
+     * Auto flash. The flash will be used according to the camera system's determination when taking
+     * a picture.
+     */
+    AUTO,
+    /** Always flash. The flash will always be used when taking a picture. */
+    ON,
+    /** No flash. The flash will never be used when taking a picture. */
+    OFF
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ForwardingImageProxy.java b/camera/core/src/main/java/androidx/camera/core/ForwardingImageProxy.java
new file mode 100644
index 0000000..f4bb754
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ForwardingImageProxy.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.Rect;
+
+import androidx.annotation.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An {@link ImageProxy} which forwards all calls to another {@link ImageProxy}.
+ *
+ * <p>This class enables subclasses to override a few methods to achieve a custom behavior, while
+ * still delegating calls on the remaining methods to a wrapped {@link ImageProxy} instance.
+ *
+ * <p>Listeners for the image close call can be added. When the image is closed, the listeners will
+ * be notified.
+ */
+abstract class ForwardingImageProxy implements ImageProxy {
+    @GuardedBy("this")
+    protected final ImageProxy image;
+
+    @GuardedBy("this")
+    private final Set<OnImageCloseListener> onImageCloseListeners = new HashSet<>();
+
+    /**
+     * Creates a new instance which wraps the given image.
+     *
+     * @param image to wrap
+     * @return new {@link AndroidImageProxy} instance
+     */
+    protected ForwardingImageProxy(ImageProxy image) {
+        this.image = image;
+    }
+
+    @Override
+    public synchronized void close() {
+        image.close();
+        notifyOnImageCloseListeners();
+    }
+
+    @Override
+    public synchronized Rect getCropRect() {
+        return image.getCropRect();
+    }
+
+    @Override
+    public synchronized void setCropRect(Rect rect) {
+        image.setCropRect(rect);
+    }
+
+    @Override
+    public synchronized int getFormat() {
+        return image.getFormat();
+    }
+
+    @Override
+    public synchronized int getHeight() {
+        return image.getHeight();
+    }
+
+    @Override
+    public synchronized int getWidth() {
+        return image.getWidth();
+    }
+
+    @Override
+    public synchronized long getTimestamp() {
+        return image.getTimestamp();
+    }
+
+    @Override
+    public synchronized void setTimestamp(long timestamp) {
+        image.setTimestamp(timestamp);
+    }
+
+    @Override
+    public synchronized ImageProxy.PlaneProxy[] getPlanes() {
+        return image.getPlanes();
+    }
+
+    /**
+     * Adds a listener for close calls on this image.
+     *
+     * @param listener to add
+     */
+    synchronized void addOnImageCloseListener(OnImageCloseListener listener) {
+        onImageCloseListeners.add(listener);
+    }
+
+    /** Notifies the listeners that this image has been closed. */
+    protected synchronized void notifyOnImageCloseListeners() {
+        for (OnImageCloseListener listener : onImageCloseListeners) {
+            listener.onImageClose(this);
+        }
+    }
+
+    /** Listener for the image close event. */
+    interface OnImageCloseListener {
+        /**
+         * Callback for image close.
+         *
+         * @param image which is closed
+         */
+        void onImageClose(ImageProxy image);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ForwardingImageReaderListener.java b/camera/core/src/main/java/androidx/camera/core/ForwardingImageReaderListener.java
new file mode 100644
index 0000000..084561d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ForwardingImageReaderListener.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.Image;
+import android.media.ImageReader;
+
+import androidx.annotation.GuardedBy;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An {@link ImageReader.OnImageAvailableListener} which forks and forwards newly available images
+ * to multiple {@link ImageReaderProxy} instances.
+ */
+final class ForwardingImageReaderListener implements ImageReader.OnImageAvailableListener {
+    @GuardedBy("this")
+    private final List<QueuedImageReaderProxy> imageReaders;
+
+    /**
+     * Creates a new forwarding listener.
+     *
+     * @param imageReaders list of image readers which will receive a copy of every new image
+     * @return new {@link ForwardingImageReaderListener} instance
+     */
+    ForwardingImageReaderListener(List<QueuedImageReaderProxy> imageReaders) {
+        this.imageReaders = Collections.unmodifiableList(imageReaders);
+    }
+
+    @Override
+    public synchronized void onImageAvailable(ImageReader imageReader) {
+        Image image = imageReader.acquireNextImage();
+        ImageProxy imageProxy = new AndroidImageProxy(image);
+        ReferenceCountedImageProxy referenceCountedImageProxy =
+                new ReferenceCountedImageProxy(imageProxy);
+        for (QueuedImageReaderProxy imageReaderProxy : imageReaders) {
+            synchronized (imageReaderProxy) {
+                if (!imageReaderProxy.isClosed()) {
+                    ImageProxy forkedImage = referenceCountedImageProxy.fork();
+                    ForwardingImageProxy imageToEnqueue =
+                            ImageProxyDownsampler.downsample(
+                                    forkedImage,
+                                    imageReaderProxy.getWidth(),
+                                    imageReaderProxy.getHeight(),
+                                    ImageProxyDownsampler.DownsamplingMethod.AVERAGING);
+                    imageReaderProxy.enqueueImage(imageToEnqueue);
+                }
+            }
+        }
+        referenceCountedImageProxy.close();
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCase.java b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCase.java
new file mode 100644
index 0000000..5a5f92a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCase.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A use case providing CPU accessible images for an app to perform image analysis on.
+ *
+ * <p>Newly available images are acquired from the camera using an {@link ImageReader}. Each image
+ * is analyzed with an {@link ImageAnalysisUseCase.Analyzer} to produce a result. Then, the image is
+ * closed.
+ *
+ * <p>The result type, as well as distribution of the result, are left up to the implementation of
+ * the {@link Analyzer}.
+ */
+public final class ImageAnalysisUseCase extends BaseUseCase {
+    /**
+     * Provides a static configuration with implementation-agnostic options.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final Defaults DEFAULT_CONFIG = new Defaults();
+    private static final String TAG = "ImageAnalysisUseCase";
+    private final AtomicReference<Analyzer> subscribedAnalyzer;
+    private final AtomicInteger relativeRotation = new AtomicInteger();
+    private final Handler handler;
+    private final ImageAnalysisUseCaseConfiguration.Builder useCaseConfigBuilder;
+    @Nullable
+    private ImageReaderProxy imageReader;
+    /**
+     * Creates a new image analysis use case from the given configuration.
+     *
+     * @param configuration for this use case instance
+     */
+    public ImageAnalysisUseCase(ImageAnalysisUseCaseConfiguration configuration) {
+        super(configuration);
+        useCaseConfigBuilder = ImageAnalysisUseCaseConfiguration.Builder.fromConfig(configuration);
+
+        // Get the combined configuration with defaults
+        ImageAnalysisUseCaseConfiguration combinedConfig =
+                (ImageAnalysisUseCaseConfiguration) getUseCaseConfiguration();
+        subscribedAnalyzer = new AtomicReference<>();
+        handler = combinedConfig.getCallbackHandler(null);
+        if (handler == null) {
+            throw new IllegalStateException("No default handler specified.");
+        }
+        setImageFormat(ImageReaderFormatRecommender.chooseCombo().imageAnalysisFormat());
+    }
+
+    /**
+     * Removes a previously set analyzer.
+     *
+     * <p>This is equivalent to calling {@code setAnalyzer(null)}. Removing the analyzer will stop
+     * the stream of data from the camera.
+     */
+    @UiThread
+    public void removeAnalyzer() {
+        setAnalyzer(null);
+    }
+
+    /**
+     * Sets the rotation of the analysis pipeline.
+     *
+     * <p>This informs the use case of what the analyzer's reference rotation will be so it can
+     * adjust the rotation value sent to {@link Analyzer#analyze(ImageProxy, int)}.
+     *
+     * <p>In most cases this should be set to the current rotation returned by {@link
+     * Display#getRotation()}.
+     *
+     * @param rotation Desired rotation of the output image.
+     */
+    public void setTargetRotation(@RotationValue int rotation) {
+        ImageAnalysisUseCaseConfiguration oldconfig =
+                (ImageAnalysisUseCaseConfiguration) getUseCaseConfiguration();
+        int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+        if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+            useCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(useCaseConfigBuilder.build());
+
+            // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+            // For now we'll just update the relative rotation value.
+            // Attempt to get the camera ID and update the relative rotation. If we can't, we
+            // probably
+            // don't yet have permission, so we will try again in onSuggestedResolutionUpdated().
+            // Old
+            // configuration lens facing should match new configuration.
+            try {
+                String cameraId = CameraX.getCameraWithLensFacing(oldconfig.getLensFacing());
+                tryUpdateRelativeRotation(cameraId);
+            } catch (CameraInfoUnavailableException e) {
+                // Likely don't yet have permissions. This is expected if this method is called
+                // before
+                // this use case becomes active. That's OK though since we've updated the use case
+                // configuration. We'll try to update relative rotation again in
+                // onSuggestedResolutionUpdated().
+            }
+        }
+    }
+
+    /**
+     * Retrieves a previously set analyzer.
+     *
+     * @return The last set analyzer or {@code null} if no analyzer is set.
+     */
+    @UiThread
+    @Nullable
+    public Analyzer getAnalyzer() {
+        return subscribedAnalyzer.get();
+    }
+
+    /**
+     * Sets an analyzer to receive and analyze images.
+     *
+     * <p>Setting an analyzer will signal to the camera that it should begin sending data. The
+     * stream of data can be stopped by setting the analyzer to {@code null} or by calling {@link
+     * #removeAnalyzer()}.
+     *
+     * <p>Distribution of the result is left up to the implementation of the {@link Analyzer}.
+     *
+     * @param analyzer of the images or {@code null} to stop the stream of data.
+     */
+    @UiThread
+    public void setAnalyzer(@Nullable Analyzer analyzer) {
+        Analyzer previousAnalyzer = subscribedAnalyzer.getAndSet(analyzer);
+        if (previousAnalyzer == null && analyzer != null) {
+            notifyActive();
+        } else if (previousAnalyzer != null && analyzer == null) {
+            notifyInactive();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return TAG + ":" + getName();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void clear() {
+        if (imageReader != null) {
+            imageReader.close();
+            imageReader = null;
+        }
+        super.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @Nullable
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        ImageAnalysisUseCaseConfiguration defaults =
+                CameraX.getDefaultUseCaseConfiguration(ImageAnalysisUseCaseConfiguration.class);
+        if (defaults != null) {
+            return ImageAnalysisUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        ImageAnalysisUseCaseConfiguration configuration =
+                (ImageAnalysisUseCaseConfiguration) getUseCaseConfiguration();
+
+        String cameraId;
+        LensFacing lensFacing = configuration.getLensFacing();
+        try {
+            cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (CameraInfoUnavailableException e) {
+            throw new IllegalArgumentException(
+                    "Unable to find camera with LensFacing " + lensFacing, e);
+        }
+
+        Size resolution = suggestedResolutionMap.get(cameraId);
+        if (resolution == null) {
+            throw new IllegalArgumentException(
+                    "Suggested resolution map missing resolution for camera " + cameraId);
+        }
+
+        if (imageReader != null) {
+            imageReader.close();
+        }
+
+        imageReader =
+                ImageReaderProxys.createCompatibleReader(
+                        cameraId,
+                        resolution.getWidth(),
+                        resolution.getHeight(),
+                        getImageFormat(),
+                        configuration.getImageQueueDepth(),
+                        handler);
+
+        tryUpdateRelativeRotation(cameraId);
+        imageReader.setOnImageAvailableListener(
+                imageReader -> {
+                    Analyzer analyzer = subscribedAnalyzer.get();
+                    try (ImageProxy image =
+                                 configuration
+                                         .getImageReaderMode(configuration.getImageReaderMode())
+                                         .equals(ImageReaderMode.ACQUIRE_NEXT_IMAGE)
+                                         ? imageReader.acquireNextImage()
+                                         : imageReader.acquireLatestImage()) {
+                        // Do not analyze if unable to acquire an ImageProxy
+                        if (image == null) {
+                            return;
+                        }
+
+                        if (analyzer != null) {
+                            analyzer.analyze(image, relativeRotation.get());
+                        }
+                    }
+                },
+                handler);
+
+        SessionConfiguration.Builder sessionConfigBuilder =
+                SessionConfiguration.Builder.createFrom(configuration);
+        sessionConfigBuilder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+
+        attachToCamera(cameraId, sessionConfigBuilder.build());
+
+        return suggestedResolutionMap;
+    }
+
+    private void tryUpdateRelativeRotation(String cameraId) {
+        ImageOutputConfiguration configuration =
+                (ImageOutputConfiguration) getUseCaseConfiguration();
+        // Get the relative rotation or default to 0 if the camera info is unavailable
+        try {
+            CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+            relativeRotation.set(
+                    cameraInfo.getSensorRotationDegrees(
+                            configuration.getTargetRotation(Surface.ROTATION_0)));
+        } catch (CameraInfoUnavailableException e) {
+            Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
+        }
+    }
+
+    /**
+     * The different ways that the image sent to the analyzer is acquired from the underlying {@link
+     * ImageReader}. This corresponds to acquireLatestImage or acquireNextImage in {@link
+     * ImageReader}.
+     *
+     * @see android.media.ImageReader
+     */
+    public enum ImageReaderMode {
+        /** Acquires the latest image in the queue, discarding any images older than the latest. */
+        ACQUIRE_LATEST_IMAGE,
+        /** Acquires the next image in the queue. */
+        ACQUIRE_NEXT_IMAGE,
+    }
+
+    /** An analyzer of images. */
+    public interface Analyzer {
+        /**
+         * Analyzes an image to produce a result.
+         *
+         * <p>The caller is responsible for ensuring this analysis method can be executed quickly
+         * enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
+         * images will not be acquired and analyzed.
+         *
+         * <p>The image passed to this method becomes invalid after this method returns. The caller
+         * should not store external references to this image, as these references will become
+         * invalid.
+         *
+         * @param image           to analyze
+         * @param rotationDegrees The rotation required to match the rotation given by
+         *                        ImageOutputConfiguration#getTargetRotation(int).
+         */
+        void analyze(ImageProxy image, int rotationDegrees);
+    }
+
+    /**
+     * Provides a base static default configuration for the ImageAnalysisUseCase
+     *
+     * <p>These values may be overridden by the implementation. They only provide a minimum set of
+     * defaults that are implementation independent.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Defaults
+            implements ConfigurationProvider<ImageAnalysisUseCaseConfiguration> {
+        private static final ImageReaderMode DEFAULT_IMAGE_READER_MODE =
+                ImageReaderMode.ACQUIRE_NEXT_IMAGE;
+        private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+        private static final Rational DEFAULT_ASPECT_RATIO = new Rational(4, 3);
+        private static final int DEFAULT_IMAGE_QUEUE_DEPTH = 6;
+        private static final Size DEFAULT_TARGET_RESOLUTION = new Size(640, 480);
+        private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
+        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 1;
+
+        private static final ImageAnalysisUseCaseConfiguration DEFAULT_CONFIG;
+
+        static {
+            ImageAnalysisUseCaseConfiguration.Builder builder =
+                    new ImageAnalysisUseCaseConfiguration.Builder()
+                            .setImageReaderMode(DEFAULT_IMAGE_READER_MODE)
+                            .setCallbackHandler(DEFAULT_HANDLER)
+                            .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+                            .setImageQueueDepth(DEFAULT_IMAGE_QUEUE_DEPTH)
+                            .setTargetResolution(DEFAULT_TARGET_RESOLUTION)
+                            .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+                            .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+            DEFAULT_CONFIG = builder.build();
+        }
+
+        @Override
+        public ImageAnalysisUseCaseConfiguration getConfiguration() {
+            return DEFAULT_CONFIG;
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCaseConfiguration.java
new file mode 100644
index 0000000..9826fd4
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCaseConfiguration.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.ImageReader;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageAnalysisUseCase.ImageReaderMode;
+
+/** Configuration for an image analysis use case. */
+public final class ImageAnalysisUseCaseConfiguration
+        implements UseCaseConfiguration<ImageAnalysisUseCase>,
+        ImageOutputConfiguration,
+        CameraDeviceConfiguration,
+        ThreadConfiguration {
+
+    // Option Declarations:
+    // ***********************************************************************************************
+    static final Option<ImageReaderMode> OPTION_IMAGE_READER_MODE =
+            Option.create("camerax.core.imageAnalysis.imageReaderMode", ImageReaderMode.class);
+    static final Option<Integer> OPTION_IMAGE_QUEUE_DEPTH =
+            Option.create("camerax.core.imageAnalysis.imageQueueDepth", int.class);
+    private final OptionsBundle config;
+
+    ImageAnalysisUseCaseConfiguration(OptionsBundle config) {
+        this.config = config;
+    }
+
+    /**
+     * Returns the mode that the image is acquired from {@link ImageReader}.
+     *
+     * <p>The available values are {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE} and {@link
+     * ImageReaderMode#ACQUIRE_LATEST_IMAGE}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    public ImageReaderMode getImageReaderMode(@Nullable ImageReaderMode valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_IMAGE_READER_MODE, valueIfMissing);
+    }
+
+    /**
+     * Returns the mode that the image is acquired from {@link ImageReader}.
+     *
+     * <p>The available values are {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE} and {@link
+     * ImageReaderMode#ACQUIRE_LATEST_IMAGE}.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public ImageReaderMode getImageReaderMode() {
+        return getConfiguration().retrieveOption(OPTION_IMAGE_READER_MODE);
+    }
+
+    /**
+     * Returns the number of images available to the camera pipeline.
+     *
+     * <p>The image queue depth is the total number of images, including the image being analyzed,
+     * available to the camera pipeline. If analysis takes long enough, the image queue may become
+     * full and stall the camera pipeline.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getImageQueueDepth(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_IMAGE_QUEUE_DEPTH, valueIfMissing);
+    }
+
+    /**
+     * Returns the number of images available to the camera pipeline.
+     *
+     * <p>The image queue depth is the total number of images, including the image being analyzed,
+     * available to the camera pipeline. If analysis takes long enough, the image queue may become
+     * full and stall the camera pipeline.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getImageQueueDepth() {
+        return getConfiguration().retrieveOption(OPTION_IMAGE_QUEUE_DEPTH);
+    }
+
+    /**
+     * Retrieves the resolution of the target intending to use from this configuration.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Override
+    public Size getTargetResolution(Size valueIfMissing) {
+        return getConfiguration()
+                .retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the resolution of the target intending to use from this configuration.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    @Override
+    public Size getTargetResolution() {
+        return getConfiguration().retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** Builder for a {@link ImageAnalysisUseCaseConfiguration}. */
+    public static final class Builder
+            implements CameraDeviceConfiguration.Builder<
+            ImageAnalysisUseCaseConfiguration, Builder>,
+            ImageOutputConfiguration.Builder<ImageAnalysisUseCaseConfiguration, Builder>,
+            ThreadConfiguration.Builder<ImageAnalysisUseCaseConfiguration, Builder>,
+            UseCaseConfiguration.Builder<
+                    ImageAnalysisUseCase, ImageAnalysisUseCaseConfiguration, Builder> {
+        private final MutableOptionsBundle mutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            this.mutableConfig = mutableConfig;
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(ImageAnalysisUseCase.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass(ImageAnalysisUseCase.class);
+        }
+
+        /**
+         * Generates a Builder from another Configuration object.
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        public static Builder fromConfig(ImageAnalysisUseCaseConfiguration configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * Sets the mode that the image is acquired from {@link ImageReader}.
+         *
+         * <p>The available values are {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE} and {@link
+         * ImageReaderMode#ACQUIRE_LATEST_IMAGE}.
+         *
+         * @param mode The mode to set.
+         * @return The current Builder.
+         */
+        public Builder setImageReaderMode(ImageReaderMode mode) {
+            getMutableConfiguration().insertOption(OPTION_IMAGE_READER_MODE, mode);
+            return builder();
+        }
+
+        /**
+         * Sets the number of images available to the camera pipeline.
+         *
+         * <p>The image queue depth is the number of images available to the camera to fill with
+         * data. This includes the image currently being analyzed by {@link
+         * ImageAnalysisUseCase.Analyzer#analyze(ImageProxy, int)}. Increasing the image queue depth
+         * may make camera operation smoother, depending on the {@link ImageReaderMode}, at the cost
+         * of increased memory usage.
+         *
+         * <p>When the {@link ImageReaderMode} is set to {@link
+         * ImageReaderMode#ACQUIRE_LATEST_IMAGE}, increasing the image queue depth will increase the
+         * amount of time available to analyze an image before stalling the capture pipeline.
+         *
+         * <p>When the {@link ImageReaderMode} is set to {@link ImageReaderMode#ACQUIRE_NEXT_IMAGE},
+         * increasing the image queue depth may make the camera pipeline run smoother on systems
+         * under high load. However, the time spent analyzing an image should still be kept under a
+         * single frame period for the current frame rate, on average, to avoid stalling the camera
+         * pipeline.
+         *
+         * @param depth The total number of images available to the camera.
+         * @return The current Builder.
+         */
+        public Builder setImageQueueDepth(int depth) {
+            getMutableConfiguration().insertOption(OPTION_IMAGE_QUEUE_DEPTH, depth);
+            return builder();
+        }
+
+        /**
+         * Sets the resolution of the intended target from this configuration.
+         *
+         * <p>The target resolution attempts to establish a minimum bound for the image resolution.
+         * The actual image resolution will be the closest available resolution in size that is not
+         * smaller than the target resolution, as determined by the Camera implementation. However,
+         * if no resolution exists that is equal to or larger than the target resolution, the
+         * nearest available resolution smaller than the target resolution will be chosen.
+         *
+         * @param resolution The target resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         */
+        @Override
+        public Builder setTargetResolution(Size resolution) {
+            getMutableConfiguration()
+                    .insertOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, resolution);
+            return builder();
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public ImageAnalysisUseCaseConfiguration build() {
+            return new ImageAnalysisUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCase.java b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCase.java
new file mode 100644
index 0000000..595c4ae
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCase.java
@@ -0,0 +1,1035 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.location.Location;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraCaptureMetaData.AeState;
+import androidx.camera.core.CameraCaptureMetaData.AfMode;
+import androidx.camera.core.CameraCaptureMetaData.AfState;
+import androidx.camera.core.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.CameraCaptureResult.EmptyCameraCaptureResult;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.io.File;
+import java.util.ArrayDeque;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A use case for taking a picture.
+ *
+ * <p>This class is designed for basic picture taking. It provides simple controls on how a picture
+ * will be taken. The caller is responsible for deciding how to use the captured picture, such as
+ * saving the picture to a file.
+ *
+ * <p>The captured image is made available through an {@link ImageReader} which is passed to an
+ * {@link ImageCaptureUseCase.OnImageCapturedListener}.
+ */
+public class ImageCaptureUseCase extends BaseUseCase {
+    /**
+     * Provides a static configuration with implementation-agnostic options.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final Defaults DEFAULT_CONFIG = new Defaults();
+    private static final String TAG = "ImageCaptureUseCase";
+    private static final long CHECK_3A_TIMEOUT_IN_MS = 1000L;
+    private static final int MAX_IMAGES = 2;
+    // Empty metadata object used as a placeholder for no user-supplied metadata.
+    // Should be initialized to all default values.
+    private static final Metadata EMPTY_METADATA = new Metadata();
+    final Handler handler;
+    final Handler mainHandler = new Handler(Looper.getMainLooper());
+    private final SessionConfiguration.Builder sessionConfigBuilder;
+    private final ArrayDeque<ImageCaptureRequest> imageCaptureRequests = new ArrayDeque<>();
+    private final ExecutorService executor =
+            Executors.newFixedThreadPool(
+                    1,
+                    new ThreadFactory() {
+                        private final AtomicInteger id = new AtomicInteger(0);
+
+                        @Override
+                        public Thread newThread(Runnable r) {
+                            return new Thread(
+                                    r,
+                                    CameraXThreads.TAG + "image_capture_" + id.getAndIncrement());
+                        }
+                    });
+    private final CaptureCallbackChecker sessionCallbackChecker = new CaptureCallbackChecker();
+    private final CaptureMode captureMode;
+    private final ImageCaptureUseCaseConfiguration.Builder useCaseConfigBuilder;
+    private ImageCaptureUseCaseConfiguration configuration;
+    private ImageReaderProxy imageReader;
+    /**
+     * A flag to check 3A converged or not.
+     *
+     * <p>In order to speed up the taking picture process, trigger AF / AE should be skipped when
+     * the flag is disabled. Set it to be enabled in the maximum quality mode and disabled in the
+     * minimum latency mode.
+     */
+    private boolean enableCheck3AConverged;
+    /** Current flash mode. */
+    private FlashMode flashMode;
+    /**
+     * Creates a new image capture use case from the given configuration.
+     *
+     * @param userConfiguration for this use case instance
+     */
+    public ImageCaptureUseCase(ImageCaptureUseCaseConfiguration userConfiguration) {
+        super(userConfiguration);
+        useCaseConfigBuilder =
+                ImageCaptureUseCaseConfiguration.Builder.fromConfig(userConfiguration);
+        setImageFormat(ImageReaderFormatRecommender.chooseCombo().imageCaptureFormat());
+        // Ensure we're using the combined configuration (user config + defaults)
+        configuration = (ImageCaptureUseCaseConfiguration) getUseCaseConfiguration();
+        captureMode = configuration.getCaptureMode();
+        flashMode = configuration.getFlashMode();
+
+        if (captureMode == CaptureMode.MAX_QUALITY) {
+            enableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
+        } else if (captureMode == CaptureMode.MIN_LATENCY) {
+            enableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
+        }
+
+        handler = configuration.getCallbackHandler(null);
+        if (handler == null) {
+            throw new IllegalStateException("No default handler specified.");
+        }
+
+        sessionConfigBuilder = SessionConfiguration.Builder.createFrom(configuration);
+        sessionConfigBuilder.setCameraCaptureCallback(sessionCallbackChecker);
+    }
+
+    private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+        try {
+            return CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to get camera id for camera lens facing " + lensFacing, e);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @Nullable
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        ImageCaptureUseCaseConfiguration defaults =
+                CameraX.getDefaultUseCaseConfiguration(ImageCaptureUseCaseConfiguration.class);
+        if (defaults != null) {
+            return ImageCaptureUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    private CameraControl getCurrentCameraControl() {
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+        return getCameraControl(cameraId);
+    }
+
+    /** Configures flash mode to CameraControl once it is ready. */
+    @Override
+    protected void onCameraControlReady(String cameraId) {
+        getCameraControl(cameraId).setFlashMode(flashMode);
+    }
+
+    /**
+     * Get the flash mode.
+     *
+     * @return the {@link FlashMode}.
+     */
+    public FlashMode getFlashMode() {
+        return flashMode;
+    }
+
+    /**
+     * Set the flash mode.
+     *
+     * @param flashMode the {@link FlashMode}.
+     */
+    public void setFlashMode(FlashMode flashMode) {
+        this.flashMode = flashMode;
+        getCurrentCameraControl().setFlashMode(flashMode);
+    }
+
+    /**
+     * Sets target aspect ratio.
+     *
+     * @param aspectRatio New target aspect ratio.
+     */
+    public void setTargetAspectRatio(Rational aspectRatio) {
+        ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+        Rational oldRatio = oldconfig.getTargetAspectRatio(null);
+        if (!aspectRatio.equals(oldRatio)) {
+            useCaseConfigBuilder.setTargetAspectRatio(aspectRatio);
+            updateUseCaseConfiguration(useCaseConfigBuilder.build());
+            configuration = (ImageCaptureUseCaseConfiguration) getUseCaseConfiguration();
+
+            // TODO(b/122846516): Reconfigure capture session if the ratio is changed drastically.
+        }
+    }
+
+    /**
+     * Sets the desired rotation of the output image.
+     *
+     * <p>This will affect the rotation of the saved image or the rotation value returned by the
+     * {@link OnImageCapturedListener}.
+     *
+     * <p>In most cases this should be set to the current rotation returned by {@link
+     * Display#getRotation()}.
+     *
+     * @param rotation Desired rotation of the output image.
+     */
+    public void setTargetRotation(@RotationValue int rotation) {
+        ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+        int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+        if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+            useCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(useCaseConfigBuilder.build());
+            configuration = (ImageCaptureUseCaseConfiguration) getUseCaseConfiguration();
+
+            // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+        }
+    }
+
+    /**
+     * Captures a new still image.
+     *
+     * <p>The listener's callback will be called only once for every invocation of this method. The
+     * listener is responsible for calling {@link Image#close()} on the returned image.
+     *
+     * @param listener for the newly captured image
+     * @hide
+     */
+    public void takePicture(OnImageCapturedListener listener) {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mainHandler.post(
+                    () -> {
+                        takePicture(listener);
+                    });
+            return;
+        }
+
+        sendImageCaptureRequest(listener, handler);
+    }
+
+    /**
+     * Captures a new still image and saves to disk.
+     *
+     * <p>The listener's callback will be called only once for every invocation of this method.
+     *
+     * @param saveLocation       Location to store the newly captured image.
+     * @param imageSavedListener Listener to be called for the newly captured image.
+     */
+    public void takePicture(File saveLocation, OnImageSavedListener imageSavedListener) {
+        takePicture(saveLocation, imageSavedListener, EMPTY_METADATA);
+    }
+
+    /**
+     * Captures a new still image and saves to disk.
+     *
+     * <p>The listener's callback will be called only once for every invocation of this method.
+     *
+     * @param saveLocation       Location to store the newly captured image.
+     * @param imageSavedListener Listener to be called for the newly captured image.
+     * @param metadata           Metadata to be stored with the saved image. For JPEG this will
+     *                           be included in
+     *                           EXIF.
+     */
+    public void takePicture(
+            File saveLocation, OnImageSavedListener imageSavedListener, Metadata metadata) {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mainHandler.post(
+                    () -> {
+                        takePicture(saveLocation, imageSavedListener, metadata);
+                    });
+            return;
+        }
+
+        /*
+         * We need to chain the following callbacks to save the image to disk:
+         *
+         * +-----------------------+
+         * |                       |
+         * |ImageCaptureUseCase.   |
+         * |OnImageCapturedListener|
+         * |                       |
+         * +-----------+-----------+
+         *             |
+         *             |
+         * +-----------v-----------+      +----------------------+
+         * |                       |      |                      |
+         * | ImageSaver.           |      | ImageCaptureUseCase. |
+         * | OnImageSavedListener  +------> OnImageSavedListener |
+         * |                       |      |                      |
+         * +-----------------------+      +----------------------+
+         */
+
+        // Convert the ImageSaver.OnImageSavedListener to ImageCaptureUseCase.OnImageSavedListener
+        ImageSaver.OnImageSavedListener imageSavedListenerWrapper =
+                new ImageSaver.OnImageSavedListener() {
+                    @Override
+                    public void onImageSaved(File file) {
+                        imageSavedListener.onImageSaved(file);
+                    }
+
+                    @Override
+                    public void onError(
+                            ImageSaver.SaveError error, String message, @Nullable Throwable cause) {
+                        UseCaseError useCaseError = UseCaseError.UNKNOWN_ERROR;
+                        switch (error) {
+                            case FILE_IO_FAILED:
+                                useCaseError = UseCaseError.FILE_IO_ERROR;
+                                break;
+                            default:
+                                // Keep the useCaseError as UNKNOWN_ERROR
+                                break;
+                        }
+
+                        imageSavedListener.onError(useCaseError, message, cause);
+                    }
+                };
+
+        Rational targetRatio = configuration.getTargetAspectRatio();
+
+        // Wrap the ImageCaptureUseCase.OnImageSavedListener with an OnImageCapturedListener so it
+        // can
+        // be put into the capture request queue
+        OnImageCapturedListener imageCaptureCallbackWrapper =
+                new OnImageCapturedListener() {
+                    @Override
+                    public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                        Handler completionHandler = (handler != null) ? handler : mainHandler;
+                        IoExecutor.getInstance()
+                                .execute(
+                                        new ImageSaver(
+                                                image,
+                                                saveLocation,
+                                                rotationDegrees,
+                                                metadata.isReversedHorizontal,
+                                                metadata.isReversedVertical,
+                                                metadata.location,
+                                                targetRatio,
+                                                imageSavedListenerWrapper,
+                                                completionHandler));
+                    }
+
+                    @Override
+                    public void onError(
+                            UseCaseError error, String message, @Nullable Throwable cause) {
+                        imageSavedListener.onError(error, message, cause);
+                    }
+                };
+
+        // Always use the mainHandler for the initial callback so we don't need to double post to
+        // another thread
+        sendImageCaptureRequest(imageCaptureCallbackWrapper, mainHandler);
+    }
+
+    @UiThread
+    private void sendImageCaptureRequest(
+            OnImageCapturedListener listener, @Nullable Handler listenerHandler) {
+
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+
+        // Get the relative rotation or default to 0 if the camera info is unavailable
+        int relativeRotation = 0;
+        try {
+            CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+            relativeRotation =
+                    cameraInfo.getSensorRotationDegrees(
+                            configuration.getTargetRotation(Surface.ROTATION_0));
+        } catch (CameraInfoUnavailableException e) {
+            Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
+        }
+
+        imageCaptureRequests.offer(
+                new ImageCaptureRequest(listener, listenerHandler, relativeRotation));
+        if (imageCaptureRequests.size() == 1) {
+            issueImageCaptureRequests();
+        }
+    }
+
+    /** Issues saved ImageCaptureRequest. */
+    @UiThread
+    private void issueImageCaptureRequests() {
+        if (imageCaptureRequests.isEmpty()) {
+            return;
+        }
+        takePictureInternal();
+    }
+
+    /**
+     * The take picture flow.
+     *
+     * <p>There are three steps to take a picture.
+     *
+     * <p>(1) Pre-take picture, which will trigger af/ae scan or open torch if necessary. Then check
+     * 3A converged if necessary.
+     *
+     * <p>(2) Issue take picture single request.
+     *
+     * <p>(3) Post-take picture, which will cancel af/ae scan or close torch if necessary.
+     */
+    private void takePictureInternal() {
+        TakePictureState state = new TakePictureState();
+
+        FluentFuture.from(preTakePicture(state))
+                .transformAsync(v -> issueTakePicture(), executor)
+                .transformAsync(v -> postTakePicture(state), executor)
+                .addCallback(
+                        new FutureCallback<Void>() {
+                            @Override
+                            public void onSuccess(Void result) {
+                            }
+
+                            @Override
+                            public void onFailure(Throwable t) {
+                                Log.e(TAG, "takePictureInternal onFailure", t);
+                            }
+                        },
+                        executor);
+    }
+
+    @Override
+    public String toString() {
+        return TAG + ":" + getName();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void clear() {
+        if (imageReader != null) {
+            imageReader.close();
+            imageReader = null;
+        }
+        executor.shutdown();
+        super.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+        Size resolution = suggestedResolutionMap.get(cameraId);
+        if (resolution == null) {
+            throw new IllegalArgumentException(
+                    "Suggested resolution map missing resolution for camera " + cameraId);
+        }
+
+        if (imageReader != null) {
+            if (imageReader.getHeight() == resolution.getHeight()
+                    && imageReader.getWidth() == resolution.getWidth()) {
+                // Resolution does not need to be updated. Return early.
+                return suggestedResolutionMap;
+            }
+            imageReader.close();
+        }
+
+        imageReader =
+                ImageReaderProxys.createCompatibleReader(
+                        cameraId,
+                        resolution.getWidth(),
+                        resolution.getHeight(),
+                        getImageFormat(),
+                        MAX_IMAGES,
+                        handler);
+
+        imageReader.setOnImageAvailableListener(
+                imageReader -> {
+                    // Call the listener so that the captured image can be processed.
+                    ImageCaptureRequest imageCaptureRequest = imageCaptureRequests.peek();
+                    if (imageCaptureRequest != null) {
+                        ImageProxy image = null;
+                        try {
+                            image = imageReader.acquireLatestImage();
+                        } catch (IllegalStateException e) {
+                            Log.e(TAG, "Failed to acquire latest image.", e);
+                        } finally {
+                            if (image != null) {
+                                // Remove the first listener from the queue
+                                imageCaptureRequests.poll();
+
+                                // Inform the listener
+                                imageCaptureRequest.dispatchImage(image);
+
+                                issueImageCaptureRequests();
+                            }
+                        }
+                    } else {
+                        // Flush the queue if we have no requests
+                        ImageProxy image = null;
+                        try {
+                            image = imageReader.acquireLatestImage();
+                        } catch (IllegalStateException e) {
+                            Log.e(TAG, "Failed to acquire latest image.", e);
+                        } finally {
+                            if (image != null) {
+                                image.close();
+                            }
+                        }
+                    }
+                },
+                mainHandler);
+
+        sessionConfigBuilder.clearSurfaces();
+        sessionConfigBuilder.addNonRepeatingSurface(new ImmediateSurface(imageReader.getSurface()));
+
+        attachToCamera(cameraId, sessionConfigBuilder.build());
+
+        // In order to speed up the take picture process, notifyActive at an early stage to attach
+        // the
+        // session capture callback to repeating and get capture result all the time.
+        notifyActive();
+
+        return suggestedResolutionMap;
+    }
+
+    /**
+     * Routine before taking picture.
+     *
+     * <p>For example, trigger 3A scan, open torch and check 3A converged if necessary.
+     */
+    private ListenableFuture<Void> preTakePicture(TakePictureState state) {
+        return FluentFuture.from(getPreCaptureStateIfNeeded())
+                .transformAsync(
+                        captureResult -> {
+                            state.preCaptureState = captureResult;
+                            triggerAfIfNeeded(state);
+
+                            if (isFlashRequired(state)) {
+                                state.isFlashTriggered = true;
+                                triggerAePrecapture(state);
+                            }
+                            return check3AConverged(state);
+                        },
+                        executor)
+                // Ignore the 3A convergence result.
+                .transform(is3AConverged -> null, executor);
+    }
+
+    /**
+     * Routine after picture was taken.
+     *
+     * <p>For example, cancel 3A scan, close torch if necessary.
+     */
+    private ListenableFuture<Void> postTakePicture(TakePictureState state) {
+        return Futures.submitAsync(
+                () -> {
+                    cancelAfAeTrigger(state);
+                    return Futures.immediateFuture(null);
+                },
+                executor);
+    }
+
+    /**
+     * Gets a capture result or not according to current configuration.
+     *
+     * <p>Conditions to get a capture result.
+     *
+     * <p>(1) The enableCheck3AConverged is enabled because it needs to know current AF mode and
+     * state.
+     *
+     * <p>(2) The flashMode is AUTO because it needs to know the current AE state.
+     */
+    // Currently this method is used to prevent there is no repeating surface to get capture result.
+    // If app is in min-latency mode and flash ALWAYS/OFF mode, it can still take picture without
+    // checking the capture result. Remove this check once no repeating surface issue is fixed.
+    private ListenableFuture<CameraCaptureResult> getPreCaptureStateIfNeeded() {
+        if (enableCheck3AConverged || getFlashMode() == FlashMode.AUTO) {
+            return sessionCallbackChecker.checkCaptureResult((captureResult) -> captureResult);
+        }
+        return Futures.immediateFuture(null);
+    }
+
+    private boolean isFlashRequired(TakePictureState state) {
+        switch (getFlashMode()) {
+            case ON:
+                return true;
+            case AUTO:
+                return state.preCaptureState.getAeState() == AeState.FLASH_REQUIRED;
+            case OFF:
+                return false;
+        }
+        throw new AssertionError(getFlashMode());
+    }
+
+    private ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
+        // Besides enableCheck3AConverged == true (MAX_QUALITY), if flash is triggered we also need
+        // to
+        // wait for 3A convergence.
+        if (!enableCheck3AConverged && !state.isFlashTriggered) {
+            return Futures.immediateFuture(false);
+        }
+
+        return sessionCallbackChecker.checkCaptureResult(
+                (captureResult) -> {
+                    // If afMode is CAF, don't check af locked to speed up.
+                    if ((captureResult.getAfMode() == AfMode.ON_CONTINUOUS_AUTO
+                            || (captureResult.getAfState() == AfState.FOCUSED
+                            || captureResult.getAfState() == AfState.LOCKED_FOCUSED
+                            || captureResult.getAfState()
+                            == AfState.LOCKED_NOT_FOCUSED))
+                            && captureResult.getAeState() == AeState.CONVERGED
+                            && captureResult.getAwbState() == AwbState.CONVERGED) {
+                        return true;
+                    }
+                    // Return null to continue check.
+                    return null;
+                },
+                CHECK_3A_TIMEOUT_IN_MS,
+                false);
+    }
+
+    /**
+     * Issues the AF scan if needed.
+     *
+     * <p>If enableCheck3AConverged is disabled or it is in CAF mode, AF scan should not be
+     * triggered. Trigger AF scan only in {@link AfMode#ON_MANUAL_AUTO} and current AF state is
+     * {@link AfState#INACTIVE}. If the AF mode is {@link AfMode#ON_MANUAL_AUTO} and AF state is not
+     * inactive, it means that a manual or auto focus request may be in progress or completed.
+     */
+    private void triggerAfIfNeeded(TakePictureState state) {
+        if (enableCheck3AConverged
+                && state.preCaptureState.getAfMode() == AfMode.ON_MANUAL_AUTO
+                && state.preCaptureState.getAfState() == AfState.INACTIVE) {
+            triggerAf(state);
+        }
+    }
+
+    /**
+     * Issues a {@link CaptureRequest#CONTROL_AF_TRIGGER_START} request to start auto focus scan.
+     */
+    private void triggerAf(TakePictureState state) {
+        state.isAfTriggered = true;
+        getCurrentCameraControl().triggerAf();
+    }
+
+    /**
+     * Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
+     * exposure scan.
+     */
+    private void triggerAePrecapture(TakePictureState state) {
+        state.isAePrecaptureTriggered = true;
+        getCurrentCameraControl().triggerAePrecapture();
+    }
+
+    /**
+     * Issues {@link CaptureRequest#CONTROL_AF_TRIGGER_CANCEL} or {@link
+     * CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL} request to cancel auto focus or auto
+     * exposure scan.
+     */
+    private void cancelAfAeTrigger(TakePictureState state) {
+        if (!state.isAfTriggered && !state.isAePrecaptureTriggered) {
+            return;
+        }
+        getCurrentCameraControl()
+                .cancelAfAeTrigger(state.isAfTriggered, state.isAePrecaptureTriggered);
+        state.isAfTriggered = false;
+        state.isAePrecaptureTriggered = false;
+    }
+
+    // TODO(b/123897971):  move the device specific code once we complete the device workaround
+    // module.
+    private void applyPixelHdrPlusChangeForCaptureMode(
+            CaptureMode captureMode, CaptureRequestConfiguration.Builder takePhotoRequestBuilder) {
+        if (Build.MANUFACTURER.equals("Google")
+                && (Build.MODEL.equals("Pixel 2") || Build.MODEL.equals("Pixel 3"))) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                switch (captureMode) {
+                    case MAX_QUALITY:
+                        // enable ZSL to make sure HDR+ is enabled
+                        takePhotoRequestBuilder.addCharacteristic(
+                                CaptureRequest.CONTROL_ENABLE_ZSL, true);
+                        break;
+                    case MIN_LATENCY:
+                        // disable ZSL to turn off HDR+
+                        takePhotoRequestBuilder.addCharacteristic(
+                                CaptureRequest.CONTROL_ENABLE_ZSL, false);
+                        break;
+                }
+            }
+        }
+    }
+
+    /** Issues a take picture request. */
+    private ListenableFuture<Void> issueTakePicture() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+        builder.addSurface(new ImmediateSurface(imageReader.getSurface()));
+        builder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+
+        applyPixelHdrPlusChangeForCaptureMode(captureMode, builder);
+
+        SettableFuture<Void> future = SettableFuture.create();
+        builder.setCameraCaptureCallback(
+                new CameraCaptureCallback() {
+                    @Override
+                    public void onCaptureCompleted(@NonNull CameraCaptureResult result) {
+                        future.set(null);
+                    }
+
+                    @Override
+                    public void onCaptureFailed(@NonNull CameraCaptureFailure failure) {
+                        Log.e(
+                                TAG,
+                                "capture picture get onCaptureFailed with reason "
+                                        + failure.getReason());
+                        future.set(null);
+                    }
+                });
+        notifySingleCapture(builder.build());
+        return future;
+    }
+
+    /**
+     * Describes the error that occurred during an image capture operation (such as {@link
+     * ImageCaptureUseCase.takePicture()}).
+     *
+     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
+     * ImageCaptureUseCase.OnImageSavedListener.onError}.
+     */
+    public enum UseCaseError {
+        /**
+         * An unknown error occurred.
+         *
+         * <p>See message parameter in onError callback or log for more details.
+         */
+        UNKNOWN_ERROR,
+        /**
+         * An error occurred while attempting to read or write a file, such as when saving an image
+         * to a File.
+         */
+        FILE_IO_ERROR
+    }
+
+    /**
+     * Capture mode options for ImageCaptureUseCase. A picture will always be taken regardless of
+     * mode, and the mode will be used on devices that support it.
+     */
+    public enum CaptureMode {
+        /**
+         * Optimizes capture pipeline to prioritize image quality over latency. When the capture
+         * mode is set to MAX_QUALITY, images may take longer to capture.
+         */
+        MAX_QUALITY,
+        /**
+         * Optimizes capture pipeline to prioritize latency over image quality. When the capture
+         * mode is set to MIN_LATENCY, images may capture faster but the image quality may be
+         * reduced.
+         */
+        MIN_LATENCY
+    }
+
+    /** Listener containing callbacks for image file I/O events. */
+    public interface OnImageSavedListener {
+        /** Called when an image has been successfully saved. */
+        void onImageSaved(@NonNull File file);
+
+        /** Called when an error occurs while attempting to save an image. */
+        void onError(
+                @NonNull UseCaseError useCaseError,
+                @NonNull String message,
+                @Nullable Throwable cause);
+    }
+
+    /**
+     * Listener called when an image capture has completed.
+     *
+     * @hide
+     */
+    public interface OnImageCapturedListener {
+        /**
+         * Callback for when the image has been captured.
+         *
+         * <p>The listener is responsible for closing the supplied {@link Image}.
+         */
+        default void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+            image.close();
+        }
+
+        /** Callback for when an error occurred during image capture. */
+        default void onError(
+                UseCaseError useCaseError, String message, @Nullable Throwable cause) {
+        }
+    }
+
+    /**
+     * Provides a base static default configuration for the ImageCaptureUseCase
+     *
+     * <p>These values may be overridden by the implementation. They only provide a minimum set of
+     * defaults that are implementation independent.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Defaults
+            implements ConfigurationProvider<ImageCaptureUseCaseConfiguration> {
+        private static final CaptureMode DEFAULT_CAPTURE_MODE = CaptureMode.MIN_LATENCY;
+        private static final FlashMode DEFAULT_FLASH_MODE = FlashMode.OFF;
+        private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+        private static final Rational DEFAULT_ASPECT_RATIO = new Rational(4, 3);
+        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 4;
+
+        private static final ImageCaptureUseCaseConfiguration DEFAULT_CONFIG;
+
+        static {
+            ImageCaptureUseCaseConfiguration.Builder builder =
+                    new ImageCaptureUseCaseConfiguration.Builder()
+                            .setCaptureMode(DEFAULT_CAPTURE_MODE)
+                            .setFlashMode(DEFAULT_FLASH_MODE)
+                            .setCallbackHandler(DEFAULT_HANDLER)
+                            .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+                            .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+            DEFAULT_CONFIG = builder.build();
+        }
+
+        @Override
+        public ImageCaptureUseCaseConfiguration getConfiguration() {
+            return DEFAULT_CONFIG;
+        }
+    }
+
+    /** Holder class for metadata that will be saved with captured images. */
+    public static final class Metadata {
+        /**
+         * Indicates an upside down mirroring, equivalent to a horizontal mirroring (reflection)
+         * followed by a 180 degree rotation.
+         */
+        public boolean isReversedHorizontal;
+        /** Indicates a left-right mirroring (reflection). */
+        public boolean isReversedVertical;
+        /** Data representing a geographic location. */
+        public @Nullable
+        Location location;
+    }
+
+    /**
+     * An intermediate action recorder while taking picture. It is used to restore certain states.
+     * For example, cancel AF/AE scan, and close flash light.
+     */
+    static final class TakePictureState {
+        CameraCaptureResult preCaptureState = EmptyCameraCaptureResult.create();
+        boolean isAfTriggered = false;
+        boolean isAePrecaptureTriggered = false;
+        boolean isFlashTriggered = false;
+    }
+
+    /**
+     * A helper class to check camera capture result.
+     *
+     * <p>CaptureCallbackChecker is an implementation of {@link CameraCaptureCallback} that checks a
+     * specified list of condition and sets a ListenableFuture when the conditions have been met. It
+     * is mainly used to continuously capture callbacks to detect specific conditions. It also
+     * handles the timeout condition if the check condition does not satisfy the given timeout, and
+     * returns the given default value if the timeout is met.
+     */
+    static final class CaptureCallbackChecker extends CameraCaptureCallback {
+        private static final long NO_TIMEOUT = 0L;
+
+        /** Capture listeners. */
+        private final Set<CaptureResultListener> captureResultListeners = new HashSet<>();
+
+        @Override
+        public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
+            deliverCaptureResultToListeners(cameraCaptureResult);
+        }
+
+        /**
+         * Check the capture results of current session capture callback by giving a {@link
+         * CaptureResultChecker}.
+         *
+         * @param checker a CaptureResult checker that returns an object with type T if the check is
+         *                complete, returning null to continue the check process.
+         * @param <T>     the type parameter for CaptureResult checker.
+         * @return a listenable future for capture result check process.
+         */
+        public <T> ListenableFuture<T> checkCaptureResult(CaptureResultChecker<T> checker) {
+            return checkCaptureResult(checker, NO_TIMEOUT, null);
+        }
+
+        /**
+         * Check the capture results of current session capture callback with timeout limit by
+         * giving a {@link CaptureResultChecker}.
+         *
+         * @param checker     a CaptureResult checker that returns an object with type T if the
+         *                    check is
+         *                    complete, returning null to continue the check process.
+         * @param timeoutInMs used to force stop checking.
+         * @param defValue    the default return value if timeout occur.
+         * @param <T>         the type parameter for CaptureResult checker.
+         * @return a listenable future for capture result check process.
+         */
+        public <T> ListenableFuture<T> checkCaptureResult(
+                CaptureResultChecker<T> checker, long timeoutInMs, T defValue) {
+            if (timeoutInMs < NO_TIMEOUT) {
+                throw new IllegalArgumentException("Invalid timeout value: " + timeoutInMs);
+            }
+            long startTimeInMs = (timeoutInMs != NO_TIMEOUT) ? SystemClock.elapsedRealtime() : 0L;
+
+            SettableFuture<T> future = SettableFuture.create();
+            addListener(
+                    (captureResult) -> {
+                        T result = checker.check(captureResult);
+                        if (result != null) {
+                            future.set(result);
+                            return true;
+                        } else if (startTimeInMs > 0
+                                && SystemClock.elapsedRealtime() - startTimeInMs > timeoutInMs) {
+                            future.set(defValue);
+                            return true;
+                        }
+                        // Return false to continue check.
+                        return false;
+                    });
+            return future;
+        }
+
+        /**
+         * Delivers camera capture result to {@link CaptureCallbackChecker#captureResultListeners}.
+         */
+        private void deliverCaptureResultToListeners(@NonNull CameraCaptureResult captureResult) {
+            synchronized (captureResultListeners) {
+                Set<CaptureResultListener> removeSet = null;
+                for (CaptureResultListener listener : new HashSet<>(captureResultListeners)) {
+                    // Remove listener if the callback return true
+                    if (listener.onCaptureResult(captureResult)) {
+                        if (removeSet == null) {
+                            removeSet = new HashSet<>();
+                        }
+                        removeSet.add(listener);
+                    }
+                }
+                if (removeSet != null) {
+                    captureResultListeners.removeAll(removeSet);
+                }
+            }
+        }
+
+        /** Add capture result listener. */
+        private void addListener(CaptureResultListener listener) {
+            synchronized (captureResultListeners) {
+                captureResultListeners.add(listener);
+            }
+        }
+
+        /** An interface to check camera capture result. */
+        public interface CaptureResultChecker<T> {
+
+            /**
+             * The callback to check camera capture result.
+             *
+             * @param captureResult the camera capture result.
+             * @return the check result, return null to continue checking.
+             */
+            T check(@NonNull CameraCaptureResult captureResult);
+        }
+
+        /** An interface to listen to camera capture results. */
+        private interface CaptureResultListener {
+
+            /**
+             * Callback to handle camera capture results.
+             *
+             * @param captureResult camera capture result.
+             * @return true to finish listening, false to continue listening.
+             */
+            boolean onCaptureResult(@NonNull CameraCaptureResult captureResult);
+        }
+    }
+
+    private final class ImageCaptureRequest {
+        OnImageCapturedListener listener;
+        @Nullable
+        Handler handler;
+        @RotationValue
+        int rotationDegrees;
+
+        ImageCaptureRequest(
+                OnImageCapturedListener listener,
+                @Nullable Handler handler,
+                @RotationValue int rotationDegrees) {
+            this.listener = listener;
+            this.handler = handler;
+            this.rotationDegrees = rotationDegrees;
+        }
+
+        void dispatchImage(ImageProxy image) {
+            if (handler != null && Looper.myLooper() != handler.getLooper()) {
+                boolean posted =
+                        handler.post(
+                                () -> {
+                                    dispatchImage(image);
+                                });
+                // Unable to post to the supplied handler, close the image.
+                if (!posted) {
+                    Log.e(TAG, "Unable to post to the supplied handler.");
+                    image.close();
+                }
+                return;
+            }
+
+            listener.onCaptureSuccess(image, rotationDegrees);
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCaseConfiguration.java
new file mode 100644
index 0000000..4adf610
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCaseConfiguration.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageCaptureUseCase.CaptureMode;
+
+/** Configuration for an image capture use case. */
+public final class ImageCaptureUseCaseConfiguration
+        implements UseCaseConfiguration<ImageCaptureUseCase>,
+        ImageOutputConfiguration,
+        CameraDeviceConfiguration,
+        ThreadConfiguration {
+
+    // Option Declarations:
+    // ***********************************************************************************************
+    static final Option<ImageCaptureUseCase.CaptureMode> OPTION_IMAGE_CAPTURE_MODE =
+            Option.create(
+                    "camerax.core.imageCapture.captureMode", ImageCaptureUseCase.CaptureMode.class);
+    static final Option<FlashMode> OPTION_FLASH_MODE =
+            Option.create("camerax.core.imageCapture.flashMode", FlashMode.class);
+    private final OptionsBundle config;
+
+    /** Creates a new configuration instance. */
+    ImageCaptureUseCaseConfiguration(OptionsBundle config) {
+        this.config = config;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /**
+     * Returns the {@link ImageCaptureUseCase.CaptureMode}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    public ImageCaptureUseCase.CaptureMode getCaptureMode(
+            @Nullable ImageCaptureUseCase.CaptureMode valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_IMAGE_CAPTURE_MODE, valueIfMissing);
+    }
+
+    /**
+     * Returns the {@link ImageCaptureUseCase.CaptureMode}.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public ImageCaptureUseCase.CaptureMode getCaptureMode() {
+        return getConfiguration().retrieveOption(OPTION_IMAGE_CAPTURE_MODE);
+    }
+
+    /**
+     * Returns the {@link FlashMode}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    public FlashMode getFlashMode(@Nullable FlashMode valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_FLASH_MODE, valueIfMissing);
+    }
+
+    /**
+     * Returns the {@link FlashMode}.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public FlashMode getFlashMode() {
+        return getConfiguration().retrieveOption(OPTION_FLASH_MODE);
+    }
+
+    /** Builder for a {@link ImageCaptureUseCaseConfiguration}. */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<
+            ImageCaptureUseCase, ImageCaptureUseCaseConfiguration, Builder>,
+            ImageOutputConfiguration.Builder<ImageCaptureUseCaseConfiguration, Builder>,
+            CameraDeviceConfiguration.Builder<ImageCaptureUseCaseConfiguration, Builder>,
+            ThreadConfiguration.Builder<ImageCaptureUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle mutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            this.mutableConfig = mutableConfig;
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(ImageCaptureUseCase.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass(ImageCaptureUseCase.class);
+        }
+
+        /**
+         * Generates a Builder from another Configuration object
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        public static Builder fromConfig(ImageCaptureUseCaseConfiguration configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public ImageCaptureUseCaseConfiguration build() {
+            return new ImageCaptureUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+        }
+
+        /**
+         * Sets the image capture mode.
+         *
+         * <p>Valid capture modes are {@link CaptureMode#MIN_LATENCY}, which prioritizes latency
+         * over image quality, or {@link CaptureMode#MAX_QUALITY}, which prioritizes image quality
+         * over latency.
+         *
+         * @param captureMode The requested image capture mode.
+         * @return The current Builder.
+         */
+        public Builder setCaptureMode(ImageCaptureUseCase.CaptureMode captureMode) {
+            getMutableConfiguration().insertOption(OPTION_IMAGE_CAPTURE_MODE, captureMode);
+            return builder();
+        }
+
+        /**
+         * Sets the {@link FlashMode}.
+         *
+         * @param flashMode The requested flash mode.
+         * @return The current Builder.
+         */
+        public Builder setFlashMode(FlashMode flashMode) {
+            getMutableConfiguration().insertOption(OPTION_FLASH_MODE, flashMode);
+            return builder();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageFormatConstants.java b/camera/core/src/main/java/androidx/camera/core/ImageFormatConstants.java
new file mode 100644
index 0000000..d846ffb
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageFormatConstants.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * This class used to constant values corresponding to the internal defined image format value used
+ * in StreamConfigurationMap.java.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ImageFormatConstants {
+    // Internal format in StreamConfigurationMap.java that will be mapped to public ImageFormat.JPEG
+    public static final int INTERNAL_DEFINED_IMAGE_FORMAT_JPEG = 0x21;
+
+    // Internal format HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED (0x22) in StreamConfigurationMap.java
+    // that will be mapped to public ImageFormat.PRIVATE after android level 23.
+    public static final int INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE = 0x22;
+
+    private ImageFormatConstants() {
+    }
+
+    ;
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageOutputConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ImageOutputConfiguration.java
new file mode 100644
index 0000000..8db7eb3
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageOutputConfiguration.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Rational;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Configuration containing options for configuring the output image data of a pipeline. */
+public interface ImageOutputConfiguration extends Configuration.Reader {
+    /**
+     * Invalid integer rotation.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    int INVALID_ROTATION = -1;
+    /**
+     * Option: camerax.core.imageOutput.targetAspectRatio
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<Rational> OPTION_TARGET_ASPECT_RATIO =
+            Option.create("camerax.core.imageOutput.targetAspectRatio", Rational.class);
+    /**
+     * Option: camerax.core.imageOutput.targetRotation
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<Integer> OPTION_TARGET_ROTATION =
+            Option.create("camerax.core.imageOutput.targetRotation", int.class);
+    /**
+     * Option: camerax.core.imageOutput.targetResolution
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<Size> OPTION_TARGET_RESOLUTION =
+            Option.create("camerax.core.imageOutput.targetResolution", Size.class);
+    /**
+     * Option: camerax.core.imageOutput.maxResolution
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<Size> OPTION_MAX_RESOLUTION =
+            Option.create("camerax.core.imageOutput.maxResolution", Size.class);
+
+    /**
+     * Retrieves the aspect ratio of the target intending to use images from this configuration.
+     *
+     * <p>This is the ratio of the target's width to the image's height, where the numerator of the
+     * provided {@link Rational} corresponds to the width, and the denominator corresponds to the
+     * height.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    default Rational getTargetAspectRatio(@Nullable Rational valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the aspect ratio of the target intending to use images from this configuration.
+     *
+     * <p>This is the ratio of the target's width to the image's height, where the numerator of the
+     * provided {@link Rational} corresponds to the width, and the denominator corresponds to the
+     * height.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    default Rational getTargetAspectRatio() {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO);
+    }
+
+    /**
+     * Retrieves the rotation of the target intending to use images from this configuration.
+     *
+     * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
+     * {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}. Rotation values are relative to
+     * the device's "natural" rotation, {@link Surface#ROTATION_0}.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @RotationValue
+    default int getTargetRotation(int valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ROTATION, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the rotation of the target intending to use images from this configuration.
+     *
+     * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
+     * {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}. Rotation values are relative to
+     * the device's "natural" rotation, {@link Surface#ROTATION_0}.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    @RotationValue
+    default int getTargetRotation() {
+        return retrieveOption(OPTION_TARGET_ROTATION);
+    }
+
+    /**
+     * Retrieves the resolution of the target intending to use from this configuration.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default Size getTargetResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_RESOLUTION, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the resolution of the target intending to use from this configuration.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default Size getTargetResolution() {
+        return retrieveOption(OPTION_TARGET_RESOLUTION);
+    }
+
+    // Option Declarations:
+    // ***********************************************************************************************
+
+    /**
+     * Retrieves the max resolution limitation of the target intending to use from this
+     * configuration.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default Size getMaxResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_MAX_RESOLUTION, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the max resolution limitation of the target intending to use from this
+     * configuration.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default Size getMaxResolution() {
+        return retrieveOption(OPTION_MAX_RESOLUTION);
+    }
+
+    /**
+     * Builder for a {@link ImageOutputConfiguration}.
+     *
+     * @param <C> The top level configuration which will be generated by {@link #build()}.
+     * @param <T> The top level builder type for which this builder is composed with.
+     */
+    interface Builder<C extends Configuration, T extends Builder<C, T>>
+            extends Configuration.Builder<C, T> {
+
+        /**
+         * Sets the aspect ratio of the intended target for images from this configuration.
+         *
+         * <p>This is the ratio of the target's width to the image's height, where the numerator of
+         * the provided {@link Rational} corresponds to the width, and the denominator corresponds
+         * to the height.
+         *
+         * @param aspectRatio A {@link Rational} representing the ratio of the target's width and
+         *                    height.
+         * @return The current Builder.
+         */
+        default T setTargetAspectRatio(Rational aspectRatio) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return builder();
+        }
+
+        /**
+         * Sets the rotation of the intended target for images from this configuration.
+         *
+         * <p>This is one of four valid values: {@link Surface#ROTATION_0}, {@link
+         * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+         * Rotation values are relative to the "natural" rotation, {@link Surface#ROTATION_0}.
+         *
+         * @param rotation The rotation of the intended target.
+         * @return The current Builder.
+         */
+        default T setTargetRotation(@RotationValue int rotation) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return builder();
+        }
+
+        /**
+         * Sets the resolution of the intended target from this configuration.
+         *
+         * @param resolution The target resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        default T setTargetResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+            return builder();
+        }
+
+        /**
+         * Sets the max resolution limitation of the intended target from this configuration.
+         *
+         * @param resolution The max resolution limitation to choose from supported output sizes
+         *                   list.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        default T setMaxResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return builder();
+        }
+    }
+
+    /**
+     * Valid integer rotation values.
+     *
+     * @hide
+     */
+    @IntDef({Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface RotationValue {
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageProxy.java b/camera/core/src/main/java/androidx/camera/core/ImageProxy.java
new file mode 100644
index 0000000..72d0beb
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageProxy.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.Rect;
+
+import java.nio.ByteBuffer;
+
+/** An image proxy which has an analogous interface as {@link android.media.Image}. */
+public interface ImageProxy extends AutoCloseable {
+    /**
+     * Closes the image.
+     *
+     * <p>@see {@link android.media.Image#close()}.
+     */
+    void close();
+
+    /**
+     * Returns the crop rectangle.
+     *
+     * <p>@see {@link android.media.Image#getCropRect()}.
+     */
+    Rect getCropRect();
+
+    /**
+     * Sets the crop rectangle.
+     *
+     * <p>@see {@link android.media.Image#setCropRect(Rect)}.
+     */
+    void setCropRect(Rect rect);
+
+    /**
+     * Returns the image format.
+     *
+     * <p>@see {@link android.media.Image#getFormat()}.
+     */
+    int getFormat();
+
+    /**
+     * Returns the image height.
+     *
+     * <p>@see {@link android.media.Image#getHeight()}.
+     */
+    int getHeight();
+
+    /**
+     * Returns the image width.
+     *
+     * <p>@see {@link android.media.Image#getWidth()}.
+     */
+    int getWidth();
+
+    /**
+     * Returns the timestamp.
+     *
+     * <p>@see {@link android.media.Image#getTimestamp()}.
+     */
+    long getTimestamp();
+
+    /**
+     * Sets the timestamp.
+     *
+     * <p>@see {@link android.media.Image#setTimestamp(long)}.
+     */
+    void setTimestamp(long timestamp);
+
+    /**
+     * Returns the array of planes.
+     *
+     * <p>@see {@link android.media.Image#getPlanes()}.
+     */
+    PlaneProxy[] getPlanes();
+
+    /** A plane proxy which has an analogous interface as {@link android.media.Image.Plane}. */
+    interface PlaneProxy {
+        /**
+         * Returns the row stride.
+         *
+         * <p>@see {@link android.media.Image.Plane#getRowStride()}.
+         */
+        int getRowStride();
+
+        /**
+         * Returns the pixel stride.
+         *
+         * <p>@see {@link android.media.Image.Plane#getPixelStride()}.
+         */
+        int getPixelStride();
+
+        /**
+         * Returns the pixels buffer.
+         *
+         * <p>@see {@link android.media.Image.Plane#getBuffer()}.
+         */
+        ByteBuffer getBuffer();
+    }
+
+    // TODO(b/123902197): HardwareBuffer access is provided on higher API levels. Wrap
+    // getHardwareBuffer() once we figure out how to provide compatibility with lower API levels.
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageProxyDownsampler.java b/camera/core/src/main/java/androidx/camera/core/ImageProxyDownsampler.java
new file mode 100644
index 0000000..209b9cf
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageProxyDownsampler.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.ImageFormat;
+import android.util.Size;
+
+import java.nio.ByteBuffer;
+
+/** Utility functions for downsampling an {@link ImageProxy}. */
+final class ImageProxyDownsampler {
+
+    private ImageProxyDownsampler() {
+    }
+
+    /**
+     * Downsamples an {@link ImageProxy}.
+     *
+     * @param image              to downsample
+     * @param downsampledWidth   width of the downsampled image
+     * @param downsampledHeight  height of the dowsampled image
+     * @param downsamplingMethod the downsampling method
+     * @return the downsampled image
+     */
+    static ForwardingImageProxy downsample(
+            ImageProxy image,
+            int downsampledWidth,
+            int downsampledHeight,
+            DownsamplingMethod downsamplingMethod) {
+        if (image.getFormat() != ImageFormat.YUV_420_888) {
+            throw new UnsupportedOperationException(
+                    "Only YUV_420_888 format is currently supported.");
+        }
+        if (image.getWidth() < downsampledWidth || image.getHeight() < downsampledHeight) {
+            throw new IllegalArgumentException(
+                    "Downsampled dimension "
+                            + new Size(downsampledWidth, downsampledHeight)
+                            + " is not <= original dimension "
+                            + new Size(image.getWidth(), image.getHeight())
+                            + ".");
+        }
+
+        if (image.getWidth() == downsampledWidth && image.getHeight() == downsampledHeight) {
+            return new ForwardingImageProxyImpl(
+                    image, image.getPlanes(), downsampledWidth, downsampledHeight);
+        }
+
+        int[] inputWidths = {image.getWidth(), image.getWidth() / 2, image.getWidth() / 2};
+        int[] inputHeights = {image.getHeight(), image.getHeight() / 2, image.getHeight() / 2};
+        int[] outputWidths = {downsampledWidth, downsampledWidth / 2, downsampledWidth / 2};
+        int[] outputHeights = {downsampledHeight, downsampledHeight / 2, downsampledHeight / 2};
+
+        ImageProxy.PlaneProxy[] outputPlanes = new ImageProxy.PlaneProxy[3];
+        for (int i = 0; i < 3; ++i) {
+            ImageProxy.PlaneProxy inputPlane = image.getPlanes()[i];
+            ByteBuffer inputBuffer = inputPlane.getBuffer();
+            byte[] output = new byte[outputWidths[i] * outputHeights[i]];
+            switch (downsamplingMethod) {
+                case NEAREST_NEIGHBOR:
+                    resizeNearestNeighbor(
+                            inputBuffer,
+                            inputWidths[i],
+                            inputPlane.getPixelStride(),
+                            inputPlane.getRowStride(),
+                            inputHeights[i],
+                            output,
+                            outputWidths[i],
+                            outputHeights[i]);
+                    break;
+                case AVERAGING:
+                    resizeAveraging(
+                            inputBuffer,
+                            inputWidths[i],
+                            inputPlane.getPixelStride(),
+                            inputPlane.getRowStride(),
+                            inputHeights[i],
+                            output,
+                            outputWidths[i],
+                            outputHeights[i]);
+                    break;
+            }
+            outputPlanes[i] = createPlaneProxy(outputWidths[i], 1, output);
+        }
+        return new ForwardingImageProxyImpl(
+                image, outputPlanes, downsampledWidth, downsampledHeight);
+    }
+
+    private static void resizeNearestNeighbor(
+            ByteBuffer input,
+            int inputWidth,
+            int inputPixelStride,
+            int inputRowStride,
+            int inputHeight,
+            byte[] output,
+            int outputWidth,
+            int outputHeight) {
+        float scaleX = (float) inputWidth / outputWidth;
+        float scaleY = (float) inputHeight / outputHeight;
+        int outputRowStride = outputWidth;
+
+        byte[] row = new byte[inputRowStride];
+        int[] sourceIndices = new int[outputWidth];
+        for (int ix = 0; ix < outputWidth; ++ix) {
+            float sourceX = ix * scaleX;
+            int floorSourceX = (int) sourceX;
+            sourceIndices[ix] = floorSourceX * inputPixelStride;
+        }
+
+        synchronized (input) {
+            input.rewind();
+            for (int iy = 0; iy < outputHeight; ++iy) {
+                float sourceY = iy * scaleY;
+                int floorSourceY = (int) sourceY;
+                int rowOffsetSource = Math.min(floorSourceY, inputHeight - 1) * inputRowStride;
+                int rowOffsetTarget = iy * outputRowStride;
+
+                input.position(rowOffsetSource);
+                input.get(row, 0, Math.min(inputRowStride, input.remaining()));
+
+                for (int ix = 0; ix < outputWidth; ++ix) {
+                    output[rowOffsetTarget + ix] = row[sourceIndices[ix]];
+                }
+            }
+        }
+    }
+
+    private static void resizeAveraging(
+            ByteBuffer input,
+            int inputWidth,
+            int inputPixelStride,
+            int inputRowStride,
+            int inputHeight,
+            byte[] output,
+            int outputWidth,
+            int outputHeight) {
+        float scaleX = (float) inputWidth / outputWidth;
+        float scaleY = (float) inputHeight / outputHeight;
+        int outputRowStride = outputWidth;
+
+        byte[] row0 = new byte[inputRowStride];
+        byte[] row1 = new byte[inputRowStride];
+        int[] sourceIndices = new int[outputWidth];
+        for (int ix = 0; ix < outputWidth; ++ix) {
+            float sourceX = ix * scaleX;
+            int floorSourceX = (int) sourceX;
+            sourceIndices[ix] = floorSourceX * inputPixelStride;
+        }
+
+        synchronized (input) {
+            input.rewind();
+            for (int iy = 0; iy < outputHeight; ++iy) {
+                float sourceY = iy * scaleY;
+                int floorSourceY = (int) sourceY;
+                int rowOffsetSource0 = Math.min(floorSourceY, inputHeight - 1) * inputRowStride;
+                int rowOffsetSource1 = Math.min(floorSourceY + 1, inputHeight - 1) * inputRowStride;
+                int rowOffsetTarget = iy * outputRowStride;
+
+                input.position(rowOffsetSource0);
+                input.get(row0, 0, Math.min(inputRowStride, input.remaining()));
+                input.position(rowOffsetSource1);
+                input.get(row1, 0, Math.min(inputRowStride, input.remaining()));
+
+                for (int ix = 0; ix < outputWidth; ++ix) {
+                    int sampleA = row0[sourceIndices[ix]] & 0xFF;
+                    int sampleB = row0[sourceIndices[ix] + inputPixelStride] & 0xFF;
+                    int sampleC = row1[sourceIndices[ix]] & 0xFF;
+                    int sampleD = row1[sourceIndices[ix] + inputPixelStride] & 0xFF;
+                    int mixed = (sampleA + sampleB + sampleC + sampleD) / 4;
+                    output[rowOffsetTarget + ix] = (byte) (mixed & 0xFF);
+                }
+            }
+        }
+    }
+
+    private static ImageProxy.PlaneProxy createPlaneProxy(
+            int rowStride, int pixelStride, byte[] data) {
+        return new ImageProxy.PlaneProxy() {
+            final ByteBuffer buffer = ByteBuffer.wrap(data);
+
+            @Override
+            public int getRowStride() {
+                return rowStride;
+            }
+
+            @Override
+            public int getPixelStride() {
+                return pixelStride;
+            }
+
+            @Override
+            public ByteBuffer getBuffer() {
+                return buffer;
+            }
+        };
+    }
+
+    static enum DownsamplingMethod {
+        // Uses nearest sample.
+        NEAREST_NEIGHBOR,
+        // Uses average of 4 nearest samples.
+        AVERAGING,
+    }
+
+    private static final class ForwardingImageProxyImpl extends ForwardingImageProxy {
+        private final PlaneProxy[] downsampledPlanes;
+        private final int downsampledWidth;
+        private final int downsampledHeight;
+
+        ForwardingImageProxyImpl(
+                ImageProxy originalImage,
+                PlaneProxy[] downsampledPlanes,
+                int downsampledWidth,
+                int downsampledHeight) {
+            super(originalImage);
+            this.downsampledPlanes = downsampledPlanes;
+            this.downsampledWidth = downsampledWidth;
+            this.downsampledHeight = downsampledHeight;
+        }
+
+        @Override
+        public synchronized int getWidth() {
+            return downsampledWidth;
+        }
+
+        @Override
+        public synchronized int getHeight() {
+            return downsampledHeight;
+        }
+
+        @Override
+        public synchronized PlaneProxy[] getPlanes() {
+            return downsampledPlanes;
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageReaderFormatRecommender.java b/camera/core/src/main/java/androidx/camera/core/ImageReaderFormatRecommender.java
new file mode 100644
index 0000000..e39d825
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageReaderFormatRecommender.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.ImageFormat;
+import android.media.ImageReader;
+
+import com.google.auto.value.AutoValue;
+
+/** Recommends formats for a combination of {@link ImageReader} instances. */
+final class ImageReaderFormatRecommender {
+
+    private ImageReaderFormatRecommender() {
+    }
+
+    /** Chooses a combination which is compatible for the current device. */
+    static FormatCombo chooseCombo() {
+        if (ImageReaderProxys.inSharedReaderWhitelist(DeviceProperties.create())) {
+            return FormatCombo.create(ImageFormat.YUV_420_888, ImageFormat.YUV_420_888);
+        } else {
+            return FormatCombo.create(ImageFormat.JPEG, ImageFormat.YUV_420_888);
+        }
+    }
+
+    /** Container for a combination of {@link ImageReader} formats. */
+    @AutoValue
+    abstract static class FormatCombo {
+        static FormatCombo create(int imageCaptureFormat, int imageAnalysisFormat) {
+            return new AutoValue_ImageReaderFormatRecommender_FormatCombo(
+                    imageCaptureFormat, imageAnalysisFormat);
+        }
+
+        // Returns the format for image capture.
+        abstract int imageCaptureFormat();
+
+        // Returns the format for image analysis.
+        abstract int imageAnalysisFormat();
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageReaderProxy.java b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxy.java
new file mode 100644
index 0000000..b98161e
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxy.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.ImageReader;
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * An image reader proxy which has an analogous interface as {@link ImageReader}.
+ *
+ * <p>Whereas an {@link ImageReader} provides {@link android.media.Image} instances, an {@link
+ * ImageReaderProxy} provides {@link ImageProxy} instances.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface ImageReaderProxy {
+    /**
+     * Acquires the latest image in the queue.
+     *
+     * <p>@see {@link ImageReader#acquireLatestImage()}.
+     */
+    @Nullable
+    ImageProxy acquireLatestImage();
+
+    /**
+     * Acquires the next image in the queue.
+     *
+     * <p>@see {@link ImageReader#acquireNextImage()}.
+     */
+    @Nullable
+    ImageProxy acquireNextImage();
+
+    /**
+     * Closes the reader.
+     *
+     * <p>@see {@link ImageReader#close()}.
+     */
+    void close();
+
+    /**
+     * Returns the image height.
+     *
+     * <p>@see {@link ImageReader#getHeight()}.
+     */
+    int getHeight();
+
+    /**
+     * Returns the image width.
+     *
+     * <p>@see {@link ImageReader#getWidth()}.
+     */
+    int getWidth();
+
+    /**
+     * Returns the image format.
+     *
+     * <p>@see {@link ImageReader#getImageFormat()}.
+     */
+    int getImageFormat();
+
+    /**
+     * Returns the max number of images in the queue.
+     *
+     * <p>@see {@link ImageReader#getMaxImages()}.
+     */
+    int getMaxImages();
+
+    /**
+     * Returns the underlying surface.
+     *
+     * <p>@see {@link ImageReader#getSurface()}.
+     */
+    Surface getSurface();
+
+    /**
+     * Sets the on-image-available listener.
+     *
+     * <p>@see {@link ImageReader#setOnImageAvailableListener}.
+     */
+    void setOnImageAvailableListener(
+            @Nullable ImageReaderProxy.OnImageAvailableListener listener,
+            @Nullable Handler handler);
+
+    /**
+     * A listener for newly available images.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    interface OnImageAvailableListener {
+        /**
+         * Callback for a newly available image.
+         *
+         * <p>@see {@link ImageReader.OnImageAvailableListener#onImageAvailable(ImageReader)}.
+         */
+        void onImageAvailable(ImageReaderProxy imageReader);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageReaderProxys.java b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxys.java
new file mode 100644
index 0000000..22d15bf
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageReaderProxys.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.ImageFormat;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.util.Log;
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Different implementations of {@link ImageReaderProxy}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ImageReaderProxys {
+    private static final String TAG = ImageReaderProxys.class.getSimpleName();
+    private static final int SHARED_IMAGE_FORMAT = ImageFormat.YUV_420_888;
+    private static final int SHARED_MAX_IMAGES = 8;
+    private static final List<QueuedImageReaderProxy> sharedImageReaderProxys = new ArrayList<>();
+    private static Set<DeviceProperties> sharedReaderWhitelist;
+    private static ImageReader sharedImageReader;
+
+    private ImageReaderProxys() {
+    }
+
+    /**
+     * Creates an {@link ImageReaderProxy} which chooses a device-compatible implementation.
+     *
+     * @param cameraId  of the target camera
+     * @param width     of the reader
+     * @param height    of the reader
+     * @param format    of the reader
+     * @param maxImages of the reader
+     * @param handler   for on-image-available callbacks
+     * @return new {@link ImageReaderProxy} instance
+     */
+    static ImageReaderProxy createCompatibleReader(
+            String cameraId, int width, int height, int format, int maxImages, Handler handler) {
+        if (inSharedReaderWhitelist(DeviceProperties.create())) {
+            return createSharedReader(cameraId, width, height, format, maxImages, handler);
+        } else {
+            return createIsolatedReader(width, height, format, maxImages, handler);
+        }
+    }
+
+    /**
+     * Creates an {@link ImageReaderProxy} which uses its own isolated {@link ImageReader}.
+     *
+     * @param width     of the reader
+     * @param height    of the reader
+     * @param format    of the reader
+     * @param maxImages of the reader
+     * @param handler   for on-image-available callbacks
+     * @return new {@link ImageReaderProxy} instance
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static ImageReaderProxy createIsolatedReader(
+            int width, int height, int format, int maxImages, Handler handler) {
+        ImageReader imageReader = ImageReader.newInstance(width, height, format, maxImages);
+        return new AndroidImageReaderProxy(imageReader);
+    }
+
+    /**
+     * Creates an {@link ImageReaderProxy} which shares an underlying {@link ImageReader}.
+     *
+     * @param cameraId  of the target camera
+     * @param width     of the reader
+     * @param height    of the reader
+     * @param format    of the reader
+     * @param maxImages of the reader
+     * @param handler   for on-image-available callbacks
+     * @return new {@link ImageReaderProxy} instance
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static ImageReaderProxy createSharedReader(
+            String cameraId, int width, int height, int format, int maxImages, Handler handler) {
+        if (sharedImageReader == null) {
+            Size resolution =
+                    CameraX.getSurfaceManager().getMaxOutputSize(cameraId, SHARED_IMAGE_FORMAT);
+            Log.d(TAG, "Resolution of base ImageReader: " + resolution);
+            sharedImageReader =
+                    ImageReader.newInstance(
+                            resolution.getWidth(),
+                            resolution.getHeight(),
+                            SHARED_IMAGE_FORMAT,
+                            SHARED_MAX_IMAGES);
+        }
+        Log.d(TAG, "Resolution of forked ImageReader: " + new Size(width, height));
+        QueuedImageReaderProxy imageReaderProxy =
+                new QueuedImageReaderProxy(
+                        width, height, format, maxImages, sharedImageReader.getSurface());
+        sharedImageReaderProxys.add(imageReaderProxy);
+        sharedImageReader.setOnImageAvailableListener(
+                new ForwardingImageReaderListener(sharedImageReaderProxys), handler);
+        imageReaderProxy.addOnReaderCloseListener(
+                reader -> {
+                    sharedImageReaderProxys.remove(reader);
+                    if (sharedImageReaderProxys.isEmpty()) {
+                        clearSharedReaders();
+                    }
+                });
+        return imageReaderProxy;
+    }
+
+    /**
+     * Returns true if the device is in the shared reader whitelist.
+     *
+     * <p>Devices in the whitelist are known to work with shared readers. Devices outside the
+     * whitelist may also work with shared readers, but they have not been tested yet.
+     *
+     * @param device to check
+     * @return true if device is in whitelist
+     */
+    static boolean inSharedReaderWhitelist(DeviceProperties device) {
+        if (sharedReaderWhitelist == null) {
+            sharedReaderWhitelist = new HashSet<>();
+            for (int sdkVersion = 21; sdkVersion <= 27; ++sdkVersion) {
+                sharedReaderWhitelist.add(DeviceProperties.create("Google", "Pixel", sdkVersion));
+                sharedReaderWhitelist.add(
+                        DeviceProperties.create("Google", "Pixel XL", sdkVersion));
+                sharedReaderWhitelist.add(
+                        DeviceProperties.create("HMD Global", "Nokia 8.1", sdkVersion));
+            }
+        }
+        return sharedReaderWhitelist.contains(device);
+    }
+
+    private static void clearSharedReaders() {
+        sharedImageReaderProxys.clear();
+        sharedImageReader.setOnImageAvailableListener(null, null);
+        sharedImageReader.close();
+        sharedImageReader = null;
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/core/src/main/java/androidx/camera/core/ImageSaver.java
new file mode 100644
index 0000000..941c979
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.ImageFormat;
+import android.location.Location;
+import android.os.Handler;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageUtil.EncodeFailedException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+final class ImageSaver implements Runnable {
+    private static final String TAG = "ImageSaver";
+    private final @Nullable
+    Location location;
+    // The image that was captured
+    private final ImageProxy image;
+    // The orientation of the image
+    private final int orientation;
+    // If true, the picture taken is reversed horizontally and needs to be flipped.
+    // Typical with front facing cameras.
+    private final boolean isReversedHorizontal;
+    // If true, the picture taken is reversed vertically and needs to be flipped.
+    private final boolean isReversedVertical;
+    // The file to save the image to
+    private final File file;
+    // The callback to call on completion
+    private final OnImageSavedListener listener;
+    // The handler to call back on
+    private final Handler handler;
+    // The width/height ratio output should be cropped to
+    @Nullable
+    private final Rational cropAspectRatio;
+
+    ImageSaver(
+            ImageProxy image,
+            File file,
+            int orientation,
+            boolean reversedHorizontal,
+            boolean reversedVertical,
+            @Nullable Location location,
+            @Nullable Rational cropAspectRatio,
+            OnImageSavedListener listener,
+            Handler handler) {
+        this.image = image;
+        this.file = file;
+        this.orientation = orientation;
+        isReversedHorizontal = reversedHorizontal;
+        isReversedVertical = reversedVertical;
+        this.listener = listener;
+        this.handler = handler;
+        this.location = location;
+
+        // Fix cropRatio by orientation.
+        if (orientation == 90 || orientation == 270) {
+            this.cropAspectRatio = inverseRational(cropAspectRatio);
+        } else {
+            this.cropAspectRatio = cropAspectRatio;
+        }
+    }
+
+    @Override
+    public void run() {
+        // Finally, we save the file to disk
+        SaveError saveError = null;
+        String errorMessage = null;
+        Exception exception = null;
+        try (ImageProxy imageToClose = image;
+             FileOutputStream output = new FileOutputStream(file)) {
+            byte[] bytes = getBytes();
+            output.write(bytes);
+
+            Exif exif = Exif.createFromFile(file);
+            exif.attachTimestamp();
+            exif.rotate(orientation);
+            if (isReversedHorizontal) {
+                exif.flipHorizontally();
+            }
+            if (isReversedVertical) {
+                exif.flipVertically();
+            }
+            if (location != null) {
+                exif.attachLocation(location);
+            }
+            exif.save();
+        } catch (IOException e) {
+            saveError = SaveError.FILE_IO_FAILED;
+            errorMessage = "Failed to write or close the file";
+            exception = e;
+        } catch (EncodeFailedException e) {
+            saveError = SaveError.ENCODE_FAILED;
+            errorMessage = "Failed to encode image";
+            exception = e;
+        }
+
+        if (saveError != null) {
+            postError(saveError, errorMessage, exception);
+        } else {
+            postSuccess();
+        }
+    }
+
+    private void postSuccess() {
+        handler.post(() -> listener.onImageSaved(file));
+    }
+
+    private void postError(SaveError saveError, String message, @Nullable Throwable cause) {
+        handler.post(() -> listener.onError(saveError, message, cause));
+    }
+
+    private byte[] getBytes() throws EncodeFailedException {
+        byte[] data = null;
+        Size sourceSize = new Size(image.getWidth(), image.getHeight());
+
+        if (ImageUtil.isAspectRatioValid(sourceSize, cropAspectRatio)) {
+            if (image.getFormat() == ImageFormat.JPEG) {
+                data =
+                        ImageUtil.cropByteArray(
+                                ImageUtil.jpegImageToJpegByteArray(image),
+                                ImageUtil.computeCropRectFromAspectRatio(
+                                        sourceSize, cropAspectRatio));
+            } else if (image.getFormat() == ImageFormat.YUV_420_888) {
+                data =
+                        ImageUtil.yuvImageToJpegByteArray(
+                                image,
+                                ImageUtil.computeCropRectFromAspectRatio(
+                                        sourceSize, cropAspectRatio));
+            } else {
+                data = ImageUtil.imageToJpegByteArray(image);
+                Log.w(TAG, "Unrecognized image format: " + image.getFormat());
+            }
+        } else {
+            data = ImageUtil.imageToJpegByteArray(image);
+        }
+
+        return data;
+    }
+
+    private Rational inverseRational(Rational rational) {
+        if (rational == null) {
+            return rational;
+        }
+        return new Rational(
+                /*numerator=*/ rational.getDenominator(), /*denominator=*/ rational.getNumerator());
+    }
+
+    /** Type of error that occurred during save */
+    public enum SaveError {
+        /** Failed to write to or close the file */
+        FILE_IO_FAILED,
+        /** Failure when attempting to encode image */
+        ENCODE_FAILED
+    }
+
+    public interface OnImageSavedListener {
+
+        void onImageSaved(File file);
+
+        void onError(SaveError saveError, String message, @Nullable Throwable cause);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageUtil.java b/camera/core/src/main/java/androidx/camera/core/ImageUtil.java
new file mode 100644
index 0000000..d18bbf6
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageUtil.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility class for image related operations.
+ *
+ * @hide
+ */
+final class ImageUtil {
+    private static final String TAG = "ImageUtil";
+
+    private ImageUtil() {
+    }
+
+    /** {@link android.media.Image} to JPEG byte array. */
+    public static byte[] imageToJpegByteArray(ImageProxy image) throws EncodeFailedException {
+        byte[] data = null;
+        if (image.getFormat() == ImageFormat.JPEG) {
+            data = jpegImageToJpegByteArray(image);
+        } else if (image.getFormat() == ImageFormat.YUV_420_888) {
+            data = yuvImageToJpegByteArray(image, null);
+        } else {
+            Log.w(TAG, "Unrecognized image format: " + image.getFormat());
+        }
+        return data;
+    }
+
+    public static byte[] jpegImageToJpegByteArray(ImageProxy image) {
+        ImageProxy.PlaneProxy[] planes = image.getPlanes();
+        ByteBuffer buffer = planes[0].getBuffer();
+        byte[] data = new byte[buffer.capacity()];
+        buffer.get(data);
+        return data;
+    }
+
+    public static byte[] yuvImageToJpegByteArray(ImageProxy image, @Nullable Rect cropRect)
+            throws EncodeFailedException {
+        return ImageUtil.nv21ToJpeg(
+                ImageUtil.yuv_420_888toNv21(image), image.getWidth(), image.getHeight(), cropRect);
+    }
+
+    /** Crops byte array with given {@link android.graphics.Rect}. */
+    public static byte[] cropByteArray(byte[] data, Rect cropRect) throws EncodeFailedException {
+        if (cropRect == null) {
+            return data;
+        }
+
+        Bitmap imageBitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+        if (imageBitmap == null) {
+            Log.w(TAG, "Source image for cropping can't be decoded.");
+            return data;
+        }
+
+        Bitmap cropBitmap = cropBitmap(imageBitmap, cropRect);
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        boolean success = cropBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
+        if (!success) {
+            throw new EncodeFailedException("cropImage failed to encode jpeg.");
+        }
+
+        imageBitmap.recycle();
+        cropBitmap.recycle();
+
+        return out.toByteArray();
+    }
+
+    /** Crops bitmap with given {@link android.graphics.Rect}. */
+    public static Bitmap cropBitmap(Bitmap bitmap, Rect cropRect) {
+        if (cropRect.width() > bitmap.getWidth() || cropRect.height() > bitmap.getHeight()) {
+            Log.w(TAG, "Crop rect size exceeds the source image.");
+            return bitmap;
+        }
+
+        return Bitmap.createBitmap(
+                bitmap, cropRect.left, cropRect.top, cropRect.width(), cropRect.height());
+    }
+
+    /** Flips bitmap. */
+    public static Bitmap flipBitmap(Bitmap bitmap, boolean flipHorizontal, boolean flipVertical) {
+        if (!flipHorizontal && !flipVertical) {
+            return bitmap;
+        }
+
+        Matrix matrix = new Matrix();
+        if (flipHorizontal) {
+            if (flipVertical) {
+                matrix.preScale(-1.0f, -1.0f);
+            } else {
+                matrix.preScale(-1.0f, 1.0f);
+            }
+        } else if (flipVertical) {
+            matrix.preScale(1.0f, -1.0f);
+        }
+
+        return Bitmap.createBitmap(
+                bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+    }
+
+    /** Rotates bitmap with specified degree. */
+    public static Bitmap rotateBitmap(Bitmap bitmap, int degree) {
+        if (degree == 0) {
+            return bitmap;
+        }
+
+        Matrix matrix = new Matrix();
+        matrix.preRotate(degree);
+
+        return Bitmap.createBitmap(
+                bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+    }
+
+    /** True if the given aspect ratio is meaningful. */
+    public static boolean isAspectRatioValid(Rational aspectRatio) {
+        return aspectRatio != null && aspectRatio.floatValue() > 0 && !aspectRatio.isNaN();
+    }
+
+    /** True if the given aspect ratio is meaningful and has effect on the given size. */
+    public static boolean isAspectRatioValid(Size sourceSize, Rational aspectRatio) {
+        return aspectRatio != null
+                && aspectRatio.floatValue() > 0
+                && isCropAspectRatioHasEffect(sourceSize, aspectRatio)
+                && !aspectRatio.isNaN();
+    }
+
+    /**
+     * Calculates crop rect with the specified aspect ratio on the given size. Assuming the rect is
+     * at the center of the source.
+     */
+    public static Rect computeCropRectFromAspectRatio(Size sourceSize, Rational aspectRatio) {
+        if (!isAspectRatioValid(aspectRatio)) {
+            Log.w(TAG, "Invalid view ratio.");
+            return null;
+        }
+
+        int sourceWidth = sourceSize.getWidth();
+        int sourceHeight = sourceSize.getHeight();
+        float srcRatio = sourceWidth / (float) sourceHeight;
+        int cropLeft = 0;
+        int cropTop = 0;
+        int outputWidth = sourceWidth;
+        int outputHeight = sourceHeight;
+        int numerator = aspectRatio.getNumerator();
+        int denominator = aspectRatio.getDenominator();
+
+        if (aspectRatio.floatValue() > srcRatio) {
+            outputHeight = Math.round((sourceWidth / (float) numerator) * denominator);
+            cropTop = (sourceHeight - outputHeight) / 2;
+        } else {
+            outputWidth = Math.round((sourceHeight / (float) denominator) * numerator);
+            cropLeft = (sourceWidth - outputWidth) / 2;
+        }
+
+        return new Rect(cropLeft, cropTop, cropLeft + outputWidth, cropTop + outputHeight);
+    }
+
+    private static byte[] nv21ToJpeg(byte[] nv21, int width, int height, @Nullable Rect cropRect)
+            throws EncodeFailedException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
+        boolean success =
+                yuv.compressToJpeg(
+                        cropRect == null ? new Rect(0, 0, width, height) : cropRect, 100, out);
+        if (!success) {
+            throw new EncodeFailedException("YuvImage failed to encode jpeg.");
+        }
+        return out.toByteArray();
+    }
+
+    private static byte[] yuv_420_888toNv21(ImageProxy image) {
+        ImageProxy.PlaneProxy yPlane = image.getPlanes()[0];
+        ImageProxy.PlaneProxy uPlane = image.getPlanes()[1];
+        ImageProxy.PlaneProxy vPlane = image.getPlanes()[2];
+
+        ByteBuffer yBuffer = yPlane.getBuffer();
+        ByteBuffer uBuffer = uPlane.getBuffer();
+        ByteBuffer vBuffer = vPlane.getBuffer();
+        yBuffer.rewind();
+        uBuffer.rewind();
+        vBuffer.rewind();
+
+        int ySize = yBuffer.remaining();
+
+        int position = 0;
+        // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
+        byte[] nv21 = new byte[ySize + (image.getWidth() * image.getHeight() / 2)];
+
+        // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+        for (int row = 0; row < image.getHeight(); row++) {
+            yBuffer.get(nv21, position, image.getWidth());
+            position += image.getWidth();
+            yBuffer.position(
+                    Math.min(ySize, yBuffer.position() - image.getWidth() + yPlane.getRowStride()));
+        }
+
+        int chromaHeight = image.getHeight() / 2;
+        int chromaWidth = image.getWidth() / 2;
+        int vRowStride = vPlane.getRowStride();
+        int uRowStride = uPlane.getRowStride();
+        int vPixelStride = vPlane.getPixelStride();
+        int uPixelStride = uPlane.getPixelStride();
+
+        // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+        // perform faster bulk gets from the byte buffers.
+        byte[] vLineBuffer = new byte[vRowStride];
+        byte[] uLineBuffer = new byte[uRowStride];
+        for (int row = 0; row < chromaHeight; row++) {
+            vBuffer.get(vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining()));
+            uBuffer.get(uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining()));
+            int vLineBufferPosition = 0;
+            int uLineBufferPosition = 0;
+            for (int col = 0; col < chromaWidth; col++) {
+                nv21[position++] = vLineBuffer[vLineBufferPosition];
+                nv21[position++] = uLineBuffer[uLineBufferPosition];
+                vLineBufferPosition += vPixelStride;
+                uLineBufferPosition += uPixelStride;
+            }
+        }
+
+        return nv21;
+    }
+
+    private static boolean isCropAspectRatioHasEffect(Size sourceSize, Rational aspectRatio) {
+        int sourceWidth = sourceSize.getWidth();
+        int sourceHeight = sourceSize.getHeight();
+        int numerator = aspectRatio.getNumerator();
+        int denominator = aspectRatio.getDenominator();
+
+        return sourceHeight != Math.round((sourceWidth / (float) numerator) * denominator)
+                || sourceWidth != Math.round((sourceHeight / (float) denominator) * numerator);
+    }
+
+    /** Exception for error during encoding image. */
+    public static final class EncodeFailedException extends Exception {
+        EncodeFailedException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImmediateSurface.java b/camera/core/src/main/java/androidx/camera/core/ImmediateSurface.java
new file mode 100644
index 0000000..5813a5f
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImmediateSurface.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * A {@link DeferrableSurface} which always returns immediately.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class ImmediateSurface implements DeferrableSurface {
+    private final Surface surface;
+
+    public ImmediateSurface(Surface surface) {
+        this.surface = surface;
+    }
+
+    @Override
+    public ListenableFuture<Surface> getSurface() {
+        return Futures.immediateFuture(surface);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/IoExecutor.java b/camera/core/src/main/java/androidx/camera/core/IoExecutor.java
new file mode 100644
index 0000000..75abc74
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/IoExecutor.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A singleton executor which should be used for I/O tasks.
+ *
+ * <p>TODO(b/115779693): Make this executor configurable
+ */
+final class IoExecutor implements Executor {
+    private static volatile Executor instance;
+
+    private final ExecutorService ioService =
+            Executors.newFixedThreadPool(
+                    2,
+                    new ThreadFactory() {
+                        private static final String THREAD_NAME_STEM =
+                                CameraXThreads.TAG + "camerax_io_%d";
+
+                        private final AtomicInteger threadId = new AtomicInteger(0);
+
+                        @Override
+                        public Thread newThread(Runnable r) {
+                            Thread t = new Thread(r);
+                            t.setName(
+                                    String.format(
+                                            Locale.US,
+                                            THREAD_NAME_STEM,
+                                            threadId.getAndIncrement()));
+                            return t;
+                        }
+                    });
+
+    static Executor getInstance() {
+        if (instance != null) {
+            return instance;
+        }
+        synchronized (IoExecutor.class) {
+            if (instance == null) {
+                instance = new IoExecutor();
+            }
+        }
+
+        return instance;
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        ioService.execute(command);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/MutableConfiguration.java b/camera/core/src/main/java/androidx/camera/core/MutableConfiguration.java
new file mode 100644
index 0000000..68eff92
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MutableConfiguration.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * MutableConfiguration is a {@link Configuration} that can be modified.
+ *
+ * <p>MutableConfiguration is the interface used to create immutable Configuration objects.
+ */
+public interface MutableConfiguration extends Configuration {
+
+    /**
+     * Inserts a Option/Value pair into the configuration.
+     *
+     * <p>If the option already exists in this configuration, it will be replaced.
+     *
+     * @param opt      The option to be added or modified
+     * @param value    The value to insert for this option.
+     * @param <ValueT> The type of the value being inserted.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    <ValueT> void insertOption(Option<ValueT> opt, ValueT value);
+
+    /**
+     * Removes an option from the configuration if it exists.
+     *
+     * @param opt      The option to remove from the configuration.
+     * @param <ValueT> The type of the value being removed.
+     * @return The value that previously existed for <code>opt</code>, or <code>null</code> if the
+     * option did not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    <ValueT> ValueT removeOption(Option<ValueT> opt);
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/MutableOptionsBundle.java b/camera/core/src/main/java/androidx/camera/core/MutableOptionsBundle.java
new file mode 100644
index 0000000..ca7b6c6
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MutableOptionsBundle.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Comparator;
+import java.util.TreeMap;
+
+/**
+ * A MutableOptionsBundle is an {@link OptionsBundle} which allows for insertion/removal.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class MutableOptionsBundle extends OptionsBundle implements MutableConfiguration {
+
+    private static final Comparator<Option<?>> ID_COMPARE =
+            (o1, o2) -> o1.getId().compareTo(o2.getId());
+
+    private MutableOptionsBundle(TreeMap<Option<?>, Object> persistentOptions) {
+        super(persistentOptions);
+    }
+
+    /**
+     * Creates an empty MutableOptionsBundle.
+     *
+     * @return an empty MutableOptionsBundle containing no options.
+     */
+    public static MutableOptionsBundle create() {
+        return new MutableOptionsBundle(new TreeMap<>(ID_COMPARE));
+    }
+
+    /**
+     * Creates a MutableOptionsBundle from an existing immutable Configuration.
+     *
+     * @param otherConfig configuration options to insert.
+     * @return a MutableOptionsBundle prepopulated with configuration options.
+     */
+    public static MutableOptionsBundle from(Configuration otherConfig) {
+        TreeMap<Option<?>, Object> persistentOptions = new TreeMap<>(ID_COMPARE);
+        for (Option<?> opt : otherConfig.listOptions()) {
+            persistentOptions.put(opt, otherConfig.retrieveOption(opt));
+        }
+
+        return new MutableOptionsBundle(persistentOptions);
+    }
+
+    @Nullable
+    @Override
+    public <ValueT> ValueT removeOption(Option<ValueT> opt) {
+        @SuppressWarnings("unchecked") // Options should have only been inserted via insertOption()
+                ValueT value = (ValueT) options.remove(opt);
+
+        return value;
+    }
+
+    @Override
+    public <ValueT> void insertOption(Option<ValueT> opt, ValueT value) {
+        options.put(opt, value);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/OnFocusCompletedListener.java b/camera/core/src/main/java/androidx/camera/core/OnFocusCompletedListener.java
new file mode 100644
index 0000000..d15b18b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/OnFocusCompletedListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.Rect;
+
+/** Listener called when focus scan has completed. */
+public interface OnFocusCompletedListener {
+    /** Callback when focus has been locked. */
+    default void onFocusLocked(Rect afRect) {
+    }
+
+    /** Callback when unable to acquire focus. */
+    default void onFocusUnableToLock(Rect afRect) {
+    }
+
+    /** Callback when timeout is reached and af state haven't settled. */
+    default void onFocusTimedOut(Rect afRect) {
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/OptionsBundle.java b/camera/core/src/main/java/androidx/camera/core/OptionsBundle.java
new file mode 100644
index 0000000..b1cd838
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/OptionsBundle.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Collections;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * An immutable implementation of {@link Configuration}.
+ *
+ * <p>OptionsBundle is a collection of {@link Configuration.Option}s and their values which can be
+ * queried based on exact {@link Configuration.Option} objects or based on Option ids.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class OptionsBundle implements Configuration {
+
+    private static final OptionsBundle EMPTY_BUNDLE =
+            new OptionsBundle(new TreeMap<>((o1, o2) -> o1.getId().compareTo(o2.getId())));
+    // TODO: Make these options parcelable
+    protected final TreeMap<Option<?>, Object> options;
+
+    OptionsBundle(TreeMap<Option<?>, Object> options) {
+        this.options = options;
+    }
+
+    /**
+     * Create an OptionsBundle from another configuration.
+     *
+     * <p>This will copy the options/values from the provided configuration.
+     *
+     * @param otherConfig Configuration containing options/values to be copied.
+     * @return A new OptionsBundle pre-populated with options/values.
+     */
+    public static OptionsBundle from(Configuration otherConfig) {
+        // No need to create another instance since OptionsBundle is immutable
+        if (OptionsBundle.class.equals(otherConfig.getClass())) {
+            return (OptionsBundle) otherConfig;
+        }
+
+        TreeMap<Option<?>, Object> persistentOptions =
+                new TreeMap<>((o1, o2) -> o1.getId().compareTo(o2.getId()));
+        for (Option<?> opt : otherConfig.listOptions()) {
+            persistentOptions.put(opt, otherConfig.retrieveOption(opt));
+        }
+
+        return new OptionsBundle(persistentOptions);
+    }
+
+    /**
+     * Create an empty OptionsBundle.
+     *
+     * <p>This options bundle will have no option/value pairs.
+     *
+     * @return An OptionsBundle pre-populated with no options/values.
+     */
+    public static OptionsBundle emptyBundle() {
+        return EMPTY_BUNDLE;
+    }
+
+    @Override
+    public Set<Option<?>> listOptions() {
+        return Collections.unmodifiableSet(options.keySet());
+    }
+
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return options.containsKey(id);
+    }
+
+    @Override
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        ValueT value = retrieveOption(id, /*valueIfMissing=*/ null);
+        if (value == null) {
+            throw new IllegalArgumentException("Option does not exist: " + id);
+        }
+
+        return value;
+    }
+
+    @Nullable
+    @Override
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        @SuppressWarnings("unchecked") // Options should have only been inserted via insertOption()
+                ValueT value = (ValueT) options.get(id);
+        if (value == null) {
+            value = valueIfMissing;
+        }
+
+        return value;
+    }
+
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        Option<Void> query = Option.create(idStem, Void.class);
+        for (Entry<Option<?>, Object> entry : options.tailMap(query).entrySet()) {
+            if (!entry.getKey().getId().startsWith(idStem)) {
+                // We've reached the end of the range that contains our search stem.
+                break;
+            }
+
+            Option<?> option = entry.getKey();
+            if (!matcher.onOptionMatched(option)) {
+                // Caller does not need further results
+                break;
+            }
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/QueuedImageReaderProxy.java b/camera/core/src/main/java/androidx/camera/core/QueuedImageReaderProxy.java
new file mode 100644
index 0000000..3c5287a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/QueuedImageReaderProxy.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An {@link ImageReaderProxy} which maintains a queue of recently available images.
+ *
+ * <p>Like a conventional {@link android.media.ImageReader}, when the queue becomes full and the
+ * user does not close older images quickly enough, newly available images will not be added to the
+ * queue and become lost. The user is responsible for setting a listener for newly available images
+ * and closing the acquired images quickly enough.
+ */
+final class QueuedImageReaderProxy
+        implements ImageReaderProxy, ForwardingImageProxy.OnImageCloseListener {
+    private final int width;
+    private final int height;
+    private final int format;
+    private final int maxImages;
+
+    @GuardedBy("this")
+    private final Surface surface;
+
+    // maxImages is not expected to be large, because images consume a lot of memory and there
+    // cannot
+    // co-exist too many images simultaneously. So, just use a List to simplify the implementation.
+    @GuardedBy("this")
+    private final List<ImageProxy> images;
+
+    @GuardedBy("this")
+    private final Set<ImageProxy> acquiredImages = new HashSet<>();
+    @GuardedBy("this")
+    private final Set<OnReaderCloseListener> onReaderCloseListeners = new HashSet<>();
+    // Current access position in the queue.
+    @GuardedBy("this")
+    private int currentPosition;
+    @GuardedBy("this")
+    @Nullable
+    private ImageReaderProxy.OnImageAvailableListener onImageAvailableListener;
+    @GuardedBy("this")
+    @Nullable
+    private Handler onImageAvailableHandler;
+    @GuardedBy("this")
+    private boolean closed;
+
+    /**
+     * Creates a new instance of a queued image reader proxy.
+     *
+     * @param width     of the images
+     * @param height    of the images
+     * @param format    of the images
+     * @param maxImages capacity of the queue
+     * @param surface   to which the reader is attached
+     * @return new {@link QueuedImageReaderProxy} instance
+     */
+    QueuedImageReaderProxy(int width, int height, int format, int maxImages, Surface surface) {
+        this.width = width;
+        this.height = height;
+        this.format = format;
+        this.maxImages = maxImages;
+        this.surface = surface;
+        images = new ArrayList<>(maxImages);
+        currentPosition = 0;
+        closed = false;
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireLatestImage() {
+        throwExceptionIfClosed();
+        if (images.isEmpty()) {
+            return null;
+        }
+        if (currentPosition >= images.size()) {
+            throw new IllegalStateException("Max images have already been acquired without close.");
+        }
+
+        // Close all images up to the tail of the list, except for already acquired images.
+        List<ImageProxy> imagesToClose = new ArrayList<>();
+        for (int i = 0; i < images.size() - 1; ++i) {
+            if (!acquiredImages.contains(images.get(i))) {
+                imagesToClose.add(images.get(i));
+            }
+        }
+        for (ImageProxy image : imagesToClose) {
+            // Calling image.close() will cause this.onImageClosed(image) to be called.
+            image.close();
+        }
+
+        // Move the current position to the tail of the list.
+        currentPosition = images.size() - 1;
+        ImageProxy acquiredImage = images.get(currentPosition++);
+        acquiredImages.add(acquiredImage);
+        return acquiredImage;
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireNextImage() {
+        throwExceptionIfClosed();
+        if (images.isEmpty()) {
+            return null;
+        }
+        if (currentPosition >= images.size()) {
+            throw new IllegalStateException("Max images have already been acquired without close.");
+        }
+        ImageProxy acquiredImage = images.get(currentPosition++);
+        acquiredImages.add(acquiredImage);
+        return acquiredImage;
+    }
+
+    /**
+     * Adds an image to the tail of the queue.
+     *
+     * <p>If the queue already contains the max number of images, the given image is not added to
+     * the queue and is closed. This is consistent with the documented behavior of an {@link
+     * android.media.ImageReader}, where new images may be lost if older images are not closed
+     * quickly enough.
+     *
+     * <p>If the image is added to the queue and an on-image-available listener has been previously
+     * set, the listener is notified that the new image is available.
+     *
+     * @param image to add
+     */
+    synchronized void enqueueImage(ForwardingImageProxy image) {
+        throwExceptionIfClosed();
+        if (images.size() < maxImages) {
+            images.add(image);
+            image.addOnImageCloseListener(this);
+            if (onImageAvailableListener != null && onImageAvailableHandler != null) {
+                final OnImageAvailableListener listener = onImageAvailableListener;
+                onImageAvailableHandler.post(
+                        () -> {
+                            if (!QueuedImageReaderProxy.this.isClosed()) {
+                                listener.onImageAvailable(QueuedImageReaderProxy.this);
+                            }
+                        });
+            }
+        } else {
+            image.close();
+        }
+    }
+
+    @Override
+    public synchronized void close() {
+        if (!closed) {
+            setOnImageAvailableListener(null, null);
+            // We need to copy into a different list, because closing an image triggers the on-close
+            // listener which in turn modifies the original list.
+            List<ImageProxy> imagesToClose = new ArrayList<>(images);
+            for (ImageProxy image : imagesToClose) {
+                image.close();
+            }
+            images.clear();
+            closed = true;
+            notifyOnReaderCloseListeners();
+        }
+    }
+
+    @Override
+    public int getHeight() {
+        throwExceptionIfClosed();
+        return height;
+    }
+
+    @Override
+    public int getWidth() {
+        throwExceptionIfClosed();
+        return width;
+    }
+
+    @Override
+    public int getImageFormat() {
+        throwExceptionIfClosed();
+        return format;
+    }
+
+    @Override
+    public int getMaxImages() {
+        throwExceptionIfClosed();
+        return maxImages;
+    }
+
+    @Override
+    public synchronized Surface getSurface() {
+        throwExceptionIfClosed();
+        return surface;
+    }
+
+    @Override
+    public synchronized void setOnImageAvailableListener(
+            @Nullable OnImageAvailableListener onImageAvailableListener,
+            @Nullable Handler onImageAvailableHandler) {
+        throwExceptionIfClosed();
+        this.onImageAvailableListener = onImageAvailableListener;
+        this.onImageAvailableHandler = onImageAvailableHandler;
+    }
+
+    @Override
+    public synchronized void onImageClose(ImageProxy image) {
+        int index = images.indexOf(image);
+        if (index >= 0) {
+            images.remove(index);
+            if (index <= currentPosition) {
+                currentPosition--;
+            }
+        }
+        acquiredImages.remove(image);
+    }
+
+    /** Returns the current number of images in the queue. */
+    synchronized int getCurrentImages() {
+        throwExceptionIfClosed();
+        return images.size();
+    }
+
+    /** Returns true if the reader is already closed. */
+    synchronized boolean isClosed() {
+        return closed;
+    }
+
+    /**
+     * Adds a listener for close calls on this reader.
+     *
+     * @param listener to add
+     */
+    synchronized void addOnReaderCloseListener(OnReaderCloseListener listener) {
+        onReaderCloseListeners.add(listener);
+    }
+
+    private synchronized void throwExceptionIfClosed() {
+        if (closed) {
+            throw new IllegalStateException("This reader is already closed.");
+        }
+    }
+
+    private synchronized void notifyOnReaderCloseListeners() {
+        for (OnReaderCloseListener listener : onReaderCloseListeners) {
+            listener.onReaderClose(this);
+        }
+    }
+
+    /** Listener for the reader close event. */
+    interface OnReaderCloseListener {
+        /**
+         * Callback for reader close.
+         *
+         * @param imageReader which is closed
+         */
+        void onReaderClose(ImageReaderProxy imageReader);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ReferenceCountedImageProxy.java b/camera/core/src/main/java/androidx/camera/core/ReferenceCountedImageProxy.java
new file mode 100644
index 0000000..6055768
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ReferenceCountedImageProxy.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.Image;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+
+/**
+ * An {@link ImageProxy} which allows forking images with reference counting.
+ *
+ * <p>When a new instance is constructed, it starts with a reference count of 1. When {@link
+ * #fork()} is called, the reference count increments by 1. When {@link #close()} is called on a
+ * forked image reference, the reference count decrements by 1. When the reference count reaches 0
+ * after a call to {@link #close()}, the underlying {@link Image} is closed.
+ */
+final class ReferenceCountedImageProxy extends ForwardingImageProxy {
+    @GuardedBy("this")
+    private int referenceCount = 1;
+
+    /**
+     * Creates a new instance which wraps the given image and sets the reference count to 1.
+     *
+     * @param image to wrap
+     * @return a new {@link ReferenceCountedImageProxy} instance
+     */
+    ReferenceCountedImageProxy(ImageProxy image) {
+        super(image);
+    }
+
+    /**
+     * Forks a copy of the image.
+     *
+     * <p>If the reference count is 0, meaning the image has already been closed previously, null is
+     * returned. Otherwise, a forked copy is returned and the reference count is incremented.
+     */
+    @Nullable
+    synchronized ImageProxy fork() {
+        if (referenceCount <= 0) {
+            return null;
+        } else {
+            referenceCount++;
+            return new SingleCloseImageProxy(this);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>When the image is closed, the reference count is decremented. If the reference count
+     * becomes 0 after this close call, the underlying {@link Image} is also closed.
+     */
+    @Override
+    public synchronized void close() {
+        if (referenceCount > 0) {
+            referenceCount--;
+            if (referenceCount <= 0) {
+                super.close();
+            }
+        }
+    }
+
+    /** Returns the current reference count. */
+    synchronized int getReferenceCount() {
+        return referenceCount;
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SessionConfiguration.java b/camera/core/src/main/java/androidx/camera/core/SessionConfiguration.java
new file mode 100644
index 0000000..c39e32b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SessionConfiguration.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureRequest.Key;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Configurations needed for a capture session.
+ *
+ * <p>The SessionConfiguration contains all the {@link android.hardware.camera2} parameters that are
+ * required to initialize a {@link android.hardware.camera2.CameraCaptureSession} and issue a {@link
+ * CaptureRequest}.
+ *
+ * @hide
+ */
+public final class SessionConfiguration {
+
+    /** The set of {@link Surface} that data from the camera will be put into. */
+    private final List<DeferrableSurface> surfaces;
+    /** The state callback for a {@link CameraDevice}. */
+    private final CameraDevice.StateCallback deviceStateCallback;
+    /** The state callback for a {@link CameraCaptureSession}. */
+    private final CameraCaptureSession.StateCallback sessionStateCallback;
+    /** The configuration for building the {@link CaptureRequest}. */
+    private final CaptureRequestConfiguration captureRequestConfiguration;
+
+    /**
+     * Private constructor for a SessionConfiguration.
+     *
+     * <p>In practice, the {@link SessionConfiguration.BaseBuilder} will be used to construct a
+     * SessionConfiguration.
+     *
+     * @param surfaces                    The set of {@link Surface} where data will be put into.
+     * @param deviceStateCallback         The state callback for a {@link CameraDevice}.
+     * @param sessionStateCallback        The state callback for a {@link CameraCaptureSession}.
+     * @param captureRequestConfiguration The configuration for building the {@link CaptureRequest}.
+     */
+    SessionConfiguration(
+            List<DeferrableSurface> surfaces,
+            StateCallback deviceStateCallback,
+            CameraCaptureSession.StateCallback sessionStateCallback,
+            CaptureRequestConfiguration captureRequestConfiguration) {
+        this.surfaces = surfaces;
+        this.deviceStateCallback = deviceStateCallback;
+        this.sessionStateCallback = sessionStateCallback;
+        this.captureRequestConfiguration = captureRequestConfiguration;
+    }
+
+    /** Returns an instance of a session configuration with minimal configurations. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static SessionConfiguration defaultEmptySessionConfiguration() {
+        return new SessionConfiguration(
+                new ArrayList<>(),
+                CameraDeviceStateCallbacks.createNoOpCallback(),
+                CameraCaptureSessionStateCallbacks.createNoOpCallback(),
+                new CaptureRequestConfiguration.Builder().build());
+    }
+
+    public List<DeferrableSurface> getSurfaces() {
+        return Collections.unmodifiableList(surfaces);
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public Map<Key<?>, CaptureRequestParameter<?>> getCameraCharacteristics() {
+        return captureRequestConfiguration.getCameraCharacteristics();
+    }
+
+    public Configuration getImplementationOptions() {
+        return captureRequestConfiguration.getImplementationOptions();
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getTemplateType() {
+        return captureRequestConfiguration.getTemplateType();
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraDevice.StateCallback getDeviceStateCallback() {
+        return deviceStateCallback;
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraCaptureSession.StateCallback getSessionStateCallback() {
+        return sessionStateCallback;
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraCaptureCallback getCameraCaptureCallback() {
+        return captureRequestConfiguration.getCameraCaptureCallback();
+    }
+
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CaptureRequestConfiguration getCaptureRequestConfiguration() {
+        return captureRequestConfiguration;
+    }
+
+    /**
+     * Interface for unpacking a configuration into a SessionConfiguration.Builder
+     *
+     * <p>TODO(b/120949879): This will likely be removed once SessionConfiguration is refactored to
+     * remove camera2 dependencies.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public interface OptionUnpacker {
+        void unpack(UseCaseConfiguration<?> config, SessionConfiguration.Builder builder);
+    }
+
+    /** Base builder for easy modification/rebuilding of a {@link SessionConfiguration}. */
+    static class BaseBuilder {
+        protected final Set<DeferrableSurface> surfaces = new HashSet<>();
+        protected final CaptureRequestConfiguration.Builder captureRequestConfigBuilder =
+                new CaptureRequestConfiguration.Builder();
+        protected CameraDevice.StateCallback deviceStateCallback =
+                CameraDeviceStateCallbacks.createNoOpCallback();
+        protected CameraCaptureSession.StateCallback sessionStateCallback =
+                CameraCaptureSessionStateCallbacks.createNoOpCallback();
+    }
+
+    /** Builder for easy modification/rebuilding of a {@link SessionConfiguration}. */
+    public static class Builder extends BaseBuilder {
+        /**
+         * Creates a {@link Builder} from a {@link UseCaseConfiguration}.
+         *
+         * <p>Populates the builder with all the properties defined in the base configuration.
+         */
+        public static Builder createFrom(UseCaseConfiguration<?> configuration) {
+            OptionUnpacker unpacker = configuration.getOptionUnpacker(null);
+            if (unpacker == null) {
+                throw new IllegalStateException(
+                        "Implementation is missing option unpacker for "
+                                + configuration.getTargetName(configuration.toString()));
+            }
+
+            Builder builder = new Builder();
+
+            // Unpack the configuration into this builder
+            unpacker.unpack(configuration, builder);
+            return builder;
+        }
+
+        /**
+         * Set the template characteristics of the SessionConfiguration.
+         *
+         * @param templateType Template constant that must match those defined by {@link
+         *                     CameraDevice}
+         *                     <p>TODO(b/120949879): This is camera2 implementation detail that
+         *                     should be moved
+         */
+        public void setTemplateType(int templateType) {
+            captureRequestConfigBuilder.setTemplateType(templateType);
+        }
+
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        public void setDeviceStateCallback(CameraDevice.StateCallback deviceStateCallback) {
+            this.deviceStateCallback = deviceStateCallback;
+        }
+
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        public void setSessionStateCallback(
+                CameraCaptureSession.StateCallback sessionStateCallback) {
+            this.sessionStateCallback = sessionStateCallback;
+        }
+
+        public void setCameraCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+            captureRequestConfigBuilder.setCameraCaptureCallback(cameraCaptureCallback);
+        }
+
+        public void addSurface(DeferrableSurface surface) {
+            surfaces.add(surface);
+            captureRequestConfigBuilder.addSurface(surface);
+        }
+
+        public void addNonRepeatingSurface(DeferrableSurface surface) {
+            surfaces.add(surface);
+        }
+
+        public void removeSurface(DeferrableSurface surface) {
+            surfaces.remove(surface);
+            captureRequestConfigBuilder.removeSurface(surface);
+        }
+
+        public void clearSurfaces() {
+            surfaces.clear();
+            captureRequestConfigBuilder.clearSurfaces();
+        }
+
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        public <T> void addCharacteristic(Key<T> key, T value) {
+            captureRequestConfigBuilder.addCharacteristic(key, value);
+        }
+
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public void addCharacteristics(Map<Key<?>, CaptureRequestParameter<?>> characteristics) {
+            captureRequestConfigBuilder.addCharacteristics(characteristics);
+        }
+
+        public void setImplementationOptions(Configuration config) {
+            captureRequestConfigBuilder.setImplementationOptions(config);
+        }
+
+        /**
+         * Builds an instance of a SessionConfiguration that has all the combined parameters of the
+         * SessionConfiguration that have been added to the Builder.
+         */
+        public SessionConfiguration build() {
+            return new SessionConfiguration(
+                    new ArrayList<>(surfaces),
+                    deviceStateCallback,
+                    sessionStateCallback,
+                    captureRequestConfigBuilder.build());
+        }
+    }
+
+    /**
+     * Builder for combining multiple instances of {@link SessionConfiguration}. This will check if
+     * all the parameters for the {@link SessionConfiguration} are compatible with each other
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class ValidatingBuilder extends BaseBuilder {
+        private static final String TAG = "ValidatingBuilder";
+        private final List<CameraDevice.StateCallback> deviceStateCallbacks = new ArrayList<>();
+        private final List<CameraCaptureSession.StateCallback> sessionStateCallbacks =
+                new ArrayList<>();
+        private final List<CameraCaptureCallback> cameraCaptureCallbacks = new ArrayList<>();
+        private boolean valid = true;
+        private boolean templateSet = false;
+
+        /**
+         * Add the SessionConfiguration to the set of SessionConfiguration that have been aggregated
+         * by the ValidatingBuilder
+         */
+        public void add(SessionConfiguration sessionConfiguration) {
+            CaptureRequestConfiguration captureRequestConfiguration =
+                    sessionConfiguration.getCaptureRequestConfiguration();
+
+            // Check template
+            if (!templateSet) {
+                captureRequestConfigBuilder.setTemplateType(
+                        captureRequestConfiguration.getTemplateType());
+                templateSet = true;
+            } else if (captureRequestConfigBuilder.getTemplateType()
+                    != captureRequestConfiguration.getTemplateType()) {
+                String errorMessage =
+                        "Invalid configuration due to template type: "
+                                + captureRequestConfigBuilder.getTemplateType()
+                                + " != "
+                                + captureRequestConfiguration.getTemplateType();
+                Log.d(TAG, errorMessage);
+                valid = false;
+            }
+
+            // Check device state callback
+            deviceStateCallbacks.add(sessionConfiguration.getDeviceStateCallback());
+
+            // Check session state callback
+            sessionStateCallbacks.add(sessionConfiguration.getSessionStateCallback());
+
+            // Check camera capture callback
+            cameraCaptureCallbacks.add(captureRequestConfiguration.getCameraCaptureCallback());
+
+            // Check surfaces
+            surfaces.addAll(sessionConfiguration.getSurfaces());
+
+            // Check capture request surfaces
+            captureRequestConfigBuilder
+                    .getSurfaces()
+                    .addAll(captureRequestConfiguration.getSurfaces());
+
+            captureRequestConfigBuilder.addImplementationOptions(
+                    captureRequestConfiguration.getImplementationOptions());
+
+            if (!surfaces.containsAll(captureRequestConfigBuilder.getSurfaces())) {
+                String errorMessage =
+                        "Invalid configuration due to capture request surfaces are not a subset "
+                                + "of surfaces";
+                Log.d(TAG, errorMessage);
+                valid = false;
+            }
+
+            // Check characteristics
+            for (Map.Entry<Key<?>, CaptureRequestParameter<?>> entry :
+                    captureRequestConfiguration.getCameraCharacteristics().entrySet()) {
+                Key<?> addedKey = entry.getKey();
+                if (captureRequestConfigBuilder.getCharacteristic().containsKey(entry.getKey())) {
+                    // value is equal
+                    CaptureRequestParameter<?> addedValue = entry.getValue();
+                    CaptureRequestParameter<?> oldValue =
+                            captureRequestConfigBuilder.getCharacteristic().get(addedKey);
+                    if (!addedValue.getValue().equals(oldValue.getValue())) {
+                        String errorMessage =
+                                "Invalid configuration due to conflicting CaptureRequest.Keys: "
+                                        + addedValue
+                                        + " != "
+                                        + oldValue;
+                        Log.d(TAG, errorMessage);
+                        valid = false;
+                    }
+                } else {
+                    captureRequestConfigBuilder
+                            .getCharacteristic()
+                            .put(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+
+        /** Check if the set of SessionConfiguration that have been combined are valid */
+        public boolean isValid() {
+            return templateSet && valid;
+        }
+
+        /**
+         * Builds an instance of a SessionConfiguration that has all the combined parameters of the
+         * SessionConfiguration that have been added to the ValidatingBuilder.
+         */
+        public SessionConfiguration build() {
+            if (!valid) {
+                throw new IllegalArgumentException("Unsupported session configuration combination");
+            }
+            captureRequestConfigBuilder.setCameraCaptureCallback(
+                    CameraCaptureCallbacks.createComboCallback(cameraCaptureCallbacks));
+            return new SessionConfiguration(
+                    new ArrayList<>(surfaces),
+                    CameraDeviceStateCallbacks.createComboCallback(deviceStateCallbacks),
+                    CameraCaptureSessionStateCallbacks.createComboCallback(sessionStateCallbacks),
+                    captureRequestConfigBuilder.build());
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SingleCloseImageProxy.java b/camera/core/src/main/java/androidx/camera/core/SingleCloseImageProxy.java
new file mode 100644
index 0000000..0fa842a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SingleCloseImageProxy.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.GuardedBy;
+
+/** A {@link ImageProxy} which filters out redundant calls to {@link #close()}. */
+final class SingleCloseImageProxy extends ForwardingImageProxy {
+    @GuardedBy("this")
+    private boolean closed = false;
+
+    /**
+     * Creates a new instances which wraps the given image.
+     *
+     * @param image to wrap
+     * @return new {@link SingleCloseImageProxy} instance
+     */
+    SingleCloseImageProxy(ImageProxy image) {
+        super(image);
+    }
+
+    @Override
+    public synchronized void close() {
+        if (!closed) {
+            closed = true;
+            super.close();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SurfaceCombination.java b/camera/core/src/main/java/androidx/camera/core/SurfaceCombination.java
new file mode 100644
index 0000000..d8ae203
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceCombination.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Surface configuration combination
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices. This structure is used to store a list of surface configuration as a combination.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class SurfaceCombination {
+
+    private final List<SurfaceConfiguration> surfaceConfigurationList = new ArrayList<>();
+    ;
+
+    public SurfaceCombination() {
+    }
+
+    private static void generateArrangements(
+            List<int[]> arrangementsResultList, int n, int[] result, int index) {
+        if (index >= result.length) {
+            arrangementsResultList.add(result.clone());
+            return;
+        }
+
+        for (int i = 0; i < n; i++) {
+            boolean included = false;
+
+            for (int j = 0; j < index; j++) {
+                if (i == result[j]) {
+                    included = true;
+                    break;
+                }
+            }
+
+            if (!included) {
+                result[index] = i;
+                generateArrangements(arrangementsResultList, n, result, index + 1);
+            }
+        }
+    }
+
+    public boolean addSurfaceConfiguration(SurfaceConfiguration surfaceConfiguration) {
+        if (surfaceConfiguration == null) {
+            return false;
+        }
+
+        return surfaceConfigurationList.add(surfaceConfiguration);
+    }
+
+    public boolean removeSurfaceConfiguration(SurfaceConfiguration surfaceConfiguration) {
+        if (surfaceConfiguration == null) {
+            return false;
+        }
+
+        return surfaceConfigurationList.remove(surfaceConfiguration);
+    }
+
+    public List<SurfaceConfiguration> getSurfaceConfigurationList() {
+        return surfaceConfigurationList;
+    }
+
+    /**
+     * Check whether the input surface configuration list is under the capability of the combination
+     * of this object.
+     *
+     * @param configurationList the surface configuration list to be compared
+     * @return the check result that whether it could be supported
+     */
+    public boolean isSupported(List<SurfaceConfiguration> configurationList) {
+        boolean isSupported = false;
+
+        if (configurationList == null || configurationList.isEmpty()) {
+            return true;
+        }
+
+        /**
+         * Sublist of this surfaceConfiguration may be able to support the desired configuration.
+         * For example, (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (JPEG, MAXIMUM) can supported by the
+         * following level3 camera device combination - (PRIV, PREVIEW) + (PRIV, ANALYSIS) + (JPEG,
+         * MAXIMUM) + (RAW, MAXIMUM).
+         */
+        if (configurationList.size() > surfaceConfigurationList.size()) {
+            return false;
+        }
+
+        List<int[]> elementsArrangements = getElementsArrangements(surfaceConfigurationList.size());
+
+        for (int[] elementsArrangement : elementsArrangements) {
+            boolean checkResult = true;
+
+            for (int index = 0; index < surfaceConfigurationList.size(); index++) {
+                if (elementsArrangement[index] < configurationList.size()) {
+                    checkResult &=
+                            surfaceConfigurationList
+                                    .get(index)
+                                    .isSupported(configurationList.get(elementsArrangement[index]));
+
+                    if (!checkResult) {
+                        break;
+                    }
+                }
+            }
+
+            if (checkResult) {
+                isSupported = true;
+                break;
+            }
+        }
+
+        return isSupported;
+    }
+
+    private List<int[]> getElementsArrangements(int n) {
+        List<int[]> arrangementsResultList = new ArrayList<>();
+
+        generateArrangements(arrangementsResultList, n, new int[n], 0);
+
+        return arrangementsResultList;
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SurfaceConfiguration.java b/camera/core/src/main/java/androidx/camera/core/SurfaceConfiguration.java
new file mode 100644
index 0000000..b1e6e80
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceConfiguration.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.hardware.camera2.CameraCaptureSession.StateCallback;
+import android.os.Handler;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.List;
+
+/**
+ * Surface configuration type and size pair
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices. It defines what combination
+ * of surface configuration type and size pairs can be supported for different hardware level camera
+ * devices.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@AutoValue
+public abstract class SurfaceConfiguration {
+    /** Prevent sublcassing */
+    SurfaceConfiguration() {
+    }
+
+    public static SurfaceConfiguration create(ConfigurationType type, ConfigurationSize size) {
+        return new AutoValue_SurfaceConfiguration(type, size);
+    }
+
+    public abstract ConfigurationType getConfigurationType();
+
+    public abstract ConfigurationSize getConfigurationSize();
+
+    /**
+     * Check whether the input surface configuration has a smaller size than this object and can be
+     * supported
+     *
+     * @param surfaceConfiguration the surface configuration to be compared
+     * @return the check result that whether it could be supported
+     */
+    public final boolean isSupported(SurfaceConfiguration surfaceConfiguration) {
+        boolean isSupported = false;
+        ConfigurationType configurationType = surfaceConfiguration.getConfigurationType();
+        ConfigurationSize configurationSize = surfaceConfiguration.getConfigurationSize();
+
+        // Check size and type to make sure it could be supported
+        if (configurationSize.id <= getConfigurationSize().id
+                && configurationType == getConfigurationType()) {
+            isSupported = true;
+        }
+        return isSupported;
+    }
+
+    /**
+     * The Camera2 configuration type for the surface.
+     *
+     * <p>These are the enumerations defined in {@link
+     * android.hardware.camera2.CameraDevice#createCaptureSession(List, StateCallback, Handler)}.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public enum ConfigurationType {
+        PRIV,
+        YUV,
+        JPEG,
+        RAW
+    }
+
+    /**
+     * The Camera2 stream sizes for the surface.
+     *
+     * <p>These are the enumerations defined in {@link
+     * android.hardware.camera2.CameraDevice#createCaptureSession(List, StateCallback, Handler)}.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public enum ConfigurationSize {
+        /** Default AYALYSIS size is 640x480. */
+        ANALYSIS(0),
+        /**
+         * PREVIEW refers to the best size match to the device's screen resolution, or to 1080p
+         * (1920x1080), whichever is smaller.
+         */
+        PREVIEW(1),
+        /**
+         * RECORD refers to the camera device's maximum supported recording resolution, as
+         * determined by CamcorderProfile.
+         */
+        RECORD(2),
+        /**
+         * MAXIMUM refers to the camera device's maximum output resolution for that format or target
+         * from StreamConfigurationMap.getOutputSizes(int)
+         */
+        MAXIMUM(3),
+        /** NOT_SUPPORT is for the size larger than MAXIMUM */
+        NOT_SUPPORT(4);
+
+        final int id;
+
+        ConfigurationSize(int id) {
+            this.id = id;
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SurfaceSizeDefinition.java b/camera/core/src/main/java/androidx/camera/core/SurfaceSizeDefinition.java
new file mode 100644
index 0000000..c79ce37
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceSizeDefinition.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Map;
+
+/**
+ * Camera device surface size definition
+ *
+ * <p>{@link android.hardware.camera2.CameraDevice#createCaptureSession} defines the default
+ * guaranteed stream combinations for different hardware level devices.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@SuppressWarnings("AutoValueImmutableFields")
+@AutoValue
+public abstract class SurfaceSizeDefinition {
+
+    /** Prevent subclassing */
+    SurfaceSizeDefinition() {
+    }
+
+    /**
+     * Create a SurfaceSizeDenifition object with input analysis, preview, record and maximum sizes
+     *
+     * @param analysisSize   Default AYALYSIS size is * 640x480.
+     * @param previewSize    PREVIEW refers to the best size match to the device's screen
+     *                       resolution,
+     *                       or to 1080p * (1920x1080), whichever is smaller.
+     * @param recordSize     RECORD refers to the camera device's maximum supported * recording
+     *                       resolution, as determined by CamcorderProfile.
+     * @param maximumSizeMap MAXIMUM refers to the camera * device's maximum output resolution for
+     *                       that format or target from * StreamConfigurationMap.getOutputSizes(int)
+     * @return new {@link SurfaceSizeDefinition} object
+     */
+    public static SurfaceSizeDefinition create(
+            Size analysisSize,
+            Size previewSize,
+            Size recordSize,
+            Map<Integer, Size> maximumSizeMap) {
+        return new AutoValue_SurfaceSizeDefinition(
+                analysisSize, previewSize, recordSize, maximumSizeMap);
+    }
+
+    public abstract Size getAnalysisSize();
+
+    public abstract Size getPreviewSize();
+
+    public abstract Size getRecordSize();
+
+    public abstract Map<Integer, Size> getMaximumSizeMap();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/TargetConfiguration.java b/camera/core/src/main/java/androidx/camera/core/TargetConfiguration.java
new file mode 100644
index 0000000..ce95a8a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/TargetConfiguration.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.UUID;
+
+/**
+ * Configuration containing options used to identify the target class and object being configured.
+ *
+ * @param <T> The type of the object being configured.
+ */
+public interface TargetConfiguration<T> extends Configuration.Reader {
+
+    /**
+     * Option: camerax.core.target.name
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<String> OPTION_TARGET_NAME = Option.create("camerax.core.target.name", String.class);
+    /**
+     * Option: camerax.core.target.class
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<Class<?>> OPTION_TARGET_CLASS =
+            Option.create("camerax.core.target.class", new TypeReference<Class<?>>() {
+            });
+
+    /**
+     * Retrieves the class of the object being configured.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    default Class<T> getTargetClass(@Nullable Class<T> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<T> storedClass = (Class<T>) retrieveOption(OPTION_TARGET_CLASS,
+                valueIfMissing);
+        return storedClass;
+    }
+
+    /**
+     * Retrieves the class of the object being configured.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    default Class<T> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<T> storedClass = (Class<T>) retrieveOption(OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    /**
+     * Retrieves the name of the target object being configured.
+     *
+     * <p>The name should be a value that can uniquely identify an instance of the object being
+     * configured.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    default String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    // Option Declarations:
+    // ***********************************************************************************************
+
+    /**
+     * Retrieves the name of the target object being configured.
+     *
+     * <p>The name should be a value that can uniquely identify an instance of the object being
+     * configured.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    default String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    /**
+     * Builder for a {@link TargetConfiguration}.
+     *
+     * <p>A {@link TargetConfiguration} contains options used to identify the target class and
+     * object being configured.
+     *
+     * @param <T> The type of the object being configured.
+     * @param <C> The top level configuration which will be generated by {@link #build()}.
+     * @param <B> The top level builder type for which this builder is composed with.
+     */
+    interface Builder<T, C extends Configuration, B extends Builder<T, C, B>>
+            extends Configuration.Builder<C, B> {
+
+        /**
+         * Sets the class of the object being configured.
+         *
+         * <p>Setting the target class will automatically generate a unique target name if one does
+         * not already exist in this configuration.
+         *
+         * @param targetClass A class object corresponding to the class of the object being
+         *                    configured.
+         * @return the current Builder.
+         * @hide
+         */
+        default B setTargetClass(Class<T> targetClass) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_CLASS, targetClass);
+
+            // If no name is set yet, then generate a unique name
+            if (null == getMutableConfiguration().retrieveOption(OPTION_TARGET_NAME, null)) {
+                String targetName = targetClass.getCanonicalName() + "-" + UUID.randomUUID();
+                setTargetName(targetName);
+            }
+
+            return builder();
+        }
+
+        /**
+         * Sets the name of the target object being configured.
+         *
+         * <p>The name should be a value that can uniquely identify an instance of the object being
+         * configured.
+         *
+         * @param targetName A unique string identifier for the instance of the class being
+         *                   configured.
+         * @return the current Builder.
+         */
+        default B setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ThreadConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ThreadConfiguration.java
new file mode 100644
index 0000000..c727025
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ThreadConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.os.Handler;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** Configuration containing options pertaining to threads used by the configured object. */
+public interface ThreadConfiguration extends Configuration.Reader {
+
+    /**
+     * Option: camerax.core.thread.callbackHandler
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<Handler> OPTION_CALLBACK_HANDLER =
+            Option.create("camerax.core.thread.callbackHandler", Handler.class);
+
+    /**
+     * Returns the default handler that will be used for callbacks.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    default Handler getCallbackHandler(@Nullable Handler valueIfMissing) {
+        return retrieveOption(OPTION_CALLBACK_HANDLER, valueIfMissing);
+    }
+
+    /**
+     * Returns the default handler that will be used for callbacks.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    default Handler getCallbackHandler() {
+        return retrieveOption(OPTION_CALLBACK_HANDLER);
+    }
+
+    // Option Declarations:
+    // ***********************************************************************************************
+
+    /**
+     * Builder for a {@link ThreadConfiguration}.
+     *
+     * @param <C> The top level configuration which will be generated by {@link #build()}.
+     * @param <B> The top level builder type for which this builder is composed with.
+     */
+    interface Builder<C extends Configuration, B extends Builder<C, B>>
+            extends Configuration.Builder<C, B> {
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        MutableConfiguration getMutableConfiguration();
+
+        /**
+         * Sets the default handler that will be used for callbacks.
+         *
+         * @param handler The handler which will be used to post callbacks.
+         * @return the current Builder.
+         */
+        default B setCallbackHandler(Handler handler) {
+            getMutableConfiguration().insertOption(OPTION_CALLBACK_HANDLER, handler);
+            return builder();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/TypeReference.java b/camera/core/src/main/java/androidx/camera/core/TypeReference.java
new file mode 100644
index 0000000..e2c3d02
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/TypeReference.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2018 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 androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+
+/**
+ * Super type token; allows capturing generic types at runtime by forcing them to be reified.
+ *
+ * <p>Usage example:
+ *
+ * <pre>{@code
+ *      // using anonymous classes (preferred)
+ *      TypeReference&lt;Integer> intToken = new TypeReference&lt;Integer>() {{ }};
+ *
+ *      // using named classes
+ *      class IntTypeReference extends TypeReference&lt;Integer> {...}
+ *      TypeReference&lt;Integer> intToken = new IntTypeReference();
+ * }</p>
+ * </pre>
+ *
+ * <p>Unlike the reference implementation, this bans nested TypeVariables; that is all dynamic types
+ * must equal to the static types.
+ *
+ * <p>See <a href="http://gafter.blogspot.com/2007/05/limitation-of-super-type-tokens.html">
+ * http://gafter.blogspot.com/2007/05/limitation-of-super-type-tokens.html</a> for more details.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public abstract class TypeReference<T> {
+    private final Type type;
+    private final int hash;
+
+    /**
+     * Create a new type reference for {@code T}.
+     *
+     * @throws IllegalArgumentException if {@code T}'s actual type contains a type variable
+     * @see TypeReference
+     */
+    protected TypeReference() {
+        ParameterizedType thisType = (ParameterizedType) getClass().getGenericSuperclass();
+
+        // extract the "T" from TypeReference<T>
+        type = thisType.getActualTypeArguments()[0];
+
+        /*
+         * Prohibit type references with type variables such as
+         *
+         *    class GenericListToken<T> extends TypeReference<List<T>>
+         *
+         * Since the "T" there is not known without an instance of T, type equality would
+         * consider *all* Lists equal regardless of T. Allowing this would defeat
+         * some of the type safety of a type reference.
+         */
+        if (containsTypeVariable(type)) {
+            throw new IllegalArgumentException(
+                    "Including a type variable in a type reference is not allowed");
+        }
+        hash = type.hashCode();
+    }
+
+    TypeReference(Type type) {
+        this.type = type;
+        if (containsTypeVariable(this.type)) {
+            throw new IllegalArgumentException(
+                    "Including a type variable in a type reference is not allowed");
+        }
+        hash = this.type.hashCode();
+    }
+
+    /**
+     * Create a specialized type reference from a dynamic class instance, bypassing the standard
+     * compile-time checks.
+     *
+     * <p>As with a regular type reference, the {@code klass} must not contain any type variables.
+     *
+     * @param klass a non-{@code null} {@link Class} instance
+     * @return a type reference which captures {@code T} at runtime
+     * @throws IllegalArgumentException if {@code T} had any type variables
+     */
+    public static <T> TypeReference<T> createSpecializedTypeReference(Class<T> klass) {
+        return new SpecializedTypeReference<T>(klass);
+    }
+
+    private static final Class<?> getRawType(Type type) {
+        if (type == null) {
+            throw new NullPointerException("type must not be null");
+        }
+
+        if (type instanceof Class<?>) {
+            return (Class<?>) type;
+        } else if (type instanceof ParameterizedType) {
+            return (Class<?>) ((ParameterizedType) type).getRawType();
+        } else if (type instanceof GenericArrayType) {
+            return getArrayClass(getRawType(((GenericArrayType) type).getGenericComponentType()));
+        } else if (type instanceof WildcardType) {
+            // Should be at most 1 upper bound, but treat it like an array for simplicity
+            return getRawType(((WildcardType) type).getUpperBounds());
+        } else if (type instanceof TypeVariable) {
+            throw new AssertionError("Type variables are not allowed in type references");
+        } else {
+            // Impossible
+            throw new AssertionError("Unhandled branch to get raw type for type " + type);
+        }
+    }
+
+    private static final Class<?> getRawType(Type[] types) {
+        if (types == null) {
+            return null;
+        }
+
+        for (Type type : types) {
+            Class<?> klass = getRawType(type);
+            if (klass != null) {
+                return klass;
+            }
+        }
+
+        return null;
+    }
+
+    private static final Class<?> getArrayClass(Class<?> componentType) {
+        return Array.newInstance(componentType, 0).getClass();
+    }
+
+    /**
+     * Check if the {@code type} contains a {@link TypeVariable} recursively.
+     *
+     * <p>Intuitively, a type variable is a type in a type expression that refers to a generic type
+     * which is not known at the definition of the expression (commonly seen when type parameters
+     * are used, e.g. {@code class Foo<T>}).
+     *
+     * <p>See <a href="http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.4">
+     * http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.4</a> for a more formal
+     * definition of a type variable.
+     *
+     * @param type a type object ({@code null} is allowed)
+     * @return {@code true} if there were nested type variables; {@code false} otherwise
+     */
+    public static boolean containsTypeVariable(Type type) {
+        if (type == null) {
+            // Trivially false
+            return false;
+        } else if (type instanceof TypeVariable<?>) {
+            /*
+             * T -> trivially true
+             */
+            return true;
+        } else if (type instanceof Class<?>) {
+            /*
+             * class Foo -> no type variable
+             * class Foo<T> - has a type variable
+             *
+             * This also covers the case of class Foo<T> extends ... / implements ...
+             * since everything on the right hand side would either include a type variable T
+             * or have no type variables.
+             */
+            Class<?> klass = (Class<?>) type;
+
+            // Empty array => class is not generic
+            if (klass.getTypeParameters().length != 0) {
+                return true;
+            } else {
+                // Does the outer class(es) contain any type variables?
+
+                /*
+                 * class Outer<T> {
+                 *   class Inner {
+                 *      T field;
+                 *   }
+                 * }
+                 *
+                 * In this case 'Inner' has no type parameters itself, but it still has a type
+                 * variable as part of the type definition.
+                 */
+                return containsTypeVariable(klass.getDeclaringClass());
+            }
+        } else if (type instanceof ParameterizedType) {
+            /*
+             * This is the "Foo<T1, T2, T3, ... Tn>" in the scope of a
+             *
+             *      // no type variables here, T1-Tn are known at this definition
+             *      class X extends Foo<T1, T2, T3, ... Tn>
+             *
+             *      // T1 is a type variable, T2-Tn are known at this definition
+             *      class X<T1> extends Foo<T1, T2, T3, ... Tn>
+             */
+            ParameterizedType p = (ParameterizedType) type;
+
+            // This needs to be recursively checked
+            for (Type arg : p.getActualTypeArguments()) {
+                if (containsTypeVariable(arg)) {
+                    return true;
+                }
+            }
+
+            return false;
+        } else if (type instanceof WildcardType) {
+            WildcardType wild = (WildcardType) type;
+
+            /*
+             * This is is the "?" inside of a
+             *
+             *       Foo<?> --> unbounded; trivially no type variables
+             *       Foo<? super T> --> lower bound; does T have a type variable?
+             *       Foo<? extends T> --> upper bound; does T have a type variable?
+             */
+
+            /*
+             *  According to JLS 4.5.1
+             *  (http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.5.1):
+             *
+             *  - More than 1 lower/upper bound is illegal
+             *  - Both a lower and upper bound is illegal
+             *
+             *  However, we use this 'array OR array' approach for readability
+             */
+            return containsTypeVariable(wild.getLowerBounds())
+                    || containsTypeVariable(wild.getUpperBounds());
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if any of the elements in this array contained a type variable.
+     *
+     * <p>Empty and null arrays trivially have no type variables.
+     *
+     * @param typeArray an array ({@code null} is ok) of types
+     * @return true if any elements contained a type variable; false otherwise
+     */
+    private static boolean containsTypeVariable(Type[] typeArray) {
+        if (typeArray == null) {
+            return false;
+        }
+
+        for (Type type : typeArray) {
+            if (containsTypeVariable(type)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static void toString(Type type, StringBuilder out) {
+        if (type != null) {
+            if (type instanceof TypeVariable<?>) {
+                // T
+                out.append(((TypeVariable<?>) type).getName());
+            } else if (type instanceof Class<?>) {
+                Class<?> klass = (Class<?>) type;
+
+                out.append(klass.getName());
+                toString(klass.getTypeParameters(), out);
+            } else if (type instanceof ParameterizedType) {
+                // "Foo<T1, T2, T3, ... Tn>"
+                ParameterizedType p = (ParameterizedType) type;
+
+                out.append(((Class<?>) p.getRawType()).getName());
+                toString(p.getActualTypeArguments(), out);
+            } else if (type instanceof GenericArrayType) {
+                GenericArrayType gat = (GenericArrayType) type;
+
+                toString(gat.getGenericComponentType(), out);
+                out.append("[]");
+            } else { // WildcardType, BoundedType
+                // TODO:
+                out.append(type);
+            }
+        }
+    }
+
+    private static void toString(Type[] types, StringBuilder out) {
+        if (types == null) {
+            return;
+        } else if (types.length == 0) {
+            return;
+        }
+
+        out.append("<");
+
+        for (int i = 0; i < types.length; ++i) {
+            toString(types[i], out);
+            if (i != types.length - 1) {
+                out.append(", ");
+            }
+        }
+
+        out.append(">");
+    }
+
+    /** Return the dynamic {@link Type} corresponding to the captured type {@code T}. */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Returns the raw type of T.
+     *
+     * <p>
+     *
+     * <ul>
+     * <li>If T is a Class itself, T itself is returned.
+     * <li>If T is a ParameterizedType, the raw type of the parameterized type is returned.
+     * <li>If T is a GenericArrayType, the returned type is the corresponding array class. For
+     * example: {@code List<Integer>[]} => {@code List[]}.
+     * <li>If T is a type variable or a wildcard type, the raw type of the first upper bound is
+     * returned. For example: {@code <X extends Foo>} => {@code Foo}.
+     * </ul>
+     *
+     * @return the raw type of {@code T}
+     */
+    @SuppressWarnings("unchecked")
+    public final Class<? super T> getRawType() {
+        return (Class<? super T>) getRawType(type);
+    }
+
+    /**
+     * Compare two objects for equality.
+     *
+     * <p>A TypeReference is only equal to another TypeReference if their captured type {@code T} is
+     * also equal.
+     */
+    @Override
+    public boolean equals(Object o) {
+        // Note that this comparison could inaccurately return true when comparing types
+        // with nested type variables; therefore we ban type variables in the constructor.
+        return o instanceof TypeReference<?> && type.equals(((TypeReference<?>) o).type);
+    }
+
+    @Override
+    public int hashCode() {
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("TypeReference<");
+        toString(getType(), builder);
+        builder.append(">");
+
+        return builder.toString();
+    }
+
+    private static class SpecializedTypeReference<T> extends TypeReference<T> {
+        public SpecializedTypeReference(Class<T> klass) {
+            super(klass);
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseAttachState.java b/camera/core/src/main/java/androidx/camera/core/UseCaseAttachState.java
new file mode 100644
index 0000000..340b1d8
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseAttachState.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Log;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Collection of use cases which are attached to a specific camera.
+ *
+ * <p>This class tracks the current state of activity for each use case. There are two states that
+ * the use case can be in: online and active. Online means the use case is currently ready for the
+ * camera capture, but not currently capturing. Active means the use case is either currently
+ * issuing a capture request or one has already been issued.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class UseCaseAttachState {
+    private static final String TAG = "UseCaseAttachState";
+    /** The name of the camera the use cases are attached to. */
+    private final String cameraId;
+    /** A map of the use cases to the corresponding state information. */
+    private final Map<BaseUseCase, UseCaseAttachInfo> attachedUseCasesToInfoMap = new HashMap<>();
+
+    /** Constructs an instance of the attach state which corresponds to the named camera. */
+    public UseCaseAttachState(String cameraId) {
+        this.cameraId = cameraId;
+    }
+
+    /**
+     * Sets the use case to an active state.
+     *
+     * <p>Adds the use case to the collection if not already in it.
+     */
+    public void setUseCaseActive(BaseUseCase useCase) {
+        UseCaseAttachInfo useCaseAttachInfo = getOrCreateUseCaseAttachInfo(useCase);
+        useCaseAttachInfo.active = true;
+    }
+
+    /**
+     * Sets the use case to an inactive state.
+     *
+     * <p>Removes the use case from the collection if also offline.
+     */
+    public void setUseCaseInactive(BaseUseCase useCase) {
+        if (!attachedUseCasesToInfoMap.containsKey(useCase)) {
+            return;
+        }
+
+        UseCaseAttachInfo useCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+        useCaseAttachInfo.active = false;
+        if (!useCaseAttachInfo.online) {
+            attachedUseCasesToInfoMap.remove(useCase);
+        }
+    }
+
+    /**
+     * Sets the use case to an online state.
+     *
+     * <p>Adds the use case to the collection if not already in it.
+     */
+    public void setUseCaseOnline(BaseUseCase useCase) {
+        UseCaseAttachInfo useCaseAttachInfo = getOrCreateUseCaseAttachInfo(useCase);
+        useCaseAttachInfo.online = true;
+    }
+
+    /**
+     * Sets the use case to an offline state.
+     *
+     * <p>Removes the use case from the collection if also inactive.
+     */
+    public void setUseCaseOffline(BaseUseCase useCase) {
+        if (!attachedUseCasesToInfoMap.containsKey(useCase)) {
+            return;
+        }
+        UseCaseAttachInfo useCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+        useCaseAttachInfo.online = false;
+        if (!useCaseAttachInfo.active) {
+            attachedUseCasesToInfoMap.remove(useCase);
+        }
+    }
+
+    public Collection<BaseUseCase> getOnlineUseCases() {
+        return Collections.unmodifiableCollection(
+                getUseCases(useCaseAttachInfo -> useCaseAttachInfo.online));
+    }
+
+    public Collection<BaseUseCase> getActiveAndOnlineUseCases() {
+        return Collections.unmodifiableCollection(
+                getUseCases(
+                        useCaseAttachInfo -> useCaseAttachInfo.active && useCaseAttachInfo.online));
+    }
+
+    /**
+     * Updates the session configuration for a use case.
+     *
+     * <p>If the use case is not already in the collection, nothing is done.
+     */
+    public void updateUseCase(BaseUseCase useCase) {
+        if (!attachedUseCasesToInfoMap.containsKey(useCase)) {
+            return;
+        }
+
+        // Rebuild the attach info from scratch to get the updated SessionConfiguration.
+        UseCaseAttachInfo newUseCaseAttachInfo =
+                new UseCaseAttachInfo(useCase.getSessionConfiguration(cameraId));
+
+        // Retain the online and active flags.
+        UseCaseAttachInfo oldUseCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+        newUseCaseAttachInfo.online = oldUseCaseAttachInfo.online;
+        newUseCaseAttachInfo.active = oldUseCaseAttachInfo.active;
+        attachedUseCasesToInfoMap.put(useCase, newUseCaseAttachInfo);
+    }
+
+    /** Returns a session configuration builder for use cases which are both active and online. */
+    public SessionConfiguration.ValidatingBuilder getActiveAndOnlineBuilder() {
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+
+        List<String> list = new ArrayList<>();
+        for (Entry<BaseUseCase, UseCaseAttachInfo> attachedUseCase :
+                attachedUseCasesToInfoMap.entrySet()) {
+            UseCaseAttachInfo useCaseAttachInfo = attachedUseCase.getValue();
+            if (useCaseAttachInfo.active && useCaseAttachInfo.online) {
+                BaseUseCase baseUseCase = attachedUseCase.getKey();
+                validatingBuilder.add(useCaseAttachInfo.sessionConfiguration);
+                list.add(baseUseCase.getName());
+            }
+        }
+        Log.d(TAG, "Active and online use case: " + list + " for camera: " + cameraId);
+        return validatingBuilder;
+    }
+
+    /** Returns a session configuration builder for use cases which are online. */
+    public SessionConfiguration.ValidatingBuilder getOnlineBuilder() {
+        SessionConfiguration.ValidatingBuilder validatingBuilder =
+                new SessionConfiguration.ValidatingBuilder();
+        List<String> list = new ArrayList<>();
+        for (Entry<BaseUseCase, UseCaseAttachInfo> attachedUseCase :
+                attachedUseCasesToInfoMap.entrySet()) {
+            UseCaseAttachInfo useCaseAttachInfo = attachedUseCase.getValue();
+            if (useCaseAttachInfo.online) {
+                validatingBuilder.add(useCaseAttachInfo.sessionConfiguration);
+                BaseUseCase baseUseCase = attachedUseCase.getKey();
+                list.add(baseUseCase.getName());
+            }
+        }
+        Log.d(TAG, "All use case: " + list + " for camera: " + cameraId);
+        return validatingBuilder;
+    }
+
+    private UseCaseAttachInfo getOrCreateUseCaseAttachInfo(BaseUseCase useCase) {
+        UseCaseAttachInfo useCaseAttachInfo = attachedUseCasesToInfoMap.get(useCase);
+        if (useCaseAttachInfo == null) {
+            useCaseAttachInfo = new UseCaseAttachInfo(useCase.getSessionConfiguration(cameraId));
+            attachedUseCasesToInfoMap.put(useCase, useCaseAttachInfo);
+        }
+        return useCaseAttachInfo;
+    }
+
+    private Collection<BaseUseCase> getUseCases(AttachStateFilter attachStateFilter) {
+        List<BaseUseCase> useCases = new ArrayList<>();
+        for (Entry<BaseUseCase, UseCaseAttachInfo> attachedUseCase :
+                attachedUseCasesToInfoMap.entrySet()) {
+            if (attachStateFilter == null || attachStateFilter.filter(attachedUseCase.getValue())) {
+                useCases.add(attachedUseCase.getKey());
+            }
+        }
+        return useCases;
+    }
+
+    private interface AttachStateFilter {
+        boolean filter(UseCaseAttachInfo attachInfo);
+    }
+
+    /** The set of state and configuration information for an attached use case. */
+    private static final class UseCaseAttachInfo {
+        /** The configurations required of the camera for the use case. */
+        final SessionConfiguration sessionConfiguration;
+        /**
+         * True if the use case is currently online (i.e. camera should have a capture session
+         * configured for it).
+         */
+        boolean online = false;
+
+        /**
+         * True if the use case is currently active (i.e. camera should be issuing capture requests
+         * for it).
+         */
+        boolean active = false;
+
+        UseCaseAttachInfo(SessionConfiguration sessionConfiguration) {
+            this.sessionConfiguration = sessionConfiguration;
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/UseCaseConfiguration.java
new file mode 100644
index 0000000..454b1a9
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseConfiguration.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.SessionConfiguration.OptionUnpacker;
+
+/**
+ * Configuration containing options for use cases.
+ *
+ * @param <T> The use case being configured.
+ */
+public interface UseCaseConfiguration<T extends BaseUseCase> extends TargetConfiguration<T> {
+
+    /**
+     * Option: camerax.core.useCase.defaultSessionConfig
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<SessionConfiguration> OPTION_DEFAULT_SESSION_CONFIG =
+            Option.create("camerax.core.useCase.defaultSessionConfig", SessionConfiguration.class);
+    /**
+     * Option: camerax.core.useCase.configUnpacker
+     *
+     * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+     * dependencies.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<OptionUnpacker> OPTION_CONFIG_UNPACKER =
+            Option.create("camerax.core.useCase.configUnpacker", OptionUnpacker.class);
+    /**
+     * Option: camerax.core.useCase.surfaceOccypyPriority
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    Option<Integer> OPTION_SURFACE_OCCUPANCY_PRIORITY =
+            Option.create("camerax.core.useCase.surfaceOccupancyPriority", int.class);
+
+    /**
+     * Retrieves the default session configuration for this use case.
+     *
+     * <p>This configuration is used to initialize the use case's session configuration with default
+     * values.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    default SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration valueIfMissing) {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the default session configuration for this use case.
+     *
+     * <p>This configuration is used to initialize the use case's session configuration with default
+     * values.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default SessionConfiguration getDefaultSessionConfiguration() {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+    }
+
+    /**
+     * Retrieves the {@link SessionConfiguration.OptionUnpacker} for this use case.
+     *
+     * <p>This unpacker is used to initialize the use case's session configuration.
+     *
+     * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+     * dependencies.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    default SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+        return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the {@link SessionConfiguration.OptionUnpacker} for this use case.
+     *
+     * <p>This unpacker is used to initialize the use case's session configuration.
+     *
+     * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+     * dependencies.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+        return retrieveOption(OPTION_CONFIG_UNPACKER);
+    }
+
+    // Option Declarations:
+    // ***********************************************************************************************
+
+    /**
+     * Retrieves the surface occupancy priority of the target intending to use from this
+     * configuration.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default int getSurfaceOccupancyPriority(int valueIfMissing) {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the surface occupancy priority of the target intending to use from this
+     * configuration.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default int getSurfaceOccupancyPriority() {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+    }
+
+    /**
+     * Builder for a {@link UseCaseConfiguration}.
+     *
+     * @param <T> The type of the object being configured.
+     * @param <C> The top level configuration which will be generated by {@link #build()}.
+     * @param <B> The top level builder type for which this builder is composed with.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    interface Builder<T, C extends Configuration, B extends Builder<T, C, B>>
+            extends TargetConfiguration.Builder<T, C, B> {
+
+        /**
+         * Sets the default session configuration for this use case.
+         *
+         * @param sessionConfig The default session configuration to use for this use case.
+         * @return the current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        default B setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+            getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return builder();
+        }
+
+        /**
+         * Sets the Option Unpacker for translating this configuration into a {@link
+         * SessionConfiguration}
+         *
+         * <p>TODO(b/120949879): This may be removed when SessionConfig removes all camera2
+         * dependencies.
+         *
+         * @param optionUnpacker The option unpacker for to use for this use case.
+         * @return the current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        default B setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+            getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+            return builder();
+        }
+
+        /**
+         * Sets the surface occupancy priority of the intended target from this configuration.
+         *
+         * <p>The stream resource of {@link android.hardware.camera2.CameraDevice} is limited. When
+         * one use case occupies a larger stream resource, it will impact the other use cases to get
+         * smaller stream resource. Use this to determine which use case can have higher priority to
+         * occupancy stream resource first.
+         *
+         * @param priority The priority to occupancy the available stream resource. Higher value
+         *                 will have higher priority.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        default B setSurfaceOccupancyPriority(int priority) {
+            getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return builder();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseConfigurationFactory.java b/camera/core/src/main/java/androidx/camera/core/UseCaseConfigurationFactory.java
new file mode 100644
index 0000000..c9e5e58
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseConfigurationFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * A Repository for generating use case configurations.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface UseCaseConfigurationFactory {
+
+    /**
+     * Returns the configuration for the given type, or <code>null</code> if the configuration
+     * cannot be produced.
+     */
+    @Nullable
+    <C extends UseCaseConfiguration<?>> C getConfiguration(Class<C> configType);
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/core/src/main/java/androidx/camera/core/UseCaseGroup.java
new file mode 100644
index 0000000..eef55e8
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A collection of {@link BaseUseCase}.
+ *
+ * <p>The group of {@link BaseUseCase} instances have synchronized interactions with the {@link
+ * BaseCamera}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class UseCaseGroup {
+    private static final String TAG = "UseCaseGroup";
+
+    /**
+     * The lock for the single {@link StateChangeListener} held by the group.
+     *
+     * <p>This lock is always acquired prior to acquiring the useCasesLock so that there is no
+     * lock-ordering deadlock.
+     */
+    private final Object listenerLock = new Object();
+    /**
+     * The lock for accessing the map of use case types to use case instances.
+     *
+     * <p>This lock is always acquired after acquiring the listenerLock so that there is no
+     * lock-ordering deadlock.
+     */
+    private final Object useCasesLock = new Object();
+    @GuardedBy("useCasesLock")
+    private final Set<BaseUseCase> useCases = new HashSet<>();
+    @GuardedBy("listenerLock")
+    private StateChangeListener listener;
+
+    /** Starts all the use cases so that they are brought into an online state. */
+    void start() {
+        synchronized (listenerLock) {
+            if (listener != null) {
+                listener.onGroupActive(this);
+            }
+        }
+    }
+
+    /** Stops all the use cases so that they are brought into an offline state. */
+    void stop() {
+        synchronized (listenerLock) {
+            if (listener != null) {
+                listener.onGroupInactive(this);
+            }
+        }
+    }
+
+    void setListener(StateChangeListener listener) {
+        synchronized (listenerLock) {
+            this.listener = listener;
+        }
+    }
+
+    /**
+     * Adds the {@link BaseUseCase} to the group.
+     *
+     * @return true if the use case is added, or false if the use case already exists in the group.
+     */
+    public boolean addUseCase(BaseUseCase useCase) {
+        synchronized (useCasesLock) {
+            return useCases.add(useCase);
+        }
+    }
+
+    /** Returns true if the {@link BaseUseCase} is contained in the group. */
+    boolean contains(BaseUseCase useCase) {
+        synchronized (useCasesLock) {
+            return useCases.contains(useCase);
+        }
+    }
+
+    /**
+     * Removes the {@link BaseUseCase} from the group.
+     *
+     * @return Returns true if the use case is removed. Otherwise returns false (if the use case did
+     * not exist in the group).
+     */
+    boolean removeUseCase(BaseUseCase useCase) {
+        synchronized (useCasesLock) {
+            return useCases.remove(useCase);
+        }
+    }
+
+    /** Clears all use cases from this group. */
+    public void clear() {
+        List<BaseUseCase> useCasesToClear = new ArrayList<>();
+        synchronized (useCasesLock) {
+            useCasesToClear.addAll(useCases);
+            useCases.clear();
+        }
+        for (BaseUseCase useCase : useCasesToClear) {
+            Log.d(TAG, "Clearing use case: " + useCase.getName());
+            useCase.clear();
+        }
+    }
+
+    /** Returns the collection of all the use cases currently contained by the UseCaseGroup. */
+    Collection<BaseUseCase> getUseCases() {
+        synchronized (useCasesLock) {
+            return Collections.unmodifiableCollection(useCases);
+        }
+    }
+
+    Map<String, Set<BaseUseCase>> getCameraIdToUseCaseMap() {
+        Map<String, Set<BaseUseCase>> cameraIdToUseCases = new HashMap<>();
+        synchronized (useCasesLock) {
+            for (BaseUseCase useCase : useCases) {
+                for (String cameraId : useCase.getAttachedCameraIds()) {
+                    Set<BaseUseCase> useCaseSet = cameraIdToUseCases.get(cameraId);
+                    if (useCaseSet == null) {
+                        useCaseSet = new HashSet<>();
+                    }
+                    useCaseSet.add(useCase);
+                    cameraIdToUseCases.put(cameraId, useCaseSet);
+                }
+            }
+        }
+        return Collections.unmodifiableMap(cameraIdToUseCases);
+    }
+
+    /** Listener called when a {@link UseCaseGroup} transitions between active/inactive states. */
+    interface StateChangeListener {
+        /**
+         * Called when a {@link UseCaseGroup} becomes active.
+         *
+         * <p>When a UseCaseGroup is active then all the contained {@link BaseUseCase} become
+         * online. This means that the {@link BaseCamera} should transition to a state as close as
+         * possible to producing, but prior to actually producing data for the use case.
+         */
+        void onGroupActive(UseCaseGroup useCaseGroup);
+
+        /**
+         * Called when a {@link UseCaseGroup} becomes inactive.
+         *
+         * <p>When a UseCaseGroup is active then all the contained {@link BaseUseCase} become
+         * offline.
+         */
+        void onGroupInactive(UseCaseGroup useCaseGroup);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseGroupLifecycleController.java b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupLifecycleController.java
new file mode 100644
index 0000000..3a83213
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupLifecycleController.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.GuardedBy;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleOwner;
+
+/** A {@link UseCaseGroup} whose starting and stopping is controlled by a {@link Lifecycle}. */
+final class UseCaseGroupLifecycleController implements DefaultLifecycleObserver {
+    private final Object useCaseGroupLock = new Object();
+
+    @GuardedBy("useCaseGroupLock")
+    private final UseCaseGroup useCaseGroup;
+
+    /** The lifecycle that controls the useCaseGroup. */
+    private final Lifecycle lifecycle;
+
+    /** Creates a new {@link UseCaseGroup} which gets controlled by lifecycle transitions. */
+    UseCaseGroupLifecycleController(Lifecycle lifecycle) {
+        this(lifecycle, new UseCaseGroup());
+    }
+
+    /** Wraps an existing {@link UseCaseGroup} so it is controlled by lifecycle transitions. */
+    UseCaseGroupLifecycleController(Lifecycle lifecycle, UseCaseGroup useCaseGroup) {
+        this.useCaseGroup = useCaseGroup;
+        this.lifecycle = lifecycle;
+        lifecycle.addObserver(this);
+    }
+
+    @Override
+    public void onStart(LifecycleOwner lifecycleOwner) {
+        synchronized (useCaseGroupLock) {
+            useCaseGroup.start();
+        }
+    }
+
+    @Override
+    public void onStop(LifecycleOwner lifecycleOwner) {
+        synchronized (useCaseGroupLock) {
+            useCaseGroup.stop();
+        }
+    }
+
+    @Override
+    public void onDestroy(LifecycleOwner lifecycleOwner) {
+        synchronized (useCaseGroupLock) {
+            useCaseGroup.clear();
+        }
+    }
+
+    /**
+     * Starts the underlying {@link UseCaseGroup} so that its {@link
+     * UseCaseGroup.StateChangeListener} can be notified.
+     *
+     * <p>This is required when the contained {@link Lifecycle} is in a STARTED state, since the
+     * default state for a {@link UseCaseGroup} is inactive. The explicit call forces a check on the
+     * actual state of the group.
+     */
+    void notifyState() {
+        synchronized (useCaseGroupLock) {
+            if (lifecycle.getCurrentState().isAtLeast(State.STARTED)) {
+                useCaseGroup.start();
+            }
+            for (BaseUseCase useCase : useCaseGroup.getUseCases()) {
+                useCase.notifyState();
+            }
+        }
+    }
+
+    UseCaseGroup getUseCaseGroup() {
+        synchronized (useCaseGroupLock) {
+            return useCaseGroup;
+        }
+    }
+
+    /**
+     * Stops observing lifecycle changes.
+     *
+     * <p>Once released the wrapped {@link UseCaseGroup} is still valid, but will no longer be
+     * triggered by lifecycle state transitions. In order to observe lifecycle changes again a new
+     * {@link UseCaseGroupLifecycleController} instance should be created.
+     *
+     * <p>Calls subsequent to the first time will do nothing.
+     */
+    void release() {
+        lifecycle.removeObserver(this);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/UseCaseGroupRepository.java b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupRepository.java
new file mode 100644
index 0000000..c945fff
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupRepository.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A repository of {@link UseCaseGroupLifecycleController} instances.
+ *
+ * <p>Each {@link UseCaseGroupLifecycleController} is associated with a {@link LifecycleOwner} that
+ * regulates the common lifecycle shared by all the use cases in the group.
+ */
+final class UseCaseGroupRepository {
+    final Object useCasesLock = new Object();
+
+    @GuardedBy("useCasesLock")
+    final Map<LifecycleOwner, UseCaseGroupLifecycleController> useCasesMap =
+            new HashMap<>();
+
+    /**
+     * Gets an existing {@link UseCaseGroupLifecycleController} associated with the given {@link
+     * LifecycleOwner}, or creates a new {@link UseCaseGroupLifecycleController} if a group does not
+     * already exist.
+     *
+     * <p>The {@link UseCaseGroupLifecycleController} is set to be an observer of the {@link
+     * LifecycleOwner}.
+     *
+     * @param lifecycleOwner to associate with the group
+     */
+    UseCaseGroupLifecycleController getOrCreateUseCaseGroup(LifecycleOwner lifecycleOwner) {
+        return getOrCreateUseCaseGroup(lifecycleOwner, useCaseGroup -> {
+        });
+    }
+
+    /**
+     * Gets an existing {@link UseCaseGroupLifecycleController} associated with the given {@link
+     * LifecycleOwner}, or creates a new {@link UseCaseGroupLifecycleController} if a group does not
+     * already exist.
+     *
+     * <p>The {@link UseCaseGroupLifecycleController} is set to be an observer of the {@link
+     * LifecycleOwner}.
+     *
+     * @param lifecycleOwner to associate with the group
+     * @param groupSetup     additional setup to do on the group if a new instance is created
+     */
+    UseCaseGroupLifecycleController getOrCreateUseCaseGroup(
+            LifecycleOwner lifecycleOwner, UseCaseGroupSetup groupSetup) {
+        UseCaseGroupLifecycleController useCaseGroupLifecycleController;
+        synchronized (useCasesLock) {
+            useCaseGroupLifecycleController = useCasesMap.get(lifecycleOwner);
+            if (useCaseGroupLifecycleController == null) {
+                useCaseGroupLifecycleController = createUseCaseGroup(lifecycleOwner);
+                groupSetup.setup(useCaseGroupLifecycleController.getUseCaseGroup());
+            }
+        }
+        return useCaseGroupLifecycleController;
+    }
+
+    /**
+     * Creates a new {@link UseCaseGroupLifecycleController} associated with the given {@link
+     * LifecycleOwner} and adds the group to the repository.
+     *
+     * <p>The {@link UseCaseGroupLifecycleController} is set to be an observer of the {@link
+     * LifecycleOwner}.
+     *
+     * @param lifecycleOwner to associate with the group
+     * @return a new {@link UseCaseGroupLifecycleController}
+     * @throws IllegalArgumentException if the {@link android.arch.lifecycle.Lifecycle} of
+     *                                  lifecycleOwner is already
+     *                                  {@link android.arch.lifecycle.Lifecycle.State.DESTROYED}.
+     */
+    private UseCaseGroupLifecycleController createUseCaseGroup(LifecycleOwner lifecycleOwner) {
+        if (lifecycleOwner.getLifecycle().getCurrentState() == State.DESTROYED) {
+            throw new IllegalArgumentException(
+                    "Trying to create use case group with destroyed lifecycle.");
+        }
+
+        UseCaseGroupLifecycleController useCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(lifecycleOwner.getLifecycle());
+        lifecycleOwner.getLifecycle().addObserver(createRemoveOnDestroyObserver());
+        synchronized (useCasesLock) {
+            useCasesMap.put(lifecycleOwner, useCaseGroupLifecycleController);
+        }
+        return useCaseGroupLifecycleController;
+    }
+
+    /**
+     * Creates a {@link DefaultLifecycleObserver} which removes any {@link
+     * UseCaseGroupLifecycleController} associated with a {@link LifecycleOwner} from this
+     * repository when that lifecycle is destroyed.
+     *
+     * @return a new {@link DefaultLifecycleObserver}
+     */
+    private DefaultLifecycleObserver createRemoveOnDestroyObserver() {
+        return new DefaultLifecycleObserver() {
+            @Override
+            public void onDestroy(LifecycleOwner lifecycleOwner) {
+                synchronized (useCasesLock) {
+                    useCasesMap.remove(lifecycleOwner);
+                }
+                lifecycleOwner.getLifecycle().removeObserver(this);
+            }
+        };
+    }
+
+    Collection<UseCaseGroupLifecycleController> getUseCaseGroups() {
+        synchronized (useCasesLock) {
+            return Collections.unmodifiableCollection(useCasesMap.values());
+        }
+    }
+
+    @VisibleForTesting
+    Map<LifecycleOwner, UseCaseGroupLifecycleController> getUseCasesMap() {
+        synchronized (useCasesLock) {
+            return useCasesMap;
+        }
+    }
+
+    /**
+     * The interface for doing additional setup work on a newly created {@link UseCaseGroup}
+     * instance.
+     */
+    public interface UseCaseGroupSetup {
+        void setup(UseCaseGroup useCaseGroup);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCase.java b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCase.java
new file mode 100644
index 0000000..7f39ce4
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCase.java
@@ -0,0 +1,900 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.location.Location;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.CamcorderProfile;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaFormat;
+import android.media.MediaMuxer;
+import android.media.MediaRecorder.AudioSource;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A use case for taking a video.
+ *
+ * <p>This class is designed for simple video capturing. It gives basic configuration of the
+ * recorded video such as resolution and file format.
+ */
+public class VideoCaptureUseCase extends BaseUseCase {
+
+    /**
+     * Provides a static configuration with implementation-agnostic options.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final Defaults DEFAULT_CONFIG = new Defaults();
+    private static final Metadata EMPTY_METADATA = new Metadata();
+    private static final String TAG = "VideoCaptureUseCase";
+    /** Amount of time to wait for dequeuing a buffer from the videoEncoder. */
+    private static final int DEQUE_TIMEOUT_USEC = 10000;
+    /** Android preferred mime type for AVC video. */
+    private static final String VIDEO_MIME_TYPE = "video/avc";
+    private static final String AUDIO_MIME_TYPE = "audio/mp4a-latm";
+    /** Camcorder profiles quality list */
+    private static final int[] CamcorderQuality = {
+            CamcorderProfile.QUALITY_2160P,
+            CamcorderProfile.QUALITY_1080P,
+            CamcorderProfile.QUALITY_720P,
+            CamcorderProfile.QUALITY_480P
+    };
+    /**
+     * Audio encoding
+     *
+     * <p>the result of PCM_8BIT and PCM_FLOAT are not good. Set PCM_16BIT as the first option.
+     */
+    private static final short[] audioEncoding = {
+            AudioFormat.ENCODING_PCM_16BIT,
+            AudioFormat.ENCODING_PCM_8BIT,
+            AudioFormat.ENCODING_PCM_FLOAT
+    };
+    private final BufferInfo videoBufferInfo = new BufferInfo();
+    private final Object muxerLock = new Object();
+    /** Thread on which all encoding occurs. */
+    private final HandlerThread videoHandlerThread =
+            new HandlerThread(CameraXThreads.TAG + "video encoding thread");
+    private final Handler videoHandler;
+    /** Thread on which audio encoding occurs. */
+    private final HandlerThread audioHandlerThread =
+            new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
+    private final Handler audioHandler;
+    private final AtomicBoolean endOfVideoStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean endOfAudioStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean endOfAudioVideoSignal = new AtomicBoolean(true);
+    private final BufferInfo audioBufferInfo = new BufferInfo();
+    /** For record the first sample written time. */
+    private final AtomicBoolean isFirstVideoSampleWrite = new AtomicBoolean(false);
+    private final AtomicBoolean isFirstAudioSampleWrite = new AtomicBoolean(false);
+    private final VideoCaptureUseCaseConfiguration.Builder useCaseConfigBuilder;
+    @NonNull
+    private MediaCodec videoEncoder;
+    @NonNull
+    private MediaCodec audioEncoder;
+    /** The muxer that writes the encoding data to file. */
+    @GuardedBy("muxerLock")
+    private MediaMuxer muxer;
+    private boolean muxerStarted = false;
+    /** The index of the video track used by the muxer. */
+    private int videoTrackIndex;
+    /** The index of the audio track used by the muxer. */
+    private int audioTrackIndex;
+    /** Surface the camera writes to, which the videoEncoder uses as input. */
+    private Surface cameraSurface;
+    /** audio raw data */
+    @NonNull
+    private AudioRecord audioRecorder;
+    private int audioBufferSize;
+    private boolean isRecording = false;
+    private int audioChannelCount;
+    private int audioSampleRate;
+    private int audioBitRate;
+
+    /**
+     * Creates a new video capture use case from the given configuration.
+     *
+     * @param configuration for this use case instance
+     */
+    public VideoCaptureUseCase(VideoCaptureUseCaseConfiguration configuration) {
+        super(configuration);
+        useCaseConfigBuilder = VideoCaptureUseCaseConfiguration.Builder.fromConfig(configuration);
+
+        // video thread start
+        videoHandlerThread.start();
+        videoHandler = new Handler(videoHandlerThread.getLooper());
+
+        // audio thread start
+        audioHandlerThread.start();
+        audioHandler = new Handler(audioHandlerThread.getLooper());
+    }
+
+    /** Creates a {@link MediaFormat} using parameters from the configuration */
+    private static MediaFormat createMediaFormat(
+            VideoCaptureUseCaseConfiguration configuration, Size resolution) {
+        MediaFormat format =
+                MediaFormat.createVideoFormat(
+                        VIDEO_MIME_TYPE, resolution.getWidth(), resolution.getHeight());
+        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.getBitRate());
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, configuration.getVideoFrameRate());
+        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, configuration.getIFrameInterval());
+
+        return format;
+    }
+
+    private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+        try {
+            return CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to get camera id for camera lens facing " + lensFacing, e);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @Nullable
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        VideoCaptureUseCaseConfiguration defaults =
+                CameraX.getDefaultUseCaseConfiguration(VideoCaptureUseCaseConfiguration.class);
+        if (defaults != null) {
+            return VideoCaptureUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        VideoCaptureUseCaseConfiguration configuration =
+                (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+        if (cameraSurface != null) {
+            videoEncoder.stop();
+            videoEncoder.release();
+            audioEncoder.stop();
+            audioEncoder.release();
+            cameraSurface.release();
+        }
+
+        try {
+            videoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
+            audioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
+        } catch (IOException e) {
+            throw new IllegalStateException("Unable to create MediaCodec due to: " + e.getCause());
+        }
+
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+        Size resolution = suggestedResolutionMap.get(cameraId);
+        if (resolution == null) {
+            throw new IllegalArgumentException(
+                    "Suggested resolution map missing resolution for camera " + cameraId);
+        }
+
+        setupEncoder(resolution);
+        return suggestedResolutionMap;
+    }
+
+    /**
+     * Starts recording video, which continues until {@link VideoCaptureUseCase#stopRecording()} is
+     * called.
+     *
+     * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
+     * {@link OnVideoSavedListener#onError(UseCaseError, String, Throwable)}.
+     *
+     * @param saveLocation Location to save the video capture
+     * @param listener     Listener to call for the recorded video
+     */
+    public void startRecording(File saveLocation, OnVideoSavedListener listener) {
+        isFirstVideoSampleWrite.set(false);
+        isFirstAudioSampleWrite.set(false);
+        startRecording(saveLocation, listener, EMPTY_METADATA);
+    }
+
+    /**
+     * Starts recording video, which continues until {@link VideoCaptureUseCase#stopRecording()} is
+     * called.
+     *
+     * <p>StartRecording() is asynchronous. User needs to check if any error occurs by setting the
+     * {@link OnVideoSavedListener#onError(UseCaseError, String, Throwable)}.
+     *
+     * @param saveLocation Location to save the video capture
+     * @param listener     Listener to call for the recorded video
+     * @param metadata     Metadata to save with the recorded video
+     */
+    public void startRecording(
+            File saveLocation, OnVideoSavedListener listener, Metadata metadata) {
+        Log.i(TAG, "startRecording");
+
+        if (!endOfAudioVideoSignal.get()) {
+            listener.onError(
+                    UseCaseError.RECORDING_IN_PROGRESS, "It is still in video recording!", null);
+            return;
+        }
+
+        try {
+            // audioRecord start
+            audioRecorder.startRecording();
+        } catch (IllegalStateException e) {
+            listener.onError(UseCaseError.ENCODER_ERROR, "AudioRecorder start fail", e);
+            return;
+        }
+
+        String cameraId =
+                getCameraIdUnchecked(
+                        ((CameraDeviceConfiguration) getUseCaseConfiguration()).getLensFacing());
+        try {
+            // video encoder start
+            Log.i(TAG, "videoEncoder start");
+            videoEncoder.start();
+            // audio encoder start
+            Log.i(TAG, "audioEncoder start");
+            audioEncoder.start();
+
+        } catch (IllegalStateException e) {
+            setupEncoder(getAttachedSurfaceResolution(cameraId));
+            listener.onError(UseCaseError.ENCODER_ERROR, "Audio/Video encoder start fail", e);
+            return;
+        }
+
+        // Get the relative rotation or default to 0 if the camera info is unavailable
+        int relativeRotation = 0;
+        try {
+            CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+            relativeRotation =
+                    cameraInfo.getSensorRotationDegrees(
+                            ((ImageOutputConfiguration) getUseCaseConfiguration())
+                                    .getTargetRotation(Surface.ROTATION_0));
+        } catch (CameraInfoUnavailableException e) {
+            Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
+        }
+
+        try {
+            synchronized (muxerLock) {
+                muxer =
+                        new MediaMuxer(
+                                saveLocation.getAbsolutePath(),
+                                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+                muxer.setOrientationHint(relativeRotation);
+                if (metadata.location != null) {
+                    muxer.setLocation(
+                            (float) metadata.location.getLatitude(),
+                            (float) metadata.location.getLongitude());
+                }
+            }
+        } catch (IOException e) {
+            setupEncoder(getAttachedSurfaceResolution(cameraId));
+            listener.onError(UseCaseError.MUXER_ERROR, "MediaMuxer creation failed!", e);
+            return;
+        }
+
+        endOfVideoStreamSignal.set(false);
+        endOfAudioStreamSignal.set(false);
+        endOfAudioVideoSignal.set(false);
+        isRecording = true;
+
+        notifyActive();
+        audioHandler.post(
+                () -> {
+                    audioEncode(listener);
+                });
+
+        videoHandler.post(
+                () -> {
+                    boolean errorOccurred = videoEncode(listener);
+                    if (!errorOccurred) {
+                        listener.onVideoSaved(saveLocation);
+                    }
+                });
+    }
+
+    /**
+     * Stops recording video, this must be called after {@link
+     * VideoCaptureUseCase#startRecording(File, OnVideoSavedListener, Metadata)} is called.
+     *
+     * <p>stopRecording() is asynchronous API. User need to check if {@link
+     * OnVideoSavedListener#onVideoSaved(File)} or {@link OnVideoSavedListener#onError(UseCaseError,
+     * String, Throwable)} be called before startRecording.
+     */
+    public void stopRecording() {
+        Log.i(TAG, "stopRecording");
+        notifyInactive();
+        if (!endOfAudioVideoSignal.get() && isRecording) {
+            // stop audio encoder thread, and wait video encoder and muxer stop.
+            endOfAudioStreamSignal.set(true);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void clear() {
+        videoHandlerThread.quitSafely();
+
+        if (videoEncoder != null) {
+            videoEncoder.release();
+            videoEncoder = null;
+        }
+
+        // audio encoder release
+        audioHandlerThread.quitSafely();
+        if (audioEncoder != null) {
+            audioEncoder.release();
+            audioEncoder = null;
+        }
+
+        if (audioRecorder != null) {
+            audioRecorder.release();
+            audioRecorder = null;
+        }
+
+        if (cameraSurface != null) {
+            cameraSurface.release();
+            cameraSurface = null;
+        }
+        super.clear();
+    }
+
+    /**
+     * Sets the desired rotation of the output video.
+     *
+     * <p>In most cases this should be set to the current rotation returned by {@link
+     * Display#getRotation()}.
+     *
+     * @param rotation Desired rotation of the output video.
+     */
+    public void setTargetRotation(@RotationValue int rotation) {
+        ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+        int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+        if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+            useCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(useCaseConfigBuilder.build());
+
+            // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+        }
+    }
+
+    /**
+     * Setup the {@link MediaCodec} for encoding video from a camera {@link Surface} and encoding
+     * audio from selected audio source.
+     */
+    private void setupEncoder(Size resolution) {
+        VideoCaptureUseCaseConfiguration configuration =
+                (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+
+        // video encoder setup
+        videoEncoder.reset();
+        videoEncoder.configure(
+                createMediaFormat(configuration, resolution), /*surface*/
+                null, /*crypto*/
+                null,
+                MediaCodec.CONFIGURE_FLAG_ENCODE);
+        if (cameraSurface != null) {
+            cameraSurface.release();
+        }
+        cameraSurface = videoEncoder.createInputSurface();
+
+        SessionConfiguration.Builder builder =
+                SessionConfiguration.Builder.createFrom(configuration);
+        builder.addSurface(new ImmediateSurface(cameraSurface));
+
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+        attachToCamera(cameraId, builder.build());
+
+        // audio encoder setup
+        setAudioParametersByCamcorderProfile(resolution, cameraId);
+        audioEncoder.reset();
+        audioEncoder.configure(
+                createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+        if (audioRecorder != null) {
+            audioRecorder.release();
+        }
+        audioRecorder = autoConfigAudioRecordSource(configuration);
+        // check audioRecorder
+        if (audioRecorder == null) {
+            Log.e(TAG, "AudioRecord object cannot initialized correctly!");
+        }
+
+        videoTrackIndex = -1;
+        audioTrackIndex = -1;
+        isRecording = false;
+    }
+
+    /**
+     * Write a buffer that has been encoded to file.
+     *
+     * @param bufferIndex the index of the buffer in the videoEncoder that has available data
+     * @return returns true if this buffer is the end of the stream
+     */
+    private boolean writeVideoEncodedBuffer(int bufferIndex) {
+        if (bufferIndex < 0) {
+            Log.e(TAG, "Output buffer should not have negative index: " + bufferIndex);
+            return false;
+        }
+        // Get data from buffer
+        ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(bufferIndex);
+
+        // Check if buffer is valid, if not then return
+        if (outputBuffer == null) {
+            Log.d(TAG, "OutputBuffer was null.");
+            return false;
+        }
+
+        // Write data to muxer if available
+        if (audioTrackIndex >= 0 && videoTrackIndex >= 0 && videoBufferInfo.size > 0) {
+            outputBuffer.position(videoBufferInfo.offset);
+            outputBuffer.limit(videoBufferInfo.offset + videoBufferInfo.size);
+            videoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
+
+            synchronized (muxerLock) {
+                if (!isFirstVideoSampleWrite.get()) {
+                    Log.i(TAG, "First video sample written.");
+                    isFirstVideoSampleWrite.set(true);
+                }
+                muxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferInfo);
+            }
+        }
+
+        // Release data
+        videoEncoder.releaseOutputBuffer(bufferIndex, false);
+
+        // Return true if EOS is set
+        return (videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+    }
+
+    private boolean writeAudioEncodedBuffer(int bufferIndex) {
+        ByteBuffer buffer = getOutputBuffer(audioEncoder, bufferIndex);
+        buffer.position(audioBufferInfo.offset);
+        if (audioTrackIndex >= 0
+                && videoTrackIndex >= 0
+                && audioBufferInfo.size > 0
+                && audioBufferInfo.presentationTimeUs > 0) {
+            try {
+                synchronized (muxerLock) {
+                    if (!isFirstAudioSampleWrite.get()) {
+                        Log.i(TAG, "First audio sample written.");
+                        isFirstAudioSampleWrite.set(true);
+                    }
+                    muxer.writeSampleData(audioTrackIndex, buffer, audioBufferInfo);
+                }
+            } catch (Exception e) {
+                Log.e(
+                        TAG,
+                        "audio error:size="
+                                + audioBufferInfo.size
+                                + "/offset="
+                                + audioBufferInfo.offset
+                                + "/timeUs="
+                                + audioBufferInfo.presentationTimeUs);
+                e.printStackTrace();
+            }
+        }
+        audioEncoder.releaseOutputBuffer(bufferIndex, false);
+        return (audioBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+    }
+
+    /**
+     * Encoding which runs indefinitely until end of stream is signaled. This should not run on the
+     * main thread otherwise it will cause the application to block.
+     *
+     * @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
+     */
+    private boolean videoEncode(OnVideoSavedListener videoSavedListener) {
+        VideoCaptureUseCaseConfiguration configuration =
+                (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+        // Main encoding loop. Exits on end of stream.
+        boolean errorOccurred = false;
+        boolean videoEos = false;
+        while (!videoEos && !errorOccurred) {
+            // Check for end of stream from main thread
+            if (endOfVideoStreamSignal.get()) {
+                videoEncoder.signalEndOfInputStream();
+                endOfVideoStreamSignal.set(false);
+            }
+
+            // Deque buffer to check for processing step
+            int outputBufferId =
+                    videoEncoder.dequeueOutputBuffer(videoBufferInfo, DEQUE_TIMEOUT_USEC);
+            switch (outputBufferId) {
+                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                    if (muxerStarted) {
+                        videoSavedListener.onError(
+                                UseCaseError.ENCODER_ERROR,
+                                "Unexpected change in video encoding format.",
+                                null);
+                        errorOccurred = true;
+                    }
+
+                    synchronized (muxerLock) {
+                        videoTrackIndex = muxer.addTrack(videoEncoder.getOutputFormat());
+                        if (audioTrackIndex >= 0 && videoTrackIndex >= 0) {
+                            muxerStarted = true;
+                            Log.i(TAG, "media muxer start");
+                            muxer.start();
+                        }
+                    }
+                    break;
+                case MediaCodec.INFO_TRY_AGAIN_LATER:
+                    // Timed out. Just wait until next attempt to deque.
+                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
+                    // Ignore output buffers changed since we dequeue a single buffer instead of
+                    // multiple
+                    break;
+                default:
+                    videoEos = writeVideoEncodedBuffer(outputBufferId);
+            }
+        }
+
+        try {
+            Log.i(TAG, "videoEncoder stop");
+            videoEncoder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedListener.onError(UseCaseError.ENCODER_ERROR, "Video encoder stop failed!", e);
+            errorOccurred = true;
+        }
+
+        try {
+            // new MediaMuxer instance required for each new file written, and release current one.
+            synchronized (muxerLock) {
+                if (muxer != null) {
+                    if (muxerStarted) {
+                        muxer.stop();
+                    }
+                    muxer.release();
+                    muxer = null;
+                }
+            }
+        } catch (IllegalStateException e) {
+            videoSavedListener.onError(UseCaseError.MUXER_ERROR, "Muxer stop failed!", e);
+            errorOccurred = true;
+        }
+
+        muxerStarted = false;
+        // Do the setup of the videoEncoder at the end of video recording instead of at the start of
+        // recording because it requires attaching a new Surface. This causes a glitch so we don't
+        // want
+        // that to incur latency at the start of capture.
+        setupEncoder(
+                getAttachedSurfaceResolution(getCameraIdUnchecked(configuration.getLensFacing())));
+        notifyReset();
+
+        // notify the UI thread that the video recording has finished
+        endOfAudioVideoSignal.set(true);
+
+        Log.i(TAG, "Video encode thread end.");
+        return errorOccurred;
+    }
+
+    private boolean audioEncode(OnVideoSavedListener videoSavedListener) {
+        // Audio encoding loop. Exits on end of stream.
+        boolean audioEos = false;
+        int outIndex;
+        while (!audioEos && isRecording) {
+            // Check for end of stream from main thread
+            if (endOfAudioStreamSignal.get()) {
+                endOfAudioStreamSignal.set(false);
+                isRecording = false;
+            }
+
+            // get audio deque input buffer
+            if (audioEncoder != null && audioRecorder != null) {
+                int index = audioEncoder.dequeueInputBuffer(-1);
+                if (index >= 0) {
+                    final ByteBuffer buffer = getInputBuffer(audioEncoder, index);
+                    buffer.clear();
+                    int length = audioRecorder.read(buffer, audioBufferSize);
+                    if (length > 0) {
+                        audioEncoder.queueInputBuffer(
+                                index,
+                                0,
+                                length,
+                                (System.nanoTime() / 1000),
+                                isRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+                    }
+                }
+
+                // start to dequeue audio output buffer
+                do {
+                    outIndex = audioEncoder.dequeueOutputBuffer(audioBufferInfo, 0);
+                    switch (outIndex) {
+                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                            synchronized (muxerLock) {
+                                audioTrackIndex = muxer.addTrack(audioEncoder.getOutputFormat());
+                                if (audioTrackIndex >= 0 && videoTrackIndex >= 0) {
+                                    muxerStarted = true;
+                                    muxer.start();
+                                }
+                            }
+                            break;
+                        case MediaCodec.INFO_TRY_AGAIN_LATER:
+                            break;
+                        default:
+                            audioEos = writeAudioEncodedBuffer(outIndex);
+                    }
+                } while (outIndex >= 0 && !audioEos); // end of dequeue output buffer
+            }
+        } // end of while loop
+
+        // Audio Stop
+        try {
+            Log.i(TAG, "audioRecorder stop");
+            audioRecorder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedListener.onError(
+                    UseCaseError.ENCODER_ERROR, "Audio recorder stop failed!", e);
+        }
+
+        try {
+            audioEncoder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedListener.onError(UseCaseError.ENCODER_ERROR, "Audio encoder stop failed!", e);
+        }
+
+        Log.i(TAG, "Audio encode thread end");
+        // Use AtomicBoolean to signal because MediaCodec.signalEndOfInputStream() is not thread
+        // safe
+        endOfVideoStreamSignal.set(true);
+
+        return false;
+    }
+
+    private ByteBuffer getInputBuffer(MediaCodec codec, int index) {
+        return codec.getInputBuffer(index);
+    }
+
+    private ByteBuffer getOutputBuffer(MediaCodec codec, int index) {
+        return codec.getOutputBuffer(index);
+    }
+
+    /** Creates a {@link MediaFormat} using parameters for audio from the configuration */
+    private MediaFormat createAudioMediaFormat() {
+        MediaFormat format =
+                MediaFormat.createAudioFormat(AUDIO_MIME_TYPE, audioSampleRate, audioChannelCount);
+        format.setInteger(
+                MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, audioBitRate);
+
+        return format;
+    }
+
+    /** Create a AudioRecord object to get raw data */
+    private AudioRecord autoConfigAudioRecordSource(
+            VideoCaptureUseCaseConfiguration configuration) {
+        for (short audioFormat : audioEncoding) {
+
+            // Use channel count to determine stereo vs mono
+            int channelConfig =
+                    audioChannelCount == 1
+                            ? AudioFormat.CHANNEL_IN_MONO
+                            : AudioFormat.CHANNEL_IN_STEREO;
+            int source = configuration.getAudioRecordSource();
+
+            try {
+                int bufferSize =
+                        AudioRecord.getMinBufferSize(audioSampleRate, channelConfig, audioFormat);
+
+                if (bufferSize <= 0) {
+                    bufferSize = configuration.getAudioMinBufferSize();
+                }
+
+                AudioRecord recorder =
+                        new AudioRecord(
+                                source,
+                                audioSampleRate,
+                                channelConfig,
+                                audioFormat,
+                                bufferSize * 2);
+
+                if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
+                    audioBufferSize = bufferSize;
+                    Log.i(
+                            TAG,
+                            "source: "
+                                    + source
+                                    + " audioSampleRate: "
+                                    + audioSampleRate
+                                    + " channelConfig: "
+                                    + channelConfig
+                                    + " audioFormat: "
+                                    + audioFormat
+                                    + " bufferSize: "
+                                    + bufferSize);
+                    return recorder;
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Exception, keep trying.", e);
+            }
+        }
+
+        return null;
+    }
+
+    /** Set audio record parameters by CamcorderProfile */
+    private void setAudioParametersByCamcorderProfile(Size currentResolution, String cameraId) {
+        CamcorderProfile profile;
+        boolean isCamcorderProfileFound = false;
+
+        for (int quality : CamcorderQuality) {
+            if (CamcorderProfile.hasProfile(Integer.parseInt(cameraId), quality)) {
+                profile = CamcorderProfile.get(Integer.parseInt(cameraId), quality);
+                if (currentResolution.getWidth() == profile.videoFrameWidth
+                        && currentResolution.getHeight() == profile.videoFrameHeight) {
+                    audioChannelCount = profile.audioChannels;
+                    audioSampleRate = profile.audioSampleRate;
+                    audioBitRate = profile.audioBitRate;
+                    isCamcorderProfileFound = true;
+                    break;
+                }
+            }
+        }
+
+        // In case no corresponding camcorder profile can be founded, * get default value from
+        // VideoCaptureUseCaseConfiguration.
+        if (!isCamcorderProfileFound) {
+            VideoCaptureUseCaseConfiguration config =
+                    (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+            audioChannelCount = config.getAudioChannelCount();
+            audioSampleRate = config.getAudioSampleRate();
+            audioBitRate = config.getAudioBitRate();
+        }
+    }
+
+    /**
+     * Describes the error that occurred during video capture operations.
+     *
+     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
+     * VideoCaptureUseCase.OnVideoSavedListener.onError}.
+     *
+     * <p>See message parameter in onError callback or log for more details.
+     */
+    public enum UseCaseError {
+        /**
+         * An unknown error occurred.
+         *
+         * <p>See message parameter in onError callback or log for more details.
+         */
+        UNKNOWN_ERROR,
+        /**
+         * An error occurred with encoder state, either when trying to change state or when an
+         * unexpected state change occurred.
+         */
+        ENCODER_ERROR,
+        /** An error with muxer state such as during creation or when stopping. */
+        MUXER_ERROR,
+        /**
+         * An error indicating start recording was called when video recording is still in progress.
+         */
+        RECORDING_IN_PROGRESS
+    }
+
+    /** Listener containing callbacks for video file I/O events. */
+    public interface OnVideoSavedListener {
+        /** Called when the video has been successfully saved. */
+        void onVideoSaved(File file);
+
+        /** Called when an error occurs while attempting to save the video. */
+        void onError(UseCaseError useCaseError, String message, @Nullable Throwable cause);
+    }
+
+    /**
+     * Provides a base static default configuration for the VideoCaptureUseCase
+     *
+     * <p>These values may be overridden by the implementation. They only provide a minimum set of
+     * defaults that are implementation independent.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Defaults
+            implements ConfigurationProvider<VideoCaptureUseCaseConfiguration> {
+        private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+        private static final Rational DEFAULT_ASPECT_RATIO = new Rational(16, 9);
+        private static final int DEFAULT_VIDEO_FRAME_RATE = 30;
+        /** 8Mb/s the recommend rate for 30fps 1080p */
+        private static final int DEFAULT_BIT_RATE = 8 * 1024 * 1024;
+        /** Seconds between each key frame */
+        private static final int DEFAULT_INTRA_FRAME_INTERVAL = 1;
+        /** audio bit rate */
+        private static final int DEFAULT_AUDIO_BIT_RATE = 64000;
+        /** audio sample rate */
+        private static final int DEFAULT_AUDIO_SAMPLE_RATE = 8000;
+        /** audio channel count */
+        private static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
+        /** audio record source */
+        private static final int DEFAULT_AUDIO_RECORD_SOURCE = AudioSource.MIC;
+        /** audio default minimum buffer size */
+        private static final int DEFAULT_AUDIO_MIN_BUFFER_SIZE = 1024;
+        /** Current max resolution of VideoCaptureUseCase is set as FHD */
+        private static final Size DEFAULT_MAX_RESOLUTION = new Size(1920, 1080);
+        /** Surface occupancy prioirty to this use case */
+        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 3;
+
+        private static final VideoCaptureUseCaseConfiguration DEFAULT_CONFIG;
+
+        static {
+            VideoCaptureUseCaseConfiguration.Builder builder =
+                    new VideoCaptureUseCaseConfiguration.Builder()
+                            .setCallbackHandler(DEFAULT_HANDLER)
+                            .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+                            .setVideoFrameRate(DEFAULT_VIDEO_FRAME_RATE)
+                            .setBitRate(DEFAULT_BIT_RATE)
+                            .setIFrameInterval(DEFAULT_INTRA_FRAME_INTERVAL)
+                            .setAudioBitRate(DEFAULT_AUDIO_BIT_RATE)
+                            .setAudioSampleRate(DEFAULT_AUDIO_SAMPLE_RATE)
+                            .setAudioChannelCount(DEFAULT_AUDIO_CHANNEL_COUNT)
+                            .setAudioRecordSource(DEFAULT_AUDIO_RECORD_SOURCE)
+                            .setAudioMinBufferSize(DEFAULT_AUDIO_MIN_BUFFER_SIZE)
+                            .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+                            .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+            DEFAULT_CONFIG = builder.build();
+        }
+
+        @Override
+        public VideoCaptureUseCaseConfiguration getConfiguration() {
+            return DEFAULT_CONFIG;
+        }
+    }
+
+    /** Holder class for metadata that should be saved alongside captured video. */
+    public static final class Metadata {
+        /** Data representing a geographic location. */
+        public @Nullable
+        Location location;
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCaseConfiguration.java
new file mode 100644
index 0000000..7de0058
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCaseConfiguration.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** Configuration for a video capture use case. */
+public final class VideoCaptureUseCaseConfiguration
+        implements UseCaseConfiguration<VideoCaptureUseCase>,
+        ImageOutputConfiguration,
+        CameraDeviceConfiguration,
+        ThreadConfiguration {
+
+    // Option Declarations:
+    // ***********************************************************************************************
+    static final Option<Integer> OPTION_VIDEO_FRAME_RATE =
+            Option.create("camerax.core.videoCapture.recordingFrameRate", int.class);
+    static final Option<Integer> OPTION_BIT_RATE =
+            Option.create("camerax.core.videoCapture.bitRate", int.class);
+    static final Option<Integer> OPTION_INTRA_FRAME_INTERVAL =
+            Option.create("camerax.core.videoCapture.intraFrameInterval", int.class);
+    static final Option<Integer> OPTION_AUDIO_BIT_RATE =
+            Option.create("camerax.core.videoCapture.audioBitRate", int.class);
+    static final Option<Integer> OPTION_AUDIO_SAMPLE_RATE =
+            Option.create("camerax.core.videoCapture.audioSampleRate", int.class);
+    static final Option<Integer> OPTION_AUDIO_CHANNEL_COUNT =
+            Option.create("camerax.core.videoCapture.audioChannelCount", int.class);
+    static final Option<Integer> OPTION_AUDIO_RECORD_SOURCE =
+            Option.create("camerax.core.videoCapture.audioRecordSource", int.class);
+    static final Option<Integer> OPTION_AUDIO_MIN_BUFFER_SIZE =
+            Option.create("camerax.core.videoCapture.audioMinBufferSize", int.class);
+    private final OptionsBundle config;
+
+    VideoCaptureUseCaseConfiguration(OptionsBundle config) {
+        this.config = config;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /**
+     * Returns the recording frames per second.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getVideoFrameRate(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_VIDEO_FRAME_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the recording frames per second.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getVideoFrameRate() {
+        return getConfiguration().retrieveOption(OPTION_VIDEO_FRAME_RATE);
+    }
+
+    /**
+     * Returns the encoding bit rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getBitRate(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_BIT_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the encoding bit rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getBitRate() {
+        return getConfiguration().retrieveOption(OPTION_BIT_RATE);
+    }
+
+    /**
+     * Returns the number of seconds between each key frame.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    public int getIFrameInterval(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_INTRA_FRAME_INTERVAL, valueIfMissing);
+    }
+
+    /**
+     * Returns the number of seconds between each key frame.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    public int getIFrameInterval() {
+        return getConfiguration().retrieveOption(OPTION_INTRA_FRAME_INTERVAL);
+    }
+
+    /**
+     * Returns the audio encoding bit rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioBitRate(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_BIT_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio encoding bit rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioBitRate() {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_BIT_RATE);
+    }
+
+    /**
+     * Returns the audio sample rate.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioSampleRate(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_SAMPLE_RATE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio sample rate.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioSampleRate() {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_SAMPLE_RATE);
+    }
+
+    /**
+     * Returns the audio channel count.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioChannelCount(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_CHANNEL_COUNT, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio channel count.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioChannelCount() {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_CHANNEL_COUNT);
+    }
+
+    /**
+     * Returns the audio recording source.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioRecordSource(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_RECORD_SOURCE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio recording source.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioRecordSource() {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_RECORD_SOURCE);
+    }
+
+    /**
+     * Returns the audio minimum buffer size, in bytes.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioMinBufferSize(int valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE, valueIfMissing);
+    }
+
+    /**
+     * Returns the audio minimum buffer size, in bytes.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getAudioMinBufferSize() {
+        return getConfiguration().retrieveOption(OPTION_AUDIO_MIN_BUFFER_SIZE);
+    }
+
+    /** Builder for a {@link VideoCaptureUseCaseConfiguration}. */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<
+            VideoCaptureUseCase, VideoCaptureUseCaseConfiguration, Builder>,
+            ImageOutputConfiguration.Builder<VideoCaptureUseCaseConfiguration, Builder>,
+            CameraDeviceConfiguration.Builder<VideoCaptureUseCaseConfiguration, Builder>,
+            ThreadConfiguration.Builder<VideoCaptureUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle mutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            this.mutableConfig = mutableConfig;
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(VideoCaptureUseCase.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass(VideoCaptureUseCase.class);
+        }
+
+        /**
+         * Generates a Builder from another Configuration object
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        public static Builder fromConfig(VideoCaptureUseCaseConfiguration configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mutableConfig;
+        }
+
+        /** The solution for the unchecked cast warning. */
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public VideoCaptureUseCaseConfiguration build() {
+            return new VideoCaptureUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+        }
+
+        /**
+         * Sets the recording frames per second.
+         *
+         * @param videoFrameRate The requested interval in seconds.
+         * @return The current Builder.
+         */
+        public Builder setVideoFrameRate(int videoFrameRate) {
+            getMutableConfiguration().insertOption(OPTION_VIDEO_FRAME_RATE, videoFrameRate);
+            return builder();
+        }
+
+        /**
+         * Sets the encoding bit rate.
+         *
+         * @param bitRate The requested bit rate in bits per second.
+         * @return The current Builder.
+         */
+        public Builder setBitRate(int bitRate) {
+            getMutableConfiguration().insertOption(OPTION_BIT_RATE, bitRate);
+            return builder();
+        }
+
+        /**
+         * Sets number of seconds between each key frame in seconds.
+         *
+         * @param interval The requested interval in seconds.
+         * @return The current Builder.
+         */
+        public Builder setIFrameInterval(int interval) {
+            getMutableConfiguration().insertOption(OPTION_INTRA_FRAME_INTERVAL, interval);
+            return builder();
+        }
+
+        /**
+         * Sets the bit rate of the audio stream.
+         *
+         * @param bitRate The requested bit rate in bits/s.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setAudioBitRate(int bitRate) {
+            getMutableConfiguration().insertOption(OPTION_AUDIO_BIT_RATE, bitRate);
+            return builder();
+        }
+
+        /**
+         * Sets the sample rate of the audio stream.
+         *
+         * @param sampleRate The requested sample rate in bits/s.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setAudioSampleRate(int sampleRate) {
+            getMutableConfiguration().insertOption(OPTION_AUDIO_SAMPLE_RATE, sampleRate);
+            return builder();
+        }
+
+        /**
+         * Sets the number of audio channels.
+         *
+         * @param channelCount The requested number of audio channels.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setAudioChannelCount(int channelCount) {
+            getMutableConfiguration().insertOption(OPTION_AUDIO_CHANNEL_COUNT, channelCount);
+            return builder();
+        }
+
+        /**
+         * Sets the audio source.
+         *
+         * @param source The audio source. Currently only AudioSource.MIC is supported.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setAudioRecordSource(int source) {
+            getMutableConfiguration().insertOption(OPTION_AUDIO_RECORD_SOURCE, source);
+            return builder();
+        }
+
+        /**
+         * Sets the audio min buffer size.
+         *
+         * @param minBufferSize The requested audio minimum buffer size, in bytes.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setAudioMinBufferSize(int minBufferSize) {
+            getMutableConfiguration().insertOption(OPTION_AUDIO_MIN_BUFFER_SIZE, minBufferSize);
+            return builder();
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCase.java b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCase.java
new file mode 100644
index 0000000..627fb1c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCase.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A use case that provides a camera preview stream for a view finder.
+ *
+ * <p>The preview stream is connected to an underlying {@link SurfaceTexture}. The caller is still
+ * responsible for deciding how this texture is shown.
+ */
+public class ViewFinderUseCase extends BaseUseCase {
+    /**
+     * Provides a static configuration with implementation-agnostic options.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final Defaults DEFAULT_CONFIG = new Defaults();
+    private static final String TAG = "ViewFinderUseCase";
+    private final Handler mainHandler = new Handler(Looper.getMainLooper());
+    private final CheckedSurfaceTexture.OnTextureChangedListener surfaceTextureListener =
+            (newSurfaceTexture, newResolution) ->
+                    ViewFinderUseCase.this.updateOutput(newSurfaceTexture, newResolution);
+    private final CheckedSurfaceTexture checkedSurfaceTexture =
+            new CheckedSurfaceTexture(surfaceTextureListener, mainHandler);
+    private final ViewFinderUseCaseConfiguration.Builder useCaseConfigBuilder;
+    @Nullable
+    private OnViewFinderOutputUpdateListener subscribedViewFinderOutputListener;
+    @Nullable
+    private ViewFinderOutput latestViewFinderOutput;
+    private boolean surfaceDispatched = false;
+    /**
+     * Creates a new view finder use case from the given configuration.
+     *
+     * @param configuration for this use case instance
+     */
+    @MainThread
+    public ViewFinderUseCase(ViewFinderUseCaseConfiguration configuration) {
+        super(configuration);
+        useCaseConfigBuilder = ViewFinderUseCaseConfiguration.Builder.fromConfig(configuration);
+    }
+
+    private static SessionConfiguration.Builder createFrom(
+            ViewFinderUseCaseConfiguration configuration, DeferrableSurface surface) {
+        SessionConfiguration.Builder sessionConfigBuilder =
+                SessionConfiguration.Builder.createFrom(configuration);
+        sessionConfigBuilder.addSurface(surface);
+        return sessionConfigBuilder;
+    }
+
+    private static final String getCameraIdUnchecked(LensFacing lensFacing) {
+        try {
+            return CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to get camera id for camera lens facing " + lensFacing, e);
+        }
+    }
+
+    /**
+     * Removes previously ViewFinderOutput listener.
+     *
+     * <p>This is equivalent to calling {@code setOnViewFinderOutputUpdateListener(null)}.
+     */
+    @UiThread
+    public void removeViewFinderOutputListener() {
+        setOnViewFinderOutputUpdateListener(null);
+    }
+
+    /**
+     * Gets {@link OnViewFinderOutputUpdateListener}
+     *
+     * @return the last set listener or {@code null} if no listener is set
+     */
+    @UiThread
+    @Nullable
+    public OnViewFinderOutputUpdateListener getOnViewFinderOutputUpdateListener() {
+        return subscribedViewFinderOutputListener;
+    }
+
+    /**
+     * Sets a listener to get the {@link ViewFinderOutput} updates.
+     *
+     * <p>Setting this listener will signal to the camera that the use case is ready to receive
+     * data. Setting the listener to {@code null} will signal to the camera that the camera should
+     * no longer stream data to the last {@link ViewFinderOutput}.
+     *
+     * <p>Once {@link OnViewFinderOutputUpdateListener#onUpdated(ViewFinderOutput)} is called,
+     * ownership of the {@link ViewFinderOutput} and its contents is transferred to the user. It is
+     * the user's responsibility to release the last {@link SurfaceTexture} returned by {@link
+     * ViewFinderOutput#getSurfaceTexture()} when a new SurfaceTexture is provided via an update or
+     * when the user is finished with the use case.
+     *
+     * @param listener The listener which will receive {@link ViewFinderOutput} updates.
+     */
+    @UiThread
+    public void setOnViewFinderOutputUpdateListener(
+            @Nullable OnViewFinderOutputUpdateListener newListener) {
+        OnViewFinderOutputUpdateListener oldListener = subscribedViewFinderOutputListener;
+        subscribedViewFinderOutputListener = newListener;
+        if (oldListener == null && newListener != null) {
+            notifyActive();
+            if (latestViewFinderOutput != null) {
+                surfaceDispatched = true;
+                newListener.onUpdated(latestViewFinderOutput);
+            }
+        } else if (oldListener != null && newListener == null) {
+            notifyInactive();
+        } else if (oldListener != null && oldListener != newListener) {
+            if (latestViewFinderOutput != null) {
+                checkedSurfaceTexture.resetSurfaceTexture();
+            }
+        }
+    }
+
+    // TODO: Timeout may be exposed as a ViewFinderUseCaseConfiguration(moved to CameraControl)
+
+    private CameraControl getCurrentCameraControl() {
+        ViewFinderUseCaseConfiguration configuration =
+                (ViewFinderUseCaseConfiguration) getUseCaseConfiguration();
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+        return getCameraControl(cameraId);
+    }
+
+    /**
+     * Adjusts the view finder according to the properties in some local regions.
+     *
+     * <p>The auto-focus (AF) and auto-exposure (AE) properties will be recalculated from the local
+     * regions.
+     *
+     * @param focus    rectangle with dimensions in sensor coordinate frame for focus
+     * @param metering rectangle with dimensions in sensor coordinate frame for metering
+     */
+    public void focus(Rect focus, Rect metering) {
+        focus(focus, metering, null);
+    }
+
+    /**
+     * Adjusts the view finder according to the properties in some local regions with a callback
+     * called once focus scan has completed.
+     *
+     * <p>The auto-focus (AF) and auto-exposure (AE) properties will be recalculated from the local
+     * regions.
+     *
+     * @param focus    rectangle with dimensions in sensor coordinate frame for focus
+     * @param metering rectangle with dimensions in sensor coordinate frame for metering
+     * @param listener listener for when focus has completed
+     */
+    public void focus(Rect focus, Rect metering, @Nullable OnFocusCompletedListener listener) {
+        getCurrentCameraControl().focus(focus, metering, listener, mainHandler);
+    }
+
+    /**
+     * Adjusts the view finder to zoom to a local region.
+     *
+     * @param crop rectangle with dimensions in sensor coordinate frame for zooming
+     */
+    public void zoom(Rect crop) {
+        getCurrentCameraControl().setCropRegion(crop);
+    }
+
+    /**
+     * Sets torch on/off.
+     *
+     * @param torch True if turn on torch, otherwise false
+     */
+    public void enableTorch(boolean torch) {
+        getCurrentCameraControl().enableTorch(torch);
+    }
+
+    /** True if the torch is on */
+    public boolean isTorchOn() {
+        return getCurrentCameraControl().isTorchOn();
+    }
+
+    /**
+     * Sets the rotation of the surface texture consumer.
+     *
+     * <p>In most cases this should be set to the current rotation returned by {@link
+     * Display#getRotation()}. This will update the rotation value in {@link ViewFinderOutput} to
+     * reflect the angle the ViewFinderOutput should be rotated to match the supplied rotation.
+     *
+     * @param rotation Rotation of the surface texture consumer.
+     */
+    public void setTargetRotation(@RotationValue int rotation) {
+        ImageOutputConfiguration oldconfig = (ImageOutputConfiguration) getUseCaseConfiguration();
+        int oldRotation = oldconfig.getTargetRotation(ImageOutputConfiguration.INVALID_ROTATION);
+        if (oldRotation == ImageOutputConfiguration.INVALID_ROTATION || oldRotation != rotation) {
+            useCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(useCaseConfigBuilder.build());
+
+            // TODO(b/122846516): Update session configuration and possibly reconfigure session.
+            // For now we'll just attempt to update the rotation metadata.
+            invalidateMetadata();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return TAG + ":" + getName();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @Nullable
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder() {
+        ViewFinderUseCaseConfiguration defaults =
+                CameraX.getDefaultUseCaseConfiguration(ViewFinderUseCaseConfiguration.class);
+        if (defaults != null) {
+            return ViewFinderUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void clear() {
+        checkedSurfaceTexture.release();
+        removeViewFinderOutputListener();
+        notifyInactive();
+
+        SurfaceTexture oldTexture =
+                (latestViewFinderOutput == null)
+                        ? null
+                        : latestViewFinderOutput.getSurfaceTexture();
+        if (oldTexture != null && !surfaceDispatched) {
+            oldTexture.release();
+        }
+
+        super.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        ViewFinderUseCaseConfiguration configuration =
+                (ViewFinderUseCaseConfiguration) getUseCaseConfiguration();
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+        Size resolution = suggestedResolutionMap.get(cameraId);
+        if (resolution == null) {
+            throw new IllegalArgumentException(
+                    "Suggested resolution map missing resolution for camera " + cameraId);
+        }
+
+        checkedSurfaceTexture.setResolution(resolution);
+        checkedSurfaceTexture.resetSurfaceTexture();
+
+        SessionConfiguration.Builder sessionConfigBuilder =
+                createFrom(configuration, checkedSurfaceTexture);
+        attachToCamera(cameraId, sessionConfigBuilder.build());
+
+        return suggestedResolutionMap;
+    }
+
+    @UiThread
+    private void invalidateMetadata() {
+        if (latestViewFinderOutput != null) {
+            // Only update the output if we have a SurfaceTexture. Otherwise we'll wait until a
+            // SurfaceTexture is ready.
+            updateOutput(
+                    latestViewFinderOutput.getSurfaceTexture(),
+                    latestViewFinderOutput.getTextureSize());
+        }
+    }
+
+    @UiThread
+    private void updateOutput(SurfaceTexture surfaceTexture, Size resolution) {
+        ViewFinderUseCaseConfiguration useCaseConfig =
+                (ViewFinderUseCaseConfiguration) getUseCaseConfiguration();
+
+        int relativeRotation =
+                (latestViewFinderOutput == null) ? 0 : latestViewFinderOutput.getRotationDegrees();
+        try {
+            // Attempt to get the camera ID. If this fails, we probably don't have permission, so we
+            // will rely on the updated UseCaseConfiguration to set the correct rotation in
+            // onSuggestedResolutionUpdated()
+            String cameraId = CameraX.getCameraWithLensFacing(useCaseConfig.getLensFacing());
+            CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+            relativeRotation =
+                    cameraInfo.getSensorRotationDegrees(
+                            useCaseConfig.getTargetRotation(Surface.ROTATION_0));
+        } catch (CameraInfoUnavailableException e) {
+            Log.e(TAG, "Unable to update output metadata: " + e);
+        }
+
+        ViewFinderOutput newOutput =
+                ViewFinderOutput.create(surfaceTexture, resolution, relativeRotation);
+
+        // Only update the output if something has changed
+        if (!Objects.equals(latestViewFinderOutput, newOutput)) {
+            SurfaceTexture oldTexture =
+                    (latestViewFinderOutput == null)
+                            ? null
+                            : latestViewFinderOutput.getSurfaceTexture();
+            OnViewFinderOutputUpdateListener outputListener = getOnViewFinderOutputUpdateListener();
+
+            latestViewFinderOutput = newOutput;
+
+            boolean textureChanged = oldTexture != surfaceTexture;
+            if (textureChanged) {
+                // If the old surface was never dispatched, we can safely release the old
+                // SurfaceTexture.
+                if (oldTexture != null && !surfaceDispatched) {
+                    oldTexture.release();
+                }
+
+                // Keep track of whether this SurfaceTexture is dispatched
+                surfaceDispatched = false;
+            }
+
+            if (outputListener != null) {
+                // If we have a listener, then we should be active and we require a reset if the
+                // SurfaceTexture changed.
+                if (textureChanged) {
+                    notifyReset();
+                }
+
+                surfaceDispatched = true;
+                outputListener.onUpdated(newOutput);
+            }
+        }
+    }
+
+    /** Describes the error that occurred during viewfinder operation. */
+    public enum UseCaseError {
+        /** Unknown error occurred. See message or log for more details. */
+        UNKNOWN_ERROR
+    }
+
+    /** A listener of {@link ViewFinderOutput}. */
+    public interface OnViewFinderOutputUpdateListener {
+        /** Callback when ViewFinderOutput has been updated. */
+        void onUpdated(ViewFinderOutput output);
+    }
+
+    /**
+     * Provides a base static default configuration for the ViewFinderUseCase
+     *
+     * <p>These values may be overridden by the implementation. They only provide a minimum set of
+     * defaults that are implementation independent.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Defaults
+            implements ConfigurationProvider<ViewFinderUseCaseConfiguration> {
+        private static final Handler DEFAULT_HANDLER = new Handler(Looper.getMainLooper());
+        private static final Rational DEFAULT_ASPECT_RATIO = new Rational(16, 9);
+        private static final Size DEFAULT_MAX_RESOLUTION =
+                CameraX.getSurfaceManager().getPreviewSize();
+        private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 2;
+
+        private static final ViewFinderUseCaseConfiguration DEFAULT_CONFIG;
+
+        static {
+            ViewFinderUseCaseConfiguration.Builder builder =
+                    new ViewFinderUseCaseConfiguration.Builder()
+                            .setCallbackHandler(DEFAULT_HANDLER)
+                            .setTargetAspectRatio(DEFAULT_ASPECT_RATIO)
+                            .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+                            .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+            DEFAULT_CONFIG = builder.build();
+        }
+
+        @Override
+        public ViewFinderUseCaseConfiguration getConfiguration() {
+            return DEFAULT_CONFIG;
+        }
+    }
+
+    /**
+     * A bundle containing a {@link SurfaceTexture} and properties needed to display a ViewFinder.
+     */
+    @AutoValue
+    public abstract static class ViewFinderOutput {
+
+        ViewFinderOutput() {
+        }
+
+        static ViewFinderOutput create(
+                SurfaceTexture surfaceTexture, Size textureSize, int rotationDegrees) {
+            return new AutoValue_ViewFinderUseCase_ViewFinderOutput(
+                    surfaceTexture, textureSize, rotationDegrees);
+        }
+
+        /** Returns the ViewFinderOutput that receives image data. */
+        public abstract SurfaceTexture getSurfaceTexture();
+
+        /** Returns the dimensions of the ViewFinderOutput. */
+        public abstract Size getTextureSize();
+
+        /**
+         * Returns the rotation required, in degrees, to transform the ViewFinderOutput to match the
+         * orientation given by ImageOutputConfiguration#getTargetRotation(int).
+         *
+         * <p>This number is independent of any rotation value that can be derived from the
+         * ViewFinderOutput's {@link SurfaceTexture#getTransformMatrix(float[])}.
+         */
+        public abstract int getRotationDegrees();
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCaseConfiguration.java b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCaseConfiguration.java
new file mode 100644
index 0000000..8f2fc6d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCaseConfiguration.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.util.Size;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/** Configuration for an image capture use case. */
+public final class ViewFinderUseCaseConfiguration
+        implements UseCaseConfiguration<ViewFinderUseCase>,
+        ImageOutputConfiguration,
+        CameraDeviceConfiguration,
+        ThreadConfiguration {
+
+    private final OptionsBundle config;
+
+    /** Creates a new configuration instance. */
+    ViewFinderUseCaseConfiguration(OptionsBundle config) {
+        this.config = config;
+    }
+
+    /**
+     * Retrieves the resolution of the target intending to use from this configuration.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or {@code valueIfMissing} if the value does not exist in this
+     * configuration.
+     */
+    @Override
+    public Size getTargetResolution(Size valueIfMissing) {
+        return getConfiguration()
+                .retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the resolution of the target intending to use from this configuration.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    @Override
+    public Size getTargetResolution() {
+        return getConfiguration().retrieveOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** Builder for a {@link ViewFinderUseCaseConfiguration}. */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<
+            ViewFinderUseCase, ViewFinderUseCaseConfiguration, Builder>,
+            ImageOutputConfiguration.Builder<ViewFinderUseCaseConfiguration, Builder>,
+            CameraDeviceConfiguration.Builder<ViewFinderUseCaseConfiguration, Builder>,
+            ThreadConfiguration.Builder<ViewFinderUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle mutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            this.mutableConfig = mutableConfig;
+
+            Class<?> oldConfigClass =
+                    mutableConfig.retrieveOption(TargetConfiguration.OPTION_TARGET_CLASS, null);
+            if (oldConfigClass != null && !oldConfigClass.equals(ViewFinderUseCase.class)) {
+                throw new IllegalArgumentException(
+                        "Invalid target class configuration for "
+                                + Builder.this
+                                + ": "
+                                + oldConfigClass);
+            }
+
+            setTargetClass(ViewFinderUseCase.class);
+        }
+
+        /**
+         * Generates a Builder from another Configuration object
+         *
+         * @param configuration An immutable configuration to pre-populate this builder.
+         * @return The new Builder.
+         */
+        public static Builder fromConfig(ViewFinderUseCaseConfiguration configuration) {
+            return new Builder(MutableOptionsBundle.from(configuration));
+        }
+
+        /**
+         * Sets the resolution of the intended target from this configuration.
+         *
+         * <p>The target resolution attempts to establish a minimum bound for the view finder
+         * resolution. The actual view finder resolution will be the closest available resolution in
+         * size that is not smaller than the target resolution, as determined by the Camera
+         * implementation. However, if no resolution exists that is equal to or larger than the
+         * target resolution, the nearest available resolution smaller than the target resolution
+         * will be chosen.
+         *
+         * @param resolution The target resolution to choose from supported output sizes list.
+         * @return The current Builder.
+         */
+        @Override
+        public Builder setTargetResolution(Size resolution) {
+            getMutableConfiguration()
+                    .insertOption(ImageOutputConfiguration.OPTION_TARGET_RESOLUTION, resolution);
+            return builder();
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public ViewFinderUseCaseConfiguration build() {
+            return new ViewFinderUseCaseConfiguration(OptionsBundle.from(mutableConfig));
+        }
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/AppConfigurationRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/AppConfigurationRobolectricTest.java
new file mode 100644
index 0000000..69013d2
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/AppConfigurationRobolectricTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.testing.fakes.FakeAppConfiguration;
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
+import androidx.camera.testing.fakes.FakeCameraFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class AppConfigurationRobolectricTest {
+
+    private AppConfiguration appConfiguration;
+
+    @Before
+    public void setUp() {
+        appConfiguration = FakeAppConfiguration.create();
+    }
+
+    @Test
+    public void canGetConfigTarget() {
+        Class<CameraX> configTarget = appConfiguration.getTargetClass(/*valueIfMissing=*/ null);
+        assertThat(configTarget).isEqualTo(CameraX.class);
+    }
+
+    @Test
+    public void canGetCameraFactory() {
+        CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+        assertThat(cameraFactory).isInstanceOf(FakeCameraFactory.class);
+    }
+
+    @Test
+    public void canGetDeviceSurfaceManager() {
+        CameraDeviceSurfaceManager surfaceManager =
+                appConfiguration.getDeviceSurfaceManager(/*valueIfMissing=*/ null);
+        assertThat(surfaceManager).isInstanceOf(FakeCameraDeviceSurfaceManager.class);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureRobolectricTest.java
new file mode 100644
index 0000000..576e94a
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureRobolectricTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CheckedSurfaceTexture.OnTextureChangedListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class CheckedSurfaceTextureRobolectricTest {
+
+    private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+    private Size defaultResolution;
+    private CheckedSurfaceTexture checkedSurfaceTexture;
+    private SurfaceTexture latestSurfaceTexture;
+    private final CheckedSurfaceTexture.OnTextureChangedListener textureChangedListener =
+            new OnTextureChangedListener() {
+                @Override
+                public void onTextureChanged(
+                        @Nullable SurfaceTexture newOutput, @Nullable Size newResolution) {
+                    latestSurfaceTexture = newOutput;
+                }
+            };
+
+    @Before
+    public void setup() {
+        defaultResolution = new Size(640, 480);
+        checkedSurfaceTexture =
+                new CheckedSurfaceTexture(textureChangedListener, mainThreadHandler);
+        checkedSurfaceTexture.setResolution(defaultResolution);
+    }
+
+    @Test
+    public void viewFinderOutputUpdatesWhenReset() {
+        // Create the initial surface texture
+        checkedSurfaceTexture.resetSurfaceTexture();
+
+        // Surface texture should have been set
+        SurfaceTexture initialOutput = latestSurfaceTexture;
+
+        // Create a new surface texture
+        checkedSurfaceTexture.resetSurfaceTexture();
+
+        assertThat(initialOutput).isNotNull();
+        assertThat(latestSurfaceTexture).isNotNull();
+        assertThat(latestSurfaceTexture).isNotEqualTo(initialOutput);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/ExifRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/ExifRobolectricTest.java
new file mode 100644
index 0000000..49a0781
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/ExifRobolectricTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.location.Location;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowSystemClock;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class ExifRobolectricTest {
+    private static final InputStream FAKE_INPUT_STREAM =
+            new InputStream() {
+                @Override
+                public int read() throws IOException {
+                    return 0;
+                }
+            };
+    private Exif exif;
+
+    @Before
+    public void setup() throws Exception {
+        ShadowLog.stream = System.out;
+        exif = Exif.createFromInputStream(FAKE_INPUT_STREAM);
+    }
+
+    @Test
+    public void defaultsAreExpectedValues() {
+        assertThat(exif.getRotation()).isEqualTo(0);
+        assertThat(exif.isFlippedHorizontally()).isFalse();
+        assertThat(exif.isFlippedVertically()).isFalse();
+        assertThat(exif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+        assertThat(exif.getLocation()).isNull();
+        assertThat(exif.getDescription()).isNull();
+    }
+
+    @Test
+    public void rotateProducesCorrectRotation() {
+        assertThat(exif.getRotation()).isEqualTo(0);
+        exif.rotate(90);
+        assertThat(exif.getRotation()).isEqualTo(90);
+        exif.rotate(90);
+        assertThat(exif.getRotation()).isEqualTo(180);
+        exif.rotate(90);
+        assertThat(exif.getRotation()).isEqualTo(270);
+        exif.rotate(90);
+        assertThat(exif.getRotation()).isEqualTo(0);
+        exif.rotate(-90);
+        assertThat(exif.getRotation()).isEqualTo(270);
+        exif.rotate(360);
+        assertThat(exif.getRotation()).isEqualTo(270);
+        exif.rotate(500 * 360 - 90);
+        assertThat(exif.getRotation()).isEqualTo(180);
+    }
+
+    @Test
+    public void flipHorizontallyWillToggle() {
+        assertThat(exif.isFlippedHorizontally()).isFalse();
+        exif.flipHorizontally();
+        assertThat(exif.isFlippedHorizontally()).isTrue();
+        exif.flipHorizontally();
+        assertThat(exif.isFlippedHorizontally()).isFalse();
+    }
+
+    @Test
+    public void flipVerticallyWillToggle() {
+        assertThat(exif.isFlippedVertically()).isFalse();
+        exif.flipVertically();
+        assertThat(exif.isFlippedVertically()).isTrue();
+        exif.flipVertically();
+        assertThat(exif.isFlippedVertically()).isFalse();
+    }
+
+    @Test
+    public void flipAndRotateUpdatesHorizontalAndVerticalFlippedState() {
+        assertThat(exif.getRotation()).isEqualTo(0);
+        assertThat(exif.isFlippedHorizontally()).isFalse();
+        assertThat(exif.isFlippedVertically()).isFalse();
+
+        exif.rotate(-90);
+        assertThat(exif.getRotation()).isEqualTo(270);
+
+        exif.flipHorizontally();
+        assertThat(exif.getRotation()).isEqualTo(90);
+        assertThat(exif.isFlippedVertically()).isTrue();
+
+        exif.flipVertically();
+        assertThat(exif.getRotation()).isEqualTo(90);
+        assertThat(exif.isFlippedHorizontally()).isFalse();
+        assertThat(exif.isFlippedVertically()).isFalse();
+
+        exif.rotate(90);
+        assertThat(exif.getRotation()).isEqualTo(180);
+
+        exif.flipVertically();
+        assertThat(exif.getRotation()).isEqualTo(0);
+        assertThat(exif.isFlippedHorizontally()).isTrue();
+        assertThat(exif.isFlippedVertically()).isFalse();
+
+        exif.flipHorizontally();
+        assertThat(exif.getRotation()).isEqualTo(0);
+        assertThat(exif.isFlippedHorizontally()).isFalse();
+        assertThat(exif.isFlippedVertically()).isFalse();
+    }
+
+    @Test
+    public void timestampCanBeAttachedAndRemoved() {
+        assertThat(exif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+        exif.attachTimestamp();
+        assertThat(exif.getTimestamp()).isNotEqualTo(Exif.INVALID_TIMESTAMP);
+
+        exif.removeTimestamp();
+        assertThat(exif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void attachedTimestampUsesSystemWallTime() {
+        long beforeTimestamp = System.currentTimeMillis();
+
+        // The Exif class is instrumented since it's in the androidx.* namespace.
+        // Set the ShadowSystemClock to match the real system clock.
+        ShadowSystemClock.setNanoTime(System.currentTimeMillis() * 1000 * 1000);
+        exif.attachTimestamp();
+        long afterTimestamp = System.currentTimeMillis();
+
+        // Check that the attached timestamp is in the closed range [beforeTimestamp,
+        // afterTimestamp].
+        long attachedTimestamp = exif.getTimestamp();
+        assertThat(attachedTimestamp).isAtLeast(beforeTimestamp);
+        assertThat(attachedTimestamp).isAtMost(afterTimestamp);
+    }
+
+    @Test
+    public void locationCanBeAttachedAndRemoved() {
+        assertThat(exif.getLocation()).isNull();
+
+        Location location = new Location("TEST");
+        location.setLatitude(22.3);
+        location.setLongitude(114);
+        location.setTime(System.currentTimeMillis() / 1000 * 1000);
+        exif.attachLocation(location);
+        assertThat(location.toString()).isEqualTo(exif.getLocation().toString());
+
+        exif.removeLocation();
+        assertThat(exif.getLocation()).isNull();
+    }
+
+    @Test
+    public void locationWithAltitudeCanBeAttached() {
+        Location location = new Location("TEST");
+        location.setLatitude(22.3);
+        location.setLongitude(114);
+        location.setTime(System.currentTimeMillis() / 1000 * 1000);
+        location.setAltitude(5.0);
+        exif.attachLocation(location);
+        assertThat(location.toString()).isEqualTo(exif.getLocation().toString());
+    }
+
+    @Test
+    public void locationWithSpeedCanBeAttached() {
+        Location location = new Location("TEST");
+        location.setLatitude(22.3);
+        location.setLongitude(114);
+        location.setTime(System.currentTimeMillis() / 1000 * 1000);
+        location.setSpeed(5.0f);
+        exif.attachLocation(location);
+        // Location loses precision when set through attachLocation(), so check the locations are
+        // roughly equal first
+        Location exifLocation = exif.getLocation();
+        assertThat(location.getSpeed()).isWithin(0.01f).of(exifLocation.getSpeed());
+
+        // Remove speed and compare the rest by string
+        exifLocation.removeSpeed();
+        location.removeSpeed();
+        assertThat(location.toString()).isEqualTo(exifLocation.toString());
+    }
+
+    @Test
+    public void descriptionCanBeAttachedAndRemoved() {
+        assertThat(exif.getDescription()).isNull();
+
+        exif.setDescription("Hello World");
+        assertThat(exif.getDescription()).isEqualTo("Hello World");
+
+        exif.setDescription(null);
+        assertThat(exif.getDescription()).isNull();
+    }
+
+    @Test
+    public void saveUpdatesLastModifiedTimestampUnlessRemoved() {
+        assertThat(exif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+        try {
+            exif.save();
+        } catch (IOException e) {
+            // expected
+        }
+
+        assertThat(exif.getLastModifiedTimestamp()).isNotEqualTo(Exif.INVALID_TIMESTAMP);
+
+        // removeTimestamp should also be clearing the last modified timestamp
+        exif.removeTimestamp();
+        assertThat(exif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+        // Even when saving again
+        try {
+            exif.save();
+        } catch (IOException e) {
+            // expected
+        }
+
+        assertThat(exif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void toStringProducesNonNullString() {
+        assertThat(exif.toString()).isNotNull();
+        exif.setDescription("Hello World");
+        exif.attachTimestamp();
+        Location location = new Location("TEST");
+        location.setLatitude(22.3);
+        location.setLongitude(114);
+        location.setTime(System.currentTimeMillis() / 1000 * 1000);
+        location.setAltitude(5.0);
+        exif.attachLocation(location);
+        assertThat(exif.toString()).isNotNull();
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryRobolectricTest.java
new file mode 100644
index 0000000..5b81226
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryRobolectricTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class ExtendableUseCaseConfigFactoryRobolectricTest {
+
+    private ExtendableUseCaseConfigFactory factory;
+
+    @Before
+    public void setUp() {
+        factory = new ExtendableUseCaseConfigFactory();
+    }
+
+    @Test
+    public void canInstallProvider_andRetrieveConfig() {
+        factory.installDefaultProvider(
+                FakeUseCaseConfiguration.class, new FakeUseCaseConfigurationProvider());
+
+        FakeUseCaseConfiguration config = factory.getConfiguration(FakeUseCaseConfiguration.class);
+        assertThat(config).isNotNull();
+        assertThat(config.getTargetClass(null)).isEqualTo(FakeUseCase.class);
+    }
+
+    private static class FakeUseCaseConfigurationProvider
+            implements ConfigurationProvider<FakeUseCaseConfiguration> {
+
+        @Override
+        public FakeUseCaseConfiguration getConfiguration() {
+            return new FakeUseCaseConfiguration.Builder().build();
+        }
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleRobolectricTest.java
new file mode 100644
index 0000000..a125b7a
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleRobolectricTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.core.Configuration.Option;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class MutableOptionsBundleRobolectricTest {
+
+    private static final Option<Object> OPTION_1 = Option.create("option.1", Object.class);
+    private static final Option<Object> OPTION_1_A = Option.create("option.1.a", Object.class);
+    private static final Option<Object> OPTION_2 = Option.create("option.2", Object.class);
+
+    private static final Object VALUE_1 = new Object();
+    private static final Object VALUE_1_A = new Object();
+    private static final Object VALUE_2 = new Object();
+    private static final Object VALUE_MISSING = new Object();
+
+    @Test
+    public void canCreateEmptyBundle() {
+        MutableOptionsBundle bundle = MutableOptionsBundle.create();
+        assertThat(bundle).isNotNull();
+    }
+
+    @Test
+    public void canAddValue() {
+        MutableOptionsBundle bundle = MutableOptionsBundle.create();
+        bundle.insertOption(OPTION_1, VALUE_1);
+
+        assertThat(bundle.retrieveOption(OPTION_1, VALUE_MISSING)).isSameAs(VALUE_1);
+    }
+
+    @Test
+    public void canRemoveValue() {
+        MutableOptionsBundle bundle = MutableOptionsBundle.create();
+        bundle.insertOption(OPTION_1, VALUE_1);
+        bundle.removeOption(OPTION_1);
+
+        assertThat(bundle.retrieveOption(OPTION_1, VALUE_MISSING)).isSameAs(VALUE_MISSING);
+    }
+
+    @Test
+    public void canCreateFromConfiguration_andAddMore() {
+        MutableOptionsBundle mutOpts = MutableOptionsBundle.create();
+        mutOpts.insertOption(OPTION_1, VALUE_1);
+        mutOpts.insertOption(OPTION_1_A, VALUE_1_A);
+
+        Configuration config = OptionsBundle.from(mutOpts);
+
+        MutableOptionsBundle mutOpts2 = MutableOptionsBundle.from(config);
+        mutOpts2.insertOption(OPTION_2, VALUE_2);
+
+        Configuration config2 = OptionsBundle.from(mutOpts2);
+
+        assertThat(config.listOptions()).containsExactly(OPTION_1, OPTION_1_A);
+        assertThat(config2.listOptions()).containsExactly(OPTION_1, OPTION_1_A, OPTION_2);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/OptionRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/OptionRobolectricTest.java
new file mode 100644
index 0000000..33358a8
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/OptionRobolectricTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.core.Configuration.Option;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class OptionRobolectricTest {
+
+    private static final String OPTION_1_ID = "option.1";
+
+    private static final Object TOKEN = new Object();
+
+    @Test
+    public void canCreateOption_andRetrieveId() {
+        Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+        assertThat(option.getId()).isEqualTo(OPTION_1_ID);
+    }
+
+    @Test
+    public void canCreateOption_fromClass_andRetrieveClass() {
+        Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+        assertThat(option.getValueClass()).isEqualTo(Integer.class);
+    }
+
+    @Test
+    public void canCreateOption_fromPrimitiveClass_andRetrievePrimitiveClass() {
+        Option<Integer> option = Option.create(OPTION_1_ID, int.class);
+        assertThat(option.getValueClass()).isEqualTo(int.class);
+    }
+
+    @Test
+    public void canCreateOption_fromTypeReference() {
+        Option<List<Integer>> option =
+                Option.create(OPTION_1_ID, new TypeReference<List<Integer>>() {
+                });
+        assertThat(option).isNotNull();
+    }
+
+    @Test
+    public void canCreateOption_withNullToken() {
+        Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+        assertThat(option.getToken()).isNull();
+    }
+
+    @Test
+    public void canCreateOption_withToken() {
+        Option<Integer> option = Option.create(OPTION_1_ID, Integer.class, TOKEN);
+        assertThat(option.getToken()).isSameAs(TOKEN);
+    }
+
+    @Test
+    public void canRetrieveOption_fromMap_usingSeparateOptionInstances() {
+        Option<Integer> option = Option.create(OPTION_1_ID, Integer.class);
+        Option<Integer> optionCopy = Option.create(OPTION_1_ID, Integer.class);
+
+        Map<Option<?>, Object> map = new HashMap<>();
+        map.put(option, 1);
+
+        assertThat(map).containsKey(optionCopy);
+        assertThat(map.get(optionCopy)).isEqualTo(1);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/OptionsBundleRobolectricTest.java b/camera/core/src/test/java/androidx/camera/core/OptionsBundleRobolectricTest.java
new file mode 100644
index 0000000..9cc5690
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/OptionsBundleRobolectricTest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.core.Configuration.Option;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class OptionsBundleRobolectricTest {
+
+    private static final Option<Object> OPTION_1 = Option.create("option.1", Object.class);
+    private static final Option<Object> OPTION_1_A = Option.create("option.1.a", Object.class);
+    private static final Option<Object> OPTION_2 = Option.create("option.2", Object.class);
+    private static final Option<Object> OPTION_MISSING =
+            Option.create("option.missing", Object.class);
+
+    private static final Object VALUE_1 = new Object();
+    private static final Object VALUE_1_A = new Object();
+    private static final Object VALUE_2 = new Object();
+    private static final Object VALUE_MISSING = new Object();
+
+    private OptionsBundle allOpts;
+
+    @Before
+    public void setUp() {
+        MutableOptionsBundle mutOpts = MutableOptionsBundle.create();
+        mutOpts.insertOption(OPTION_1, VALUE_1);
+        mutOpts.insertOption(OPTION_1_A, VALUE_1_A);
+        mutOpts.insertOption(OPTION_2, VALUE_2);
+
+        allOpts = OptionsBundle.from(mutOpts);
+    }
+
+    @Test
+    public void canRetrieveValue() {
+        assertThat(allOpts.retrieveOption(OPTION_1)).isSameAs(VALUE_1);
+        assertThat(allOpts.retrieveOption(OPTION_1_A)).isSameAs(VALUE_1_A);
+        assertThat(allOpts.retrieveOption(OPTION_2)).isSameAs(VALUE_2);
+    }
+
+    @Test
+    public void willReturnDefault_ifOptionIsMissing() {
+        Object value = allOpts.retrieveOption(OPTION_MISSING, VALUE_MISSING);
+        assertThat(value).isSameAs(VALUE_MISSING);
+    }
+
+    @Test
+    public void willReturnStoredValue_whenGivenDefault() {
+        Object value = allOpts.retrieveOption(OPTION_1, VALUE_MISSING);
+        assertThat(value).isSameAs(VALUE_1);
+    }
+
+    @Test
+    public void canListOptions() {
+        Set<Option<?>> list = allOpts.listOptions();
+        for (Option<?> opt : list) {
+            assertThat(opt).isAnyOf(OPTION_1, OPTION_1_A, OPTION_2);
+        }
+
+        assertThat(list).hasSize(3);
+    }
+
+    @Test
+    public void canCreateCopyOptionsBundle() {
+        OptionsBundle copyBundle = OptionsBundle.from(allOpts);
+
+        assertThat(copyBundle.containsOption(OPTION_1)).isTrue();
+        assertThat(copyBundle.containsOption(OPTION_1_A)).isTrue();
+        assertThat(copyBundle.containsOption(OPTION_2)).isTrue();
+    }
+
+    @Test
+    public void canFindPartialIds() {
+        allOpts.findOptions(
+                "option.1",
+                option -> {
+                    assertThat(option).isAnyOf(OPTION_1, OPTION_1_A);
+                    return true;
+                });
+    }
+
+    @Test
+    public void canStopSearchingAfterFirstMatch() {
+        AtomicInteger count = new AtomicInteger();
+        allOpts.findOptions(
+                "option",
+                option -> {
+                    count.getAndIncrement();
+                    return false;
+                });
+
+        assertThat(count.get()).isEqualTo(1);
+    }
+
+    @Test
+    public void canGetZeroResults_fromFind() {
+        AtomicInteger count = new AtomicInteger();
+        allOpts.findOptions(
+                "invalid_find_string",
+                option -> {
+                    count.getAndIncrement();
+                    return false;
+                });
+
+        assertThat(count.get()).isEqualTo(0);
+    }
+
+    @Test
+    public void canRetrieveValue_fromFindLambda() {
+        AtomicReference<Object> value = new AtomicReference<>(VALUE_MISSING);
+        allOpts.findOptions(
+                "option.2",
+                option -> {
+                    value.set(allOpts.retrieveOption(option));
+                    return true;
+                });
+
+        assertThat(value.get()).isSameAs(VALUE_2);
+    }
+
+    @Test
+    public void retrieveMissingOption_willThrow() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> {
+                    allOpts.retrieveOption(OPTION_MISSING);
+                });
+    }
+}
diff --git a/camera/gradle.properties b/camera/gradle.properties
new file mode 100644
index 0000000..c0384c8
--- /dev/null
+++ b/camera/gradle.properties
@@ -0,0 +1,8 @@
+# Improve build performance
+org.gradle.jvmargs=-Xmx6g -XX:ReservedCodeCacheSize=2g -Dfile.encoding=UTF-8
+org.gradle.parallel=true
+org.gradle.configureondemand=true
+org.gradle.caching=true
+# For offline storage of dependencies. This string should be considered a child
+# directory of the main directory defined by ${rootProject.projectDir}.
+offlineRepositoryRoot=offline-repository
diff --git a/camera/integration-tests/camera2interopburst/build.gradle b/camera/integration-tests/camera2interopburst/build.gradle
new file mode 100644
index 0000000..b4227dd5
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileOptions {
+        sourceCompatibility project.ext.javaVersion
+        targetCompatibility project.ext.javaVersion
+    }
+
+    compileSdkVersion project.ext.compileSdk
+
+    defaultConfig {
+        applicationId "androidx.camera.testapp.camera2interopburst"
+        minSdkVersion project.ext.minSdk
+        targetSdkVersion project.ext.targetSdk
+        versionCode 1
+        versionName project.ext.version
+        multiDexEnabled true
+    }
+
+    sourceSets {
+        main.manifest.srcFile 'src/main/AndroidManifest.xml'
+        main.java.srcDirs = ['src/main/java']
+        main.java.excludes = ['**/build/**']
+        main.java.includes = ['**/*.java']
+        main.res.srcDirs = ['src/main/res']
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation project(':camera2')
+    implementation project(':core')
+
+    // Lifecycle and LiveData
+    implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+    // Android Support Library
+    implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support.constraint:constraint-layout:1.0.2"
+    implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/camera2interopburst/src/main/AndroidManifest.xml b/camera/integration-tests/camera2interopburst/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ade8c53
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.testapp.camera2interopburst">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:theme="@style/Theme.AppCompat">
+
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/MainActivity.java b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/MainActivity.java
new file mode 100644
index 0000000..451221b
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/MainActivity.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.camera2interopburst;
+
+import android.Manifest;
+import android.app.Activity;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.MutableLiveData;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureRequest;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.camera.camera2.Camera2Configuration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * An activity using CameraX-Camera2 interop to capture a burst.
+ *
+ * <p>First, the activity uses CameraX to set up a ViewFinderUseCase and ImageAnalysisUseCase. The
+ * ImageAnalysisUseCase converts Image instances into Bitmap instances. During the setup, custom
+ * CameraCaptureSession.StateCallback and CameraCaptureSession.CaptureCallback instances are passed
+ * to CameraX. These callbacks enable the activity to get references to the CameraCaptureSession and
+ * repeating CaptureRequest created internally by CameraX.
+ *
+ * <p>Then, when the user clicks on the viewfinder, CameraX's repeating request is stopped and a new
+ * burst capture request is issued, using the active CameraCaptureSession still owned by CameraX.
+ * The images captured during the burst are shown in an overlay mosaic for visualization.
+ *
+ * <p>Finally, after the burst capture concludes, CameraX's previous repeating request is resumed,
+ * until the next time the user starts a burst.
+ */
+@SuppressWarnings("AndroidJdkLibsChecker") // CompletableFuture not generally available yet.
+public class MainActivity extends AppCompatActivity {
+    private static final String TAG = MainActivity.class.getName();
+    private static final int CAMERA_REQUEST_CODE = 101;
+    private static final int BURST_FRAME_COUNT = 30;
+    private static final int MOSAIC_ROWS = 3;
+    private static final int MOSAIC_COLS = BURST_FRAME_COUNT / MOSAIC_ROWS;
+    // TODO: Figure out dynamically to fill the screen, instead of hard-coding.
+    private static final int TILE_WIDTH = 102;
+    private static final int TILE_HEIGHT = 72;
+    ;
+
+    // Waiting for the permissions approval.
+    private final CompletableFuture<Integer> completableFuture = new CompletableFuture<>();
+
+    // For handling touch events on the TextureView.
+    private final View.OnTouchListener onTouchListener = new OnTouchListener();
+
+    // Tracks the burst state.
+    private final Object burstLock = new Object();
+    // Camera2 interop objects.
+    private final SessionUpdatingSessionStateCallback sessionStateCallback =
+            new SessionUpdatingSessionStateCallback();
+    private final RequestUpdatingSessionCaptureCallback sessionCaptureCallback =
+            new RequestUpdatingSessionCaptureCallback();
+    private final Object mosaicLock = new Object();
+    @GuardedBy("mosaicLock")
+    private final Bitmap mosaic =
+            Bitmap.createBitmap(
+                    MOSAIC_COLS * TILE_WIDTH, MOSAIC_ROWS * TILE_HEIGHT, Bitmap.Config.ARGB_8888);
+    private final MutableLiveData<Bitmap> analysisResult = new MutableLiveData<>();
+    @GuardedBy("burstLock")
+    private boolean burstInProgress = false;
+    @GuardedBy("burstLock")
+    private int burstFrameCount = 0;
+    // For visualizing the images captured in the burst.
+    private ImageView imageView;
+    // For running ops on a background thread.
+    private Handler backgroundHandler;
+    private HandlerThread backgroundHandlerThread;
+
+    private static void makePermissionRequest(Activity context) {
+        ActivityCompat.requestPermissions(
+                context, new String[]{Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle bundle) {
+        super.onCreate(bundle);
+        setContentView(R.layout.activity_main);
+
+        backgroundHandlerThread = new HandlerThread("Background");
+        backgroundHandlerThread.start();
+        backgroundHandler = new Handler(backgroundHandlerThread.getLooper());
+
+        new Thread(
+                () -> {
+                    setupCamera();
+                })
+                .start();
+        setupPermissions(this);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        backgroundHandler.removeCallbacksAndMessages(null);
+        backgroundHandlerThread.quitSafely();
+    }
+
+    @Override
+    protected void onPause() {
+        synchronized (burstLock) {
+            burstInProgress = false;
+        }
+        super.onPause();
+    }
+
+    private void setupCamera() {
+        try {
+            // Wait for permissions before proceeding.
+            if (completableFuture.get() == PackageManager.PERMISSION_DENIED) {
+                Log.e(TAG, "Permission to open camera denied.");
+                return;
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            Log.e(TAG, "Exception occurred getting permission future: " + e);
+        }
+        LifecycleOwner lifecycleOwner = this;
+
+        // Run this on the UI thread to manipulate the Textures & Views.
+        MainActivity.this.runOnUiThread(
+                () -> {
+                    imageView = findViewById(R.id.imageView);
+
+                    ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder =
+                            new ViewFinderUseCaseConfiguration.Builder()
+                                    .setTargetName("ViewFinder");
+
+                    new Camera2Configuration.Extender(viewFinderConfigBuilder)
+                            .setSessionStateCallback(sessionStateCallback)
+                            .setSessionCaptureCallback(sessionCaptureCallback);
+
+                    ViewFinderUseCaseConfiguration viewFinderConfig =
+                            viewFinderConfigBuilder.build();
+                    TextureView textureView = findViewById(R.id.textureView);
+                    textureView.setOnTouchListener(onTouchListener);
+                    ViewFinderUseCase viewFinderUseCase = new ViewFinderUseCase(viewFinderConfig);
+
+                    viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                            output -> {
+                                // If TextureView was already created, need to re-add it to change
+                                // the SurfaceTexture.
+                                ViewGroup v = (ViewGroup) textureView.getParent();
+                                v.removeView(textureView);
+                                v.addView(textureView);
+                                textureView.setSurfaceTexture(output.getSurfaceTexture());
+                            });
+
+                    CameraX.bindToLifecycle(lifecycleOwner, viewFinderUseCase);
+
+                    ImageAnalysisUseCaseConfiguration analysisConfig =
+                            new ImageAnalysisUseCaseConfiguration.Builder()
+                                    .setTargetName("ImageAnalysis")
+                                    .setCallbackHandler(backgroundHandler)
+                                    .build();
+                    ImageAnalysisUseCase analysisUseCase = new ImageAnalysisUseCase(analysisConfig);
+                    CameraX.bindToLifecycle(lifecycleOwner, analysisUseCase);
+                    analysisUseCase.setAnalyzer(
+                            (image, rotationDegrees) -> {
+                                analysisResult.postValue(convertYuv420ImageToBitmap(image));
+                            });
+                    analysisResult.observe(
+                            lifecycleOwner,
+                            bitmap -> {
+                                synchronized (burstLock) {
+                                    if (burstInProgress) {
+                                        // Update the mosaic.
+                                        insertIntoMosaic(bitmap, burstFrameCount++);
+                                        MainActivity.this.runOnUiThread(
+                                                () -> {
+                                                    synchronized (mosaicLock) {
+                                                        imageView.setImageBitmap(mosaic);
+                                                    }
+                                                });
+
+                                        // Detect the end of the burst.
+                                        if (burstFrameCount == BURST_FRAME_COUNT) {
+                                            burstInProgress = false;
+                                            submitRepeatingRequest();
+                                        }
+                                    }
+                                }
+                            });
+                });
+    }
+
+    private void setupPermissions(Activity context) {
+        int permission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA);
+        if (permission != PackageManager.PERMISSION_GRANTED) {
+            makePermissionRequest(context);
+        } else {
+            completableFuture.complete(permission);
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, String[] permissions, int[] grantResults) {
+        switch (requestCode) {
+            case CAMERA_REQUEST_CODE: {
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    Log.i(TAG, "Camera Permission Granted.");
+                } else {
+                    Log.i(TAG, "Camera Permission Denied.");
+                }
+                completableFuture.complete(grantResults[0]);
+                return;
+            }
+            default: {
+            }
+        }
+    }
+
+    private void submitCaptureBurstRequest() {
+        try {
+            // Use the existing session created by CameraX.
+            CameraCaptureSession session = sessionStateCallback.getSession();
+            // Use the previous request created by CameraX.
+            CaptureRequest request = sessionCaptureCallback.getRequest();
+            List<CaptureRequest> requests = new ArrayList<>(BURST_FRAME_COUNT);
+            for (int i = 0; i < BURST_FRAME_COUNT; ++i) {
+                requests.add(request);
+            }
+            session.captureBurst(requests, /*callback=*/ null, backgroundHandler);
+        } catch (CameraAccessException e) {
+            throw new RuntimeException("Could not submit the burst capture request.", e);
+        }
+    }
+
+    private void submitRepeatingRequest() {
+        try {
+            // Use the existing session created by CameraX.
+            CameraCaptureSession session = sessionStateCallback.getSession();
+            // Use the previous request created by CameraX.
+            CaptureRequest request = sessionCaptureCallback.getRequest();
+            // TODO: This capture callback is not the same as that used by CameraX internally.
+            // Find a way to use exactly that same callback.
+            session.setRepeatingRequest(request, sessionCaptureCallback, backgroundHandler);
+        } catch (CameraAccessException e) {
+            throw new RuntimeException("Could not submit the repeating request.", e);
+        }
+    }
+
+    // TODO: Do proper YUV420-to-RGB conversion, instead of just taking the Y channel and
+    // propagating it to all 3 channels.
+    private Bitmap convertYuv420ImageToBitmap(ImageProxy image) {
+        ImageProxy.PlaneProxy plane = image.getPlanes()[0];
+        ByteBuffer buffer = plane.getBuffer();
+        final int bytesCount = buffer.remaining();
+        byte[] imageBytes = new byte[bytesCount];
+        buffer.get(imageBytes);
+
+        // TODO: Reuse a bitmap from a pool.
+        Bitmap bitmap =
+                Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
+
+        int[] bitmapPixels = new int[bitmap.getWidth() * bitmap.getHeight()];
+        for (int row = 0; row < bitmap.getHeight(); ++row) {
+            int imageBytesPosition = row * plane.getRowStride();
+            int bitmapPixelsPosition = row * bitmap.getWidth();
+            for (int col = 0; col < bitmap.getWidth(); ++col) {
+                int channelValue = (imageBytes[imageBytesPosition++] & 0xFF);
+                bitmapPixels[bitmapPixelsPosition++] =
+                        Color.rgb(channelValue, channelValue, channelValue);
+            }
+        }
+        bitmap.setPixels(
+                bitmapPixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
+        return bitmap;
+    }
+
+    private void insertIntoMosaic(Bitmap bitmap, int position) {
+        // TODO: Reuse a bitmap from a pool.
+        Bitmap rescaledBitmap =
+                Bitmap.createScaledBitmap(bitmap, TILE_WIDTH, TILE_HEIGHT, /*filter=*/ false);
+
+        int tileRowOffset = (position / MOSAIC_COLS) * TILE_HEIGHT;
+        int tileColOffset = (position % MOSAIC_COLS) * TILE_WIDTH;
+        for (int row = 0; row < rescaledBitmap.getHeight(); ++row) {
+            for (int col = 0; col < rescaledBitmap.getWidth(); ++col) {
+                int color = rescaledBitmap.getPixel(col, row);
+                synchronized (mosaicLock) {
+                    mosaic.setPixel(col + tileColOffset, row + tileRowOffset, color);
+                }
+            }
+        }
+    }
+
+    /** An on-touch listener which submits a capture burst request when the view is touched. */
+    private class OnTouchListener implements View.OnTouchListener {
+        @Override
+        public boolean onTouch(View view, MotionEvent event) {
+            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                synchronized (burstLock) {
+                    if (!burstInProgress) {
+                        burstFrameCount = 0;
+                        synchronized (mosaicLock) {
+                            mosaic.eraseColor(0);
+                        }
+                        try {
+                            sessionStateCallback.getSession().stopRepeating();
+                        } catch (CameraAccessException e) {
+                            throw new RuntimeException("Could not stop the repeating request.", e);
+                        }
+                        submitCaptureBurstRequest();
+                        burstInProgress = true;
+                    }
+                }
+            }
+            return true;
+        }
+    }
+}
diff --git a/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/RequestUpdatingSessionCaptureCallback.java b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/RequestUpdatingSessionCaptureCallback.java
new file mode 100644
index 0000000..b548c41
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/RequestUpdatingSessionCaptureCallback.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.camera2interopburst;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+import android.view.Surface;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/** A capture session capture callback which updates a reference to the capture request. */
+final class RequestUpdatingSessionCaptureCallback extends CameraCaptureSession.CaptureCallback {
+    private final AtomicReference<CaptureRequest> request = new AtomicReference<>();
+
+    @Override
+    public void onCaptureBufferLost(
+            CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+    }
+
+    @Override
+    public void onCaptureCompleted(
+            CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+    }
+
+    @Override
+    public void onCaptureFailed(
+            CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+    }
+
+    @Override
+    public void onCaptureProgressed(
+            CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+    }
+
+    @Override
+    public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+    }
+
+    @Override
+    public void onCaptureSequenceCompleted(
+            CameraCaptureSession session, int sequenceId, long frame) {
+    }
+
+    @Override
+    public void onCaptureStarted(
+            CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+        this.request.set(request);
+    }
+
+    CaptureRequest getRequest() {
+        return request.get();
+    }
+}
diff --git a/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/SessionUpdatingSessionStateCallback.java b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/SessionUpdatingSessionStateCallback.java
new file mode 100644
index 0000000..3094c91
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/java/androidx/camera/testapp/camera2interopburst/SessionUpdatingSessionStateCallback.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.camera2interopburst;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.support.annotation.GuardedBy;
+import android.util.Log;
+import android.view.Surface;
+
+/** A capture session state callback which updates a reference to the capture session. */
+final class SessionUpdatingSessionStateCallback extends CameraCaptureSession.StateCallback {
+    private static final String TAG = "SessionUpdatingSessionStateCallback";
+
+    private final Object sessionLock = new Object();
+
+    @GuardedBy("sessionLock")
+    private CameraCaptureSession session;
+
+    @Override
+    public void onConfigured(CameraCaptureSession session) {
+        Log.d(TAG, "onConfigured: session=" + session);
+        synchronized (sessionLock) {
+            this.session = session;
+        }
+    }
+
+    @Override
+    public void onActive(CameraCaptureSession session) {
+        Log.d(TAG, "onActive: session=" + session);
+    }
+
+    @Override
+    public void onClosed(CameraCaptureSession session) {
+        Log.d(TAG, "onClosed: session=" + session);
+        synchronized (sessionLock) {
+            if (this.session == session) {
+                this.session = null;
+            }
+        }
+    }
+
+    @Override
+    public void onReady(CameraCaptureSession session) {
+        Log.d(TAG, "onReady: session=" + session);
+    }
+
+    @Override
+    public void onCaptureQueueEmpty(CameraCaptureSession session) {
+        Log.d(TAG, "onCaptureQueueEmpty: session=" + session);
+    }
+
+    @Override
+    public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+        Log.d(TAG, "onSurfacePrepared: session=" + session + ", surface=" + surface);
+    }
+
+    @Override
+    public void onConfigureFailed(CameraCaptureSession session) {
+        Log.d(TAG, "onConfigureFailed: session=" + session);
+        synchronized (sessionLock) {
+            if (this.session == session) {
+                this.session = null;
+            }
+        }
+    }
+
+    CameraCaptureSession getSession() {
+        synchronized (sessionLock) {
+            return session;
+        }
+    }
+}
diff --git a/camera/integration-tests/camera2interopburst/src/main/res/layout/activity_main.xml b/camera/integration-tests/camera2interopburst/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..6c5b897
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/res/layout/activity_main.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:layout_constraintHeight_min="640dp"
+    tools:context="androidx.camera.app.camera2interopburst.MainActivity">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <TextureView
+            android:id="@+id/textureView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:minHeight="177dp" />
+
+        <ImageView
+            android:id="@+id/imageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="0dp"
+            android:layout_marginTop="0dp"
+            android:elevation="2dp" />
+    </RelativeLayout>
+
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/camera2interopburst/src/main/res/values/strings.xml b/camera/integration-tests/camera2interopburst/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ff582fd
--- /dev/null
+++ b/camera/integration-tests/camera2interopburst/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <string name="app_name">Camera2 Interop Burst</string>
+</resources>
diff --git a/camera/integration-tests/camera2interoperror/build.gradle b/camera/integration-tests/camera2interoperror/build.gradle
new file mode 100644
index 0000000..15c740d
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileOptions {
+        sourceCompatibility project.ext.javaVersion
+        targetCompatibility project.ext.javaVersion
+    }
+
+    compileSdkVersion project.ext.compileSdk
+
+    defaultConfig {
+        applicationId "androidx.camera.testapp.camera2interoperror"
+        minSdkVersion project.ext.minSdk
+        targetSdkVersion project.ext.targetSdk
+        versionCode 1
+        versionName project.ext.version
+        multiDexEnabled true
+    }
+
+    sourceSets {
+        main.manifest.srcFile 'src/main/AndroidManifest.xml'
+        main.java.srcDirs = ['src/main/java']
+        main.java.excludes = ['**/build/**']
+        main.java.includes = ['**/*.java']
+        main.res.srcDirs = ['src/main/res']
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation project(':camera2')
+    implementation project(':core')
+
+    // Lifecycle and LiveData
+    implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+    // Android Support Library
+    implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support.constraint:constraint-layout:1.0.2"
+    implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/camera2interoperror/src/main/AndroidManifest.xml b/camera/integration-tests/camera2interoperror/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..94d0029
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.testapp.camera2interoperror">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application
+        android:name=".CameraXInteropErrorApplication"
+        android:theme="@style/AppTheme">
+        <activity
+            android:name=".CameraXInteropErrorActivity"
+            android:label="CameraX Camera2InteropError">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCase.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCase.java
new file mode 100644
index 0000000..c6374bb
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCase.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.camera2interoperror;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.media.ImageReader;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.util.Size;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.SessionConfiguration;
+
+import java.util.Collections;
+import java.util.Map;
+
+/** A use case which attempts to use camera2 calls directly in an erroneous manner. */
+public class Camera2InteropErrorUseCase extends BaseUseCase {
+    private static final String TAG = "Camera2InteropErrorUseCase";
+    private final Camera2InteropErrorUseCaseConfiguration configuration;
+    private final CameraCaptureSession.StateCallback captureSessionStateCallback =
+            new CameraCaptureSession.StateCallback() {
+                @Override
+                public void onConfigured(@NonNull CameraCaptureSession session) {
+                    Log.d(TAG, "CameraCaptureSession.StateCallback.onConfigured()");
+                }
+
+                @Override
+                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
+                    Log.d(TAG, "CameraCaptureSession.StateCallback.onConfigured()");
+                }
+            };
+    private CameraDevice cameraDevice;
+    private final CameraDevice.StateCallback stateCallback =
+            new StateCallback() {
+                @Override
+                public void onOpened(@NonNull CameraDevice camera) {
+                    Log.d(TAG, "CameraDevice.StateCallback.onOpened()");
+                    Camera2InteropErrorUseCase.this.cameraDevice = camera;
+                }
+
+                @Override
+                public void onDisconnected(@NonNull CameraDevice camera) {
+                    Log.d(TAG, "CameraDevice.StateCallback.onDisconnected()");
+                }
+
+                @Override
+                public void onError(@NonNull CameraDevice camera, int error) {
+                    Log.d(TAG, "CameraDevice.StateCallback.onError()");
+                }
+            };
+    private ImageReader imageReader;
+
+    public Camera2InteropErrorUseCase(Camera2InteropErrorUseCaseConfiguration configuration) {
+        super(configuration);
+        this.configuration = configuration;
+    }
+
+    /** Closes the {@link CameraDevice} obtained via callback. */
+    void closeCamera() {
+        if (cameraDevice != null) {
+            Log.d(TAG, "Closing CameraDevice.");
+            cameraDevice.close();
+        } else {
+            Log.d(TAG, "No CameraDevice to close.");
+        }
+    }
+
+    /**
+     * Opens a {@link CameraCaptureSession} using the {@link CameraDevice} obtained via callback.
+     */
+    void reopenCaptureSession() {
+        try {
+            Log.d(TAG, "Opening a CameraCaptureSession.");
+            cameraDevice.createCaptureSession(
+                    Collections.singletonList(imageReader.getSurface()),
+                    captureSessionStateCallback,
+                    null);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "no permission to create capture session");
+        }
+    }
+
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        imageReader = ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2);
+
+        imageReader.setOnImageAvailableListener(
+                imageReader -> {
+                    imageReader.acquireNextImage().close();
+                },
+                null);
+
+        SessionConfiguration.Builder sessionConfigBuilder = new SessionConfiguration.Builder();
+        sessionConfigBuilder.clearSurfaces();
+        sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        sessionConfigBuilder.setDeviceStateCallback(stateCallback);
+
+        try {
+            String cameraId = CameraX.getCameraWithLensFacing(configuration.getLensFacing());
+            attachToCamera(cameraId, sessionConfigBuilder.build());
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + configuration.getLensFacing(),
+                    e);
+        }
+
+        return suggestedResolutionMap;
+    }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java
new file mode 100644
index 0000000..e816458
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/Camera2InteropErrorUseCaseConfiguration.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.camera2interoperror;
+
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.ImageOutputConfiguration;
+import androidx.camera.core.MutableConfiguration;
+import androidx.camera.core.MutableOptionsBundle;
+import androidx.camera.core.OptionsBundle;
+import androidx.camera.core.UseCaseConfiguration;
+
+/** Configuration for the camera 2 interop case configuration */
+public class Camera2InteropErrorUseCaseConfiguration
+        implements UseCaseConfiguration<Camera2InteropErrorUseCase>,
+        CameraDeviceConfiguration,
+        ImageOutputConfiguration {
+
+    private final Configuration config;
+
+    private Camera2InteropErrorUseCaseConfiguration(Configuration config) {
+        this.config = config;
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** Builder for an empty Configuration */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<
+            Camera2InteropErrorUseCase,
+            Camera2InteropErrorUseCaseConfiguration,
+            Builder>,
+            CameraDeviceConfiguration.Builder<
+                    Camera2InteropErrorUseCaseConfiguration, Builder>,
+            ImageOutputConfiguration.Builder<
+                    Camera2InteropErrorUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle optionsBundle;
+
+        public Builder() {
+            optionsBundle = MutableOptionsBundle.create();
+            setTargetClass(Camera2InteropErrorUseCase.class);
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return optionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public Camera2InteropErrorUseCaseConfiguration build() {
+            return new Camera2InteropErrorUseCaseConfiguration(OptionsBundle.from(optionsBundle));
+        }
+    }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorActivity.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorActivity.java
new file mode 100644
index 0000000..8f3cc40
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorActivity.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.camera2interoperror;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraDevice.StateCallback;
+import android.hardware.camera2.CameraManager;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.util.Rational;
+import android.view.TextureView;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.legacy.app.ActivityCompat;
+
+import java.util.concurrent.CompletableFuture;
+
+/** for testing interop */
+@SuppressWarnings("AndroidJdkLibsChecker") // CompletableFuture not generally available yet.
+public class CameraXInteropErrorActivity extends AppCompatActivity
+        implements ActivityCompat.OnRequestPermissionsResultCallback {
+    private static final String TAG = "CameraXInteropErrorActivity";
+    private static final int PERMISSIONS_REQUEST_CODE = 42;
+    private static final Rational ASPECT_RATIO = new Rational(4, 3);
+
+    private final CompletableFuture<Boolean> completableFuture = new CompletableFuture<>();
+
+    /** The LensFacing to use. */
+    private LensFacing currentCameraLensFacing = LensFacing.BACK;
+
+    private String currentCameraFacingString = "BACK";
+    private ErrorType currentError = ErrorType.REOPEN_CAMERA;
+
+    /**
+     * Creates a view finder use case.
+     *
+     * <p>This use case observes a {@link SurfaceTexture}. The texture is connected to a {@link
+     * TextureView} to display a camera preview.
+     */
+    private void createViewFinderUseCase() {
+        ViewFinderUseCaseConfiguration configuration =
+                new ViewFinderUseCaseConfiguration.Builder()
+                        .setLensFacing(currentCameraLensFacing)
+                        .setTargetName("ViewFinder")
+                        .setTargetAspectRatio(ASPECT_RATIO)
+                        .build();
+        ViewFinderUseCase viewFinderUseCase = new ViewFinderUseCase(configuration);
+
+        viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    // If TextureView was already created, need to re-add it to change the
+                    // SurfaceTexture.
+                    TextureView textureView = findViewById(R.id.textureView);
+                    ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+                    viewGroup.removeView(textureView);
+                    viewGroup.addView(textureView);
+                    textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                });
+
+        CameraX.bindToLifecycle(/* lifecycleOwner= */ this, viewFinderUseCase);
+        Log.i(TAG, "Got UseCase: " + viewFinderUseCase);
+    }
+
+    void createBadUseCase() {
+        Camera2InteropErrorUseCaseConfiguration configuration =
+                new Camera2InteropErrorUseCaseConfiguration.Builder()
+                        .setLensFacing(currentCameraLensFacing)
+                        .setTargetName("Camera2InteropErrorUseCase")
+                        .setTargetAspectRatio(ASPECT_RATIO)
+                        .build();
+        Camera2InteropErrorUseCase camera2InteropErrorUseCase =
+                new Camera2InteropErrorUseCase(configuration);
+        CameraX.bindToLifecycle(this, camera2InteropErrorUseCase);
+
+        Button button = this.findViewById(R.id.CauseError);
+
+        button.setOnClickListener(
+                view -> {
+                    switch (currentError) {
+                        case CLOSE_DEVICE:
+                            camera2InteropErrorUseCase.closeCamera();
+                            break;
+                        case REOPEN_CAMERA:
+                            CameraManager manager =
+                                    (CameraManager)
+                                            getApplicationContext()
+                                                    .getSystemService(CAMERA_SERVICE);
+                            Log.d(TAG, "Attempting to reopen camera");
+                            try {
+                                String cameraId =
+                                        CameraX.getCameraWithLensFacing(currentCameraLensFacing);
+                                manager.openCamera(
+                                        cameraId,
+                                        new StateCallback() {
+                                            @Override
+                                            public void onOpened(@NonNull CameraDevice camera) {
+                                            }
+
+                                            @Override
+                                            public void onDisconnected(
+                                                    @NonNull CameraDevice camera) {
+                                            }
+
+                                            @Override
+                                            public void onError(
+                                                    @NonNull CameraDevice camera, int error) {
+                                            }
+                                        },
+                                        null);
+                                Log.d(TAG, "Looks like nothing overtly bad occurred");
+                            } catch (Exception e) {
+                                Log.e(TAG, e.getMessage());
+                                Log.e(TAG, "Should we do something here?");
+                            }
+                            break;
+                        case OPEN_CAPTURE_SESSION:
+                            camera2InteropErrorUseCase.reopenCaptureSession();
+                            break;
+                    }
+                });
+
+        TextView textView = this.findViewById(R.id.textView);
+        textView.setText(currentError.toString());
+
+        Button button1 = this.findViewById(R.id.SelectError);
+        button1.setOnClickListener(
+                view -> {
+                    switch (currentError) {
+                        case CLOSE_DEVICE:
+                            currentError = ErrorType.REOPEN_CAMERA;
+                            break;
+                        case REOPEN_CAMERA:
+                            currentError = ErrorType.OPEN_CAPTURE_SESSION;
+                            break;
+                        case OPEN_CAPTURE_SESSION:
+                            currentError = ErrorType.CLOSE_DEVICE;
+                            break;
+                    }
+                    textView.setText(currentError.toString());
+                });
+    }
+
+    /** Creates all the use cases. */
+    private void createUseCases() {
+        createViewFinderUseCase();
+        createBadUseCase();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_camera_2main);
+
+        // Get params from adb extra string
+        Bundle bundle = this.getIntent().getExtras();
+        if (bundle != null) {
+            currentCameraFacingString = bundle.getString("cameraFacing");
+        }
+
+        new Thread(
+                () -> {
+                    setupCamera();
+                })
+                .start();
+        setupPermissions();
+    }
+
+    private void setupCamera() {
+        try {
+            // Wait for permissions before proceeding.
+            if (!completableFuture.get()) {
+                Log.d(TAG, "Permissions denied.");
+                return;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Exception occurred getting permission future: " + e);
+        }
+
+        Log.d(TAG, "Camera Facing: " + currentCameraFacingString);
+        if (currentCameraFacingString.equalsIgnoreCase("BACK")) {
+            currentCameraLensFacing = LensFacing.BACK;
+        } else if (currentCameraFacingString.equalsIgnoreCase("FRONT")) {
+            currentCameraLensFacing = LensFacing.FRONT;
+        } else {
+            throw new RuntimeException("Invalid lens facing: " + currentCameraFacingString);
+        }
+
+        Log.d(TAG, "Using camera lens facing: " + currentCameraLensFacing);
+
+        // Run this on the UI thread to manipulate the Textures & Views.
+        CameraXInteropErrorActivity.this.runOnUiThread(
+                () -> {
+                    createUseCases();
+                });
+    }
+
+    private void setupPermissions() {
+        if (!allPermissionsGranted()) {
+            makePermissionRequest();
+        } else {
+            completableFuture.complete(true);
+        }
+    }
+
+    private void makePermissionRequest() {
+        ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSIONS_REQUEST_CODE);
+    }
+
+    /** Returns true if all the necessary permissions have been granted already. */
+    private boolean allPermissionsGranted() {
+        for (String permission : getRequiredPermissions()) {
+            if (ContextCompat.checkSelfPermission(this, permission)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** Tries to acquire all the necessary permissions through a dialog. */
+    private String[] getRequiredPermissions() {
+        PackageInfo info;
+        try {
+            info =
+                    getPackageManager()
+                            .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
+        } catch (NameNotFoundException exception) {
+            Log.e(TAG, "Failed to obtain all required permissions.", exception);
+            return new String[0];
+        }
+        String[] permissions = info.requestedPermissions;
+        if (permissions != null && permissions.length > 0) {
+            return permissions;
+        } else {
+            return new String[0];
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, String[] permissions, int[] grantResults) {
+        switch (requestCode) {
+            case PERMISSIONS_REQUEST_CODE: {
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    Log.d(TAG, "Permissions Granted.");
+                    completableFuture.complete(true);
+                } else {
+                    Log.d(TAG, "Permissions Denied.");
+                    completableFuture.complete(false);
+                }
+                return;
+            }
+            default:
+                // No-op
+        }
+    }
+
+    /** The types of errors that can be induced on CameraX. */
+    enum ErrorType {
+        /** Attempt to reopen the currently opened {@link CameraDevice}. */
+        REOPEN_CAMERA,
+        /** Close the {@link CameraDevice} without going through CameraX. */
+        CLOSE_DEVICE,
+        /**
+         * Open up a {@link android.hardware.camera2.CameraCaptureSession} using the {@link
+         * CameraDevice} obtained from {@link CameraDevice.StateCallback}.
+         */
+        OPEN_CAPTURE_SESSION
+    }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorApplication.java b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorApplication.java
new file mode 100644
index 0000000..0c9a731
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/java/androidx/camera/testapp/camera2interoperror/CameraXInteropErrorApplication.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.camera2interoperror;
+
+import android.app.Application;
+
+/** An application for CameraX. */
+public class CameraXInteropErrorApplication extends Application {
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+    }
+}
diff --git a/camera/integration-tests/camera2interoperror/src/main/res/layout/activity_camera_2main.xml b/camera/integration-tests/camera2interoperror/src/main/res/layout/activity_camera_2main.xml
new file mode 100644
index 0000000..bcbe212
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/res/layout/activity_camera_2main.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/constraintLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="androidx.camera.app.camera2interoperror.CameraXInteropErrorActivity">
+
+    <TextureView
+        android:id="@+id/textureView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="#FFF"
+        android:elevation="2dp"
+        android:scaleType="fitXY"
+        android:src="@android:drawable/btn_radio"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintDimensionRatio="4:3"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="1.0"
+        app:layout_constraintStart_toStartOf="@+id/guideline"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.84000003" />
+
+    <android.support.constraint.Guideline
+        android:id="@+id/guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="0dp"
+        app:layout_constraintGuide_percent="0.7" />
+
+    <android.support.constraint.Guideline
+        android:id="@+id/selecterror"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="0dp"
+        app:layout_constraintGuide_percent="0.1" />
+
+    <android.support.constraint.Guideline
+        android:id="@+id/causeerror"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="0dp"
+        app:layout_constraintGuide_percent="0.4" />
+
+    <Button
+        android:id="@+id/SelectError"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Select Error"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="@+id/selecterror"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.84000003" />
+
+    <Button
+        android:id="@+id/CauseError"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Cause Error"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="@+id/causeerror"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.84000003" />
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/camera2interoperror/src/main/res/values/strings.xml b/camera/integration-tests/camera2interoperror/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2df0565
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+</resources>
diff --git a/camera/integration-tests/camera2interoperror/src/main/res/values/style.xml b/camera/integration-tests/camera2interoperror/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/camera2interoperror/src/main/res/values/style.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+    </style>
+</resources>
diff --git a/camera/integration-tests/cameraview/src/build.gradle b/camera/integration-tests/cameraview/src/build.gradle
new file mode 100644
index 0000000..8b0d003
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/build.gradle
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileOptions {
+        sourceCompatibility project.ext.javaVersion
+        targetCompatibility project.ext.javaVersion
+    }
+
+    compileSdkVersion project.ext.compileSdk
+
+    defaultConfig {
+        applicationId "androidx.camera.testapp.cameraview"
+        minSdkVersion project.ext.minSdk
+        targetSdkVersion project.ext.targetSdk
+        versionCode 1
+        versionName project.ext.version
+        multiDexEnabled true
+    }
+
+    sourceSets {
+        main.manifest.srcFile 'src/main/AndroidManifest.xml'
+        main.java.srcDirs = ['src/main/java']
+        main.java.excludes = ['**/build/**']
+        main.java.includes = ['**/*.java']
+        main.res.srcDirs = ['src/main/res']
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+
+    // Internal libraries
+    implementation project(':camera2')
+    implementation project(':core')
+    implementation project(':view')
+
+    // Lifecycle and LiveData
+    implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+    // Android Support Library
+    implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support.constraint:constraint-layout:1.0.2"
+    implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+
+    // EXIF
+    api 'com.android.support:exifinterface:27.+'
+}
+
diff --git a/camera/integration-tests/cameraview/src/main/AndroidManifest.xml b/camera/integration-tests/cameraview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a27776d
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.testapp.cameraview">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <!-- For using the camera -->
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <!-- For saving to the SD Card -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application
+        android:label="@string/app_name"
+        android:largeHeap="true"
+        android:theme="@style/AppTheme">
+
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name"
+            android:screenOrientation="fullUser">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>
diff --git a/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/CaptureViewOnTouchListener.java b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/CaptureViewOnTouchListener.java
new file mode 100644
index 0000000..a917a66
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/CaptureViewOnTouchListener.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.app.cameraview;
+
+import android.content.Intent;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageCaptureUseCase.UseCaseError;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.view.CameraView;
+import androidx.camera.view.CameraView.CaptureMode;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * A {@link View.OnTouchListener} which converts a view's touches into camera actions.
+ *
+ * <p>The listener converts touches on a {@link View}, such as a button, into appropriate photo
+ * taking or video recording actions through a {@link CameraView}. A click is interpreted as a
+ * take-photo signal, while a long-press is interpreted as a record-video signal.
+ */
+class CaptureViewOnTouchListener
+        implements View.OnTouchListener, OnImageSavedListener, OnVideoSavedListener {
+    private static final String TAG = "CaptureViewOnTouchListener";
+
+    private static final String FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS";
+    private static final String PHOTO_EXTENSION = ".jpg";
+    private static final String VIDEO_EXTENSION = ".mp4";
+
+    private static final int TAP = 1;
+    private static final int HOLD = 2;
+    private static final int RELEASE = 3;
+
+    private final long longPress = ViewConfiguration.getLongPressTimeout();
+    private final CameraView cameraView;
+
+    // TODO: Use a Handler for a background thread, rather than running on the current (main)
+    // thread.
+    private final Handler handler =
+            new Handler() {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                        case TAP:
+                            onTap();
+                            break;
+                        case HOLD:
+                            onHold();
+                            if (cameraView.getMaxVideoDuration() > 0) {
+                                sendEmptyMessageDelayed(RELEASE, cameraView.getMaxVideoDuration());
+                            }
+                            break;
+                        case RELEASE:
+                            onRelease();
+                            break;
+                        default:
+                            // No op
+                    }
+                }
+            };
+
+    private long downEventTimestamp;
+    private Rect viewBoundsRect;
+
+    /** Creates a new listener which links to the given {@link CameraView}. */
+    CaptureViewOnTouchListener(CameraView cameraView) {
+        this.cameraView = cameraView;
+    }
+
+    /** Called when the user taps. */
+    void onTap() {
+        if (cameraView.getCaptureMode() == CaptureMode.IMAGE
+                || cameraView.getCaptureMode() == CaptureMode.MIXED) {
+            cameraView.takePicture(createNewFile(PHOTO_EXTENSION), this);
+        }
+    }
+
+    /** Called when the user holds (long presses). */
+    void onHold() {
+        if (cameraView.getCaptureMode() == CaptureMode.VIDEO
+                || cameraView.getCaptureMode() == CaptureMode.MIXED) {
+            cameraView.startRecording(createNewFile(VIDEO_EXTENSION), this);
+        }
+    }
+
+    /** Called when the user releases. */
+    void onRelease() {
+        if (cameraView.getCaptureMode() == CaptureMode.VIDEO
+                || cameraView.getCaptureMode() == CaptureMode.MIXED) {
+            cameraView.stopRecording();
+        }
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                downEventTimestamp = System.currentTimeMillis();
+                viewBoundsRect =
+                        new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+                handler.sendEmptyMessageDelayed(HOLD, longPress);
+                view.setPressed(true);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // If the user moves their finger off the button, trigger RELEASE
+                if (viewBoundsRect.contains(
+                        view.getLeft() + (int) event.getX(), view.getTop() + (int) event.getY())) {
+                    break;
+                }
+                // Fall-through
+            case MotionEvent.ACTION_CANCEL:
+                clearHandler();
+                if (deltaSinceDownEvent() > longPress
+                        && (cameraView.getMaxVideoDuration() <= 0
+                        || deltaSinceDownEvent() < cameraView.getMaxVideoDuration())) {
+                    handler.sendEmptyMessage(RELEASE);
+                }
+                view.setPressed(false);
+                break;
+            case MotionEvent.ACTION_UP:
+                clearHandler();
+                if (deltaSinceDownEvent() < longPress) {
+                    handler.sendEmptyMessage(TAP);
+                } else if ((cameraView.getMaxVideoDuration() <= 0
+                        || deltaSinceDownEvent() < cameraView.getMaxVideoDuration())) {
+                    handler.sendEmptyMessage(RELEASE);
+                }
+                view.setPressed(false);
+                break;
+            default:
+                // No op
+        }
+        return true;
+    }
+
+    private long deltaSinceDownEvent() {
+        return System.currentTimeMillis() - downEventTimestamp;
+    }
+
+    private void clearHandler() {
+        handler.removeMessages(TAP);
+        handler.removeMessages(HOLD);
+        handler.removeMessages(RELEASE);
+    }
+
+    private File createNewFile(String extension) {
+        // Use Locale.US to ensure we get ASCII digits
+        return new File(
+                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
+                new SimpleDateFormat(FILENAME, Locale.US).format(System.currentTimeMillis())
+                        + extension);
+    }
+
+    @Override
+    public void onImageSaved(File file) {
+        report("Picture saved to " + file.getAbsolutePath());
+
+        // Print out metadata about the picture
+        // TODO: Print out metadata to log once metadata is implemented
+
+        broadcastPicture(file);
+    }
+
+    @Override
+    public void onVideoSaved(File file) {
+        report("Video saved to " + file.getAbsolutePath());
+        broadcastVideo(file);
+    }
+
+    @Override
+    public void onError(UseCaseError useCaseError, String message, @Nullable Throwable cause) {
+        report("Failure");
+    }
+
+    @Override
+    public void onError(
+            VideoCaptureUseCase.UseCaseError useCaseError,
+            String message,
+            @Nullable Throwable cause) {
+        report("Failure");
+    }
+
+    private void report(String msg) {
+        Log.d(TAG, msg);
+        Toast.makeText(cameraView.getContext(), msg, Toast.LENGTH_SHORT).show();
+    }
+
+    private void broadcastPicture(File file) {
+        if (Build.VERSION.SDK_INT < 24) {
+            Intent intent = new Intent(Camera.ACTION_NEW_PICTURE);
+            intent.setData(Uri.fromFile(file));
+            cameraView.getContext().sendBroadcast(intent);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+            intent.setData(Uri.fromFile(file));
+            cameraView.getContext().sendBroadcast(intent);
+        }
+    }
+
+    private void broadcastVideo(File file) {
+        if (Build.VERSION.SDK_INT < 24) {
+            Intent intent = new Intent(Camera.ACTION_NEW_VIDEO);
+            intent.setData(Uri.fromFile(file));
+            cameraView.getContext().sendBroadcast(intent);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+            intent.setData(Uri.fromFile(file));
+            cameraView.getContext().sendBroadcast(intent);
+        }
+    }
+}
diff --git a/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainActivity.java b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainActivity.java
new file mode 100644
index 0000000..3f88be9
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainActivity.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.app.cameraview;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+/** The main activity. */
+public class MainActivity extends AppCompatActivity {
+    private static final String TAG = "MainActivity";
+    private static final String[] REQUIRED_PERMISSIONS =
+            new String[]{
+                    Manifest.permission.CAMERA,
+                    Manifest.permission.RECORD_AUDIO,
+                    Manifest.permission.WRITE_EXTERNAL_STORAGE
+            };
+    private static final int REQUEST_CODE_PERMISSIONS = 10;
+
+    private boolean checkedPermissions = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        if (null == savedInstanceState) {
+            if (allPermissionsGranted()) {
+                startCamera();
+            } else if (!checkedPermissions) {
+                requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
+                checkedPermissions = true;
+            }
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        if (requestCode == REQUEST_CODE_PERMISSIONS) {
+            if (allPermissionsGranted()) {
+                startCamera();
+            } else {
+                report("Permissions not granted by the user.");
+            }
+        }
+    }
+
+    private boolean allPermissionsGranted() {
+        for (String permission : REQUIRED_PERMISSIONS) {
+            if (ContextCompat.checkSelfPermission(this, permission)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void startCamera() {
+        getSupportFragmentManager()
+                .beginTransaction()
+                .replace(R.id.content, new MainFragment())
+                .commit();
+    }
+
+    private void report(String msg) {
+        Log.d(TAG, msg);
+        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
+    }
+}
diff --git a/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainFragment.java b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainFragment.java
new file mode 100644
index 0000000..2325ab1
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/java/androidx/camera/testapp/cameraview/MainFragment.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.app.cameraview;
+
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.CompoundButton;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.view.CameraView;
+import androidx.camera.view.CameraView.CaptureMode;
+import androidx.camera.view.CameraView.ScaleType;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+
+/** The main camera fragment. */
+public class MainFragment extends Fragment {
+    private static final String TAG = "MainFragment";
+
+    // Possible values for this intent key are the name values of CameraX.LensFacing encoded as
+    // strings (case-insensitive): "back", "front".
+    private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
+
+    // Possible values for this intent key are the name values of CameraView.CaptureMode encoded as
+    // strings (case-insensitive): "image", "video", "mixed"
+    private static final String INTENT_EXTRA_CAPTURE_MODE = "captureMode";
+
+    private View cameraHolder;
+    private CameraView cameraView;
+    private View captureView;
+    private CompoundButton modeButton;
+    @Nullable
+    private CompoundButton toggleCameraButton;
+    private CompoundButton toggleCropButton;
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        cameraHolder = view.findViewById(R.id.layout_camera);
+        cameraView = view.findViewById(R.id.camera);
+        toggleCameraButton = view.findViewById(R.id.toggle);
+        toggleCropButton = view.findViewById(R.id.toggle_crop);
+        captureView = cameraHolder.findViewById(R.id.capture);
+        if (cameraHolder == null) {
+            throw new IllegalStateException("No View found with id R.id.layout_camera");
+        }
+        if (cameraView == null) {
+            throw new IllegalStateException("No CameraView found with id R.id.camera");
+        }
+        if (captureView == null) {
+            throw new IllegalStateException("No CameraView found with id R.id.capture");
+        }
+
+        modeButton = cameraHolder.findViewById(R.id.mode);
+
+        if (modeButton == null) {
+            throw new IllegalStateException("No View found with id R.id.mode");
+        }
+
+        // Log the location of some views, so their locations can be used to perform some automated
+        // clicks in tests.
+        logCenterCoordinates(cameraView, "camera_view");
+        logCenterCoordinates(captureView, "capture");
+        logCenterCoordinates(toggleCameraButton, "toggle_camera");
+        logCenterCoordinates(toggleCropButton, "toggle_crop");
+        logCenterCoordinates(modeButton, "mode");
+
+        // Get extra option for setting initial camera direction
+        Bundle bundle = getActivity().getIntent().getExtras();
+        if (bundle != null) {
+            String cameraDirectionString = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION);
+            if (cameraDirectionString != null) {
+                LensFacing lensFacing = LensFacing.valueOf(cameraDirectionString.toUpperCase());
+                cameraView.setCameraByLensFacing(lensFacing);
+            }
+
+            String captureModeString = bundle.getString(INTENT_EXTRA_CAPTURE_MODE);
+            if (captureModeString != null) {
+                CaptureMode captureMode = CaptureMode.valueOf(captureModeString.toUpperCase());
+                cameraView.setCaptureMode(captureMode);
+            }
+        }
+    }
+
+    @Override
+    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
+        super.onViewStateRestored(savedInstanceState);
+
+        // Set the lifecycle that will be used to control the camera
+        cameraView.bindToLifecycle(getActivity());
+
+        cameraView.setPinchToZoomEnabled(true);
+        captureView.setOnTouchListener(new CaptureViewOnTouchListener(cameraView));
+
+        // Set clickable, Let the cameraView can be interacted by Voice Access
+        cameraView.setClickable(true);
+
+        if (toggleCameraButton != null) {
+            toggleCameraButton.setVisibility(
+                    (cameraView.hasCameraWithLensFacing(LensFacing.BACK)
+                            && cameraView.hasCameraWithLensFacing(LensFacing.FRONT))
+                            ? View.VISIBLE
+                            : View.INVISIBLE);
+            toggleCameraButton.setChecked(cameraView.getCameraLensFacing() == LensFacing.FRONT);
+        }
+
+        // Set listeners here, or else restoring state will trigger them.
+        if (toggleCameraButton != null) {
+            toggleCameraButton.setOnCheckedChangeListener(
+                    (b, checked) ->
+                            cameraView.setCameraByLensFacing(
+                                    checked ? LensFacing.FRONT : LensFacing.BACK));
+        }
+
+        toggleCropButton.setChecked(cameraView.getScaleType() == ScaleType.CENTER_CROP);
+        toggleCropButton.setOnCheckedChangeListener(
+                (b, checked) -> {
+                    if (checked) {
+                        cameraView.setScaleType(ScaleType.CENTER_CROP);
+                    } else {
+                        cameraView.setScaleType(ScaleType.CENTER_INSIDE);
+                    }
+                });
+
+        if (modeButton != null) {
+            updateModeButtonIcon();
+
+            modeButton.setOnClickListener(
+                    view -> {
+                        if (cameraView.isRecording()) {
+                            Toast.makeText(
+                                    getContext(),
+                                    "Can not switch mode during video recording.",
+                                    Toast.LENGTH_SHORT)
+                                    .show();
+                            return;
+                        }
+
+                        if (cameraView.getCaptureMode() == CaptureMode.MIXED) {
+                            cameraView.setCaptureMode(CaptureMode.IMAGE);
+                        } else if (cameraView.getCaptureMode() == CaptureMode.IMAGE) {
+                            cameraView.setCaptureMode(CaptureMode.VIDEO);
+                        } else {
+                            cameraView.setCaptureMode(CaptureMode.MIXED);
+                        }
+
+                        updateModeButtonIcon();
+                    });
+        }
+    }
+
+    private void updateModeButtonIcon() {
+        if (cameraView.getCaptureMode() == CaptureMode.MIXED) {
+            modeButton.setButtonDrawable(R.drawable.ic_photo_camera);
+        } else if (cameraView.getCaptureMode() == CaptureMode.IMAGE) {
+            modeButton.setButtonDrawable(R.drawable.ic_camera);
+        } else {
+            modeButton.setButtonDrawable(R.drawable.ic_videocam);
+        }
+    }
+
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater,
+            @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_main, container, false);
+    }
+
+    private void logCenterCoordinates(View view, String name) {
+        view.getViewTreeObserver()
+                .addOnGlobalLayoutListener(
+                        new ViewTreeObserver.OnGlobalLayoutListener() {
+                            @Override
+                            public void onGlobalLayout() {
+                                Rect rect = new Rect();
+                                view.getGlobalVisibleRect(rect);
+                                Log.d(
+                                        TAG,
+                                        "View "
+                                                + name
+                                                + " Center "
+                                                + rect.centerX()
+                                                + " "
+                                                + rect.centerY());
+                                File externalDir = getActivity().getExternalFilesDir(null);
+                                File logFile =
+                                        new File(externalDir, name + "_button_coordinates.txt");
+                                try (PrintStream stream = new PrintStream(logFile)) {
+                                    stream.print(rect.centerX() + " " + rect.centerY());
+                                } catch (IOException e) {
+                                    Log.e(TAG, "Could not save to " + logFile, e);
+                                }
+                            }
+                        });
+    }
+}
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/fullscreen_selector.xml b/camera/integration-tests/cameraview/src/main/res/drawable/fullscreen_selector.xml
new file mode 100644
index 0000000..ff39715
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/fullscreen_selector.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<selector
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:constantSize="true">
+    <item android:drawable="@drawable/ic_fullscreen_exit" android:state_checked="true" android:state_enabled="true" />
+    <item android:drawable="@drawable/ic_fullscreen" android:state_checked="false" android:state_enabled="true" />
+</selector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_camera.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_camera.xml
new file mode 100644
index 0000000..a35682e
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_camera.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+
+        android:fillColor="?android:attr/colorControlNormal"
+        android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen.xml
new file mode 100644
index 0000000..467ce90
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="?android:attr/colorControlNormal"
+        android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen_exit.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen_exit.xml
new file mode 100644
index 0000000..8ee9dfc
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_fullscreen_exit.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="?android:attr/colorControlNormal"
+        android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_photo_camera.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_photo_camera.xml
new file mode 100644
index 0000000..625e155
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_photo_camera.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/drawable/ic_videocam.xml b/camera/integration-tests/cameraview/src/main/res/drawable/ic_videocam.xml
new file mode 100644
index 0000000..f009891
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/drawable/ic_videocam.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M18,10.48V6c0,-1.1 -0.9,-2 -2,-2H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-4.48l4,3.98v-11L18,10.48zM16,9.69V18H4V6h12V9.69zM11.67,11l-2.5,3.72L7.5,12L5,16h10L11.67,11z" />
+</vector>
diff --git a/camera/integration-tests/cameraview/src/main/res/layout-land/fragment_main.xml b/camera/integration-tests/cameraview/src/main/res/layout-land/fragment_main.xml
new file mode 100644
index 0000000..ee0b902
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/layout-land/fragment_main.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/layout_camera"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+
+        <androidx.camera.view.CameraView
+            android:id="@+id/camera"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_gravity="center_vertical"
+            android:layout_weight="1" />
+
+        <LinearLayout
+            android:layout_width="125dp"
+            android:layout_height="match_parent"
+            android:layout_marginBottom="10dp"
+            android:layout_marginTop="10dp"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <CheckBox
+                android:id="@+id/toggle_crop"
+                android:layout_width="wrap_content"
+                android:layout_height="50dp"
+                android:button="@drawable/fullscreen_selector" />
+
+            <CheckBox
+                android:id="@+id/mode"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:button="@drawable/ic_photo_camera" />
+
+            <Button
+                android:id="@+id/capture"
+                style="?android:buttonBarButtonStyle"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_marginBottom="50dp"
+                android:layout_marginTop="50dp"
+                android:layout_weight="1"
+                android:text="@string/btn_capture" />
+
+            <CheckBox
+                android:id="@+id/toggle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/camera/integration-tests/cameraview/src/main/res/layout/activity_main.xml b/camera/integration-tests/cameraview/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..a88437d0
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/layout/activity_main.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity" />
diff --git a/camera/integration-tests/cameraview/src/main/res/layout/fragment_main.xml b/camera/integration-tests/cameraview/src/main/res/layout/fragment_main.xml
new file mode 100644
index 0000000..72f8ecb
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/layout_camera"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <androidx.camera.view.CameraView
+            android:id="@+id/camera"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_gravity="center_horizontal"
+            android:layout_weight="1" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="58dp"
+            android:layout_marginLeft="10dp"
+            android:layout_marginRight="10dp"
+            android:gravity="center">
+
+            <CheckBox
+                android:id="@+id/toggle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
+            <Button
+                android:id="@+id/capture"
+                style="?android:buttonBarButtonStyle"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_marginLeft="50dp"
+                android:layout_marginRight="50dp"
+                android:layout_weight="1"
+                android:text="@string/btn_capture" />
+
+            <CheckBox
+                android:id="@+id/mode"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="15dp"
+                android:button="@drawable/ic_photo_camera" />
+
+            <CheckBox
+                android:id="@+id/toggle_crop"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:button="@drawable/fullscreen_selector" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/camera/integration-tests/cameraview/src/main/res/values/ids.xml b/camera/integration-tests/cameraview/src/main/res/values/ids.xml
new file mode 100644
index 0000000..91326df
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/values/ids.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <item name="layout_camera" type="id" />
+    <item name="layout_permissions" type="id" />
+    <item name="request_permissions" type="id" />
+    <item name="camera" type="id" />
+    <item name="capture" type="id" />
+    <item name="duration" type="id" />
+    <item name="progress" type="id" />
+    <item name="toggle" type="id" />
+    <item name="cancel" type="id" />
+    <item name="confirm" type="id" />
+</resources>
diff --git a/camera/integration-tests/cameraview/src/main/res/values/strings.xml b/camera/integration-tests/cameraview/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ca95f1a
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+
+    <string name="app_name">CameraView Demo</string>
+
+    <string name="view_finder">Viewfinder</string>
+    <string name="btn_capture">Capture</string>
+    <string name="btn_confirm">Confirm</string>
+    <string name="btn_cancel">Cancel</string>
+
+</resources>
diff --git a/camera/integration-tests/cameraview/src/main/res/values/styles.xml b/camera/integration-tests/cameraview/src/main/res/values/styles.xml
new file mode 100644
index 0000000..2d26a15
--- /dev/null
+++ b/camera/integration-tests/cameraview/src/main/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.NoActionBar" />
+
+</resources>
diff --git a/camera/integration-tests/hellocamerax/build.gradle b/camera/integration-tests/hellocamerax/build.gradle
new file mode 100644
index 0000000..72f4a6b
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileOptions {
+        sourceCompatibility project.ext.javaVersion
+        targetCompatibility project.ext.javaVersion
+    }
+
+    compileSdkVersion project.ext.compileSdk
+
+    defaultConfig {
+        applicationId "androidx.camera.testapp.hellocamerax"
+        minSdkVersion project.ext.minSdk
+        targetSdkVersion project.ext.targetSdk
+        versionCode 1
+        versionName project.ext.version
+        multiDexEnabled true
+    }
+
+    sourceSets {
+        main.manifest.srcFile 'src/main/AndroidManifest.xml'
+        main.java.srcDirs = ['src/main/java']
+        main.java.excludes = ['**/build/**']
+        main.java.includes = ['**/*.java']
+        main.res.srcDirs = ['src/main/res']
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation project(':camera2')
+    implementation project(':core')
+
+    // Lifecycle and LiveData
+    implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+    // Android Support Library
+    implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support.constraint:constraint-layout:1.0.2"
+    implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/hellocamerax/src/main/AndroidManifest.xml b/camera/integration-tests/hellocamerax/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c1178c1
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.testapp.hellocamerax">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application
+        android:name=".CameraXApplication"
+        android:theme="@style/AppTheme">
+        <activity
+            android:name=".CameraXActivity"
+            android:label="Hello CameraX">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXActivity.java b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXActivity.java
new file mode 100644
index 0000000..42d095e
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXActivity.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.hellocamerax;
+
+import android.arch.lifecycle.MutableLiveData;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.StrictMode;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.legacy.app.ActivityCompat;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * An activity with four use cases: (1) view finder, (2) image capture, (3) image analysis, (4)
+ * video capture.
+ *
+ * <p>All four use cases are created with CameraX and tied to the activity's lifecycle. CameraX
+ * automatically connects and disconnects the use cases from the camera in response to changes in
+ * the activity's lifecycle. Therefore, the use cases function properly when the app is paused and
+ * resumed and when the device is rotated. The complex interactions between the camera and these
+ * lifecycle events are handled internally by CameraX.
+ */
+public class CameraXActivity extends AppCompatActivity
+        implements ActivityCompat.OnRequestPermissionsResultCallback {
+    private static final String TAG = "CameraXActivity";
+    private static final int PERMISSIONS_REQUEST_CODE = 42;
+    // Possible values for this intent key: "backward" or "forward".
+    private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
+
+    private final SettableCallable<Boolean> settableResult = new SettableCallable<>();
+    private final FutureTask<Boolean> completableFuture = new FutureTask<>(settableResult);
+    private final AtomicLong imageAnalysisFrameCount = new AtomicLong(0);
+    private final MutableLiveData<String> imageAnalysisResult = new MutableLiveData<>();
+    private VideoFileSaver videoFileSaver;
+    /** The cameraId to use. Assume that 0 is the typical back facing camera. */
+    private LensFacing currentCameraLensFacing = LensFacing.BACK;
+
+    // TODO: Move the analysis processing, capture processing to separate threads, so
+    // there is smaller impact on the preview.
+    private String currentCameraDirection = "BACKWARD";
+    private ViewFinderUseCase viewFinderUseCase;
+    private ImageAnalysisUseCase imageAnalysisUseCase;
+    private ImageCaptureUseCase imageCaptureUseCase;
+    private VideoCaptureUseCase videoCaptureUseCase;
+
+    /**
+     * Creates a view finder use case.
+     *
+     * <p>This use case observes a {@link SurfaceTexture}. The texture is connected to a {@link
+     * TextureView} to display a camera preview.
+     */
+    private void createViewFinderUseCase() {
+        Button button = this.findViewById(R.id.PreviewToggle);
+        button.setBackgroundColor(Color.LTGRAY);
+        enableViewFinderUseCase();
+
+        button.setOnClickListener(
+                view -> {
+                    Button buttonView = (Button) view;
+                    if (viewFinderUseCase != null) {
+                        // Remove the use case
+                        buttonView.setBackgroundColor(Color.RED);
+                        CameraX.unbind(viewFinderUseCase);
+                        viewFinderUseCase = null;
+                    } else {
+                        // Add the use case
+                        buttonView.setBackgroundColor(Color.LTGRAY);
+
+                        enableViewFinderUseCase();
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + viewFinderUseCase);
+    }
+
+    void enableViewFinderUseCase() {
+        ViewFinderUseCaseConfiguration configuration =
+                new ViewFinderUseCaseConfiguration.Builder()
+                        .setLensFacing(currentCameraLensFacing)
+                        .setTargetName("ViewFinder")
+                        .build();
+
+        viewFinderUseCase = new ViewFinderUseCase(configuration);
+        TextureView textureView = findViewById(R.id.textureView);
+        viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    // If TextureView was already created, need to re-add it to change the
+                    // SurfaceTexture.
+                    ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+                    viewGroup.removeView(textureView);
+                    viewGroup.addView(textureView);
+                    textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                });
+
+        if (!bindToLifecycleSafely(viewFinderUseCase, R.id.PreviewToggle)) {
+            viewFinderUseCase = null;
+            return;
+        }
+
+        transformPreview();
+
+        textureView.setSurfaceTextureListener(
+                new SurfaceTextureListener() {
+                    @Override
+                    public void onSurfaceTextureAvailable(
+                            SurfaceTexture surfaceTexture, int i, int i1) {
+                    }
+
+                    @Override
+                    public void onSurfaceTextureSizeChanged(
+                            SurfaceTexture surfaceTexture, int i, int i1) {
+                        transformPreview();
+                    }
+
+                    @Override
+                    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+                        return false;
+                    }
+
+                    @Override
+                    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+                    }
+                });
+    }
+
+    void transformPreview() {
+        String cameraId = null;
+        LensFacing viewFinderLensFacing =
+                ((CameraDeviceConfiguration) viewFinderUseCase.getUseCaseConfiguration())
+                        .getLensFacing(/*valueIfMissing=*/ null);
+        if (viewFinderLensFacing != currentCameraLensFacing) {
+            throw new IllegalStateException(
+                    "Invalid view finder lens facing: "
+                            + viewFinderLensFacing
+                            + " Should be: "
+                            + currentCameraLensFacing);
+        }
+        try {
+            cameraId = CameraX.getCameraWithLensFacing(viewFinderLensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to get camera id for lens facing " + viewFinderLensFacing, e);
+        }
+        Size srcResolution = viewFinderUseCase.getAttachedSurfaceResolution(cameraId);
+
+        if (srcResolution.getWidth() == 0 || srcResolution.getHeight() == 0) {
+            return;
+        }
+
+        TextureView textureView = this.findViewById(R.id.textureView);
+
+        if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
+            return;
+        }
+
+        Matrix matrix = new Matrix();
+
+        int left = textureView.getLeft();
+        int right = textureView.getRight();
+        int top = textureView.getTop();
+        int bottom = textureView.getBottom();
+
+        // Compute the viewfinder ui size based on the available width, height, and ui orientation.
+        int viewWidth = (right - left);
+        int viewHeight = (bottom - top);
+
+        int displayRotation = getDisplayRotation();
+        Size scaled =
+                calculateViewfinderViewDimens(
+                        srcResolution, viewWidth, viewHeight, displayRotation);
+
+        // Compute the center of the view.
+        int centerX = viewWidth / 2;
+        int centerY = viewHeight / 2;
+
+        // Do corresponding rotation to correct the preview direction
+        matrix.postRotate(-getDisplayRotation(), centerX, centerY);
+
+        // Compute the scale value for center crop mode
+        float xScale = scaled.getWidth() / (float) viewWidth;
+        float yScale = scaled.getHeight() / (float) viewHeight;
+
+        if (getDisplayRotation() == 90 || getDisplayRotation() == 270) {
+            xScale = scaled.getWidth() / (float) viewHeight;
+            yScale = scaled.getHeight() / (float) viewWidth;
+        }
+
+        // Only two digits after the decimal point are valid for postScale. Need to get ceiling of
+        // two
+        // digits floating value to do the scale operation. Otherwise, the result may be scaled not
+        // large enough and will have some blank lines on the screen.
+        xScale = new BigDecimal(xScale).setScale(2, BigDecimal.ROUND_CEILING).floatValue();
+        yScale = new BigDecimal(yScale).setScale(2, BigDecimal.ROUND_CEILING).floatValue();
+
+        // Do corresponding scale to resolve the deformation problem
+        matrix.postScale(xScale, yScale, centerX, centerY);
+
+        // Compute the new left/top positions to do translate
+        int layoutL = centerX - (scaled.getWidth() / 2);
+        int layoutT = centerY - (scaled.getHeight() / 2);
+
+        // Do corresponding translation to be center crop
+        matrix.postTranslate(layoutL, layoutT);
+
+        textureView.setTransform(matrix);
+    }
+
+    /** @return One of 0, 90, 180, 270. */
+    private int getDisplayRotation() {
+        int displayRotation = getWindowManager().getDefaultDisplay().getRotation();
+
+        switch (displayRotation) {
+            case Surface.ROTATION_0:
+                displayRotation = 0;
+                break;
+            case Surface.ROTATION_90:
+                displayRotation = 90;
+                break;
+            case Surface.ROTATION_180:
+                displayRotation = 180;
+                break;
+            case Surface.ROTATION_270:
+                displayRotation = 270;
+                break;
+            default:
+                throw new UnsupportedOperationException(
+                        "Unsupported display rotation: " + displayRotation);
+        }
+
+        return displayRotation;
+    }
+
+    private Size calculateViewfinderViewDimens(
+            Size srcSize, int parentWidth, int parentHeight, int displayRotation) {
+        int inWidth = srcSize.getWidth();
+        int inHeight = srcSize.getHeight();
+        if (displayRotation == 0 || displayRotation == 180) {
+            // Need to reverse the width and height since we're in landscape orientation.
+            inWidth = srcSize.getHeight();
+            inHeight = srcSize.getWidth();
+        }
+
+        int outWidth = parentWidth;
+        int outHeight = parentHeight;
+        if (inWidth != 0 && inHeight != 0) {
+            float vfRatio = inWidth / (float) inHeight;
+            float parentRatio = parentWidth / (float) parentHeight;
+
+            // Match shortest sides together.
+            if (vfRatio < parentRatio) {
+                outWidth = parentWidth;
+                outHeight = Math.round(parentWidth / vfRatio);
+            } else {
+                outWidth = Math.round(parentHeight * vfRatio);
+                outHeight = parentHeight;
+            }
+        }
+
+        return new Size(outWidth, outHeight);
+    }
+
+    /**
+     * Creates an image analysis use case.
+     *
+     * <p>This use case observes a stream of analysis results computed from the frames.
+     */
+    private void createImageAnalysisUseCase() {
+        Button button = this.findViewById(R.id.AnalysisToggle);
+        button.setBackgroundColor(Color.LTGRAY);
+        enableImageAnalysisUseCase();
+
+        button.setOnClickListener(
+                view -> {
+                    Button buttonView = (Button) view;
+                    if (imageAnalysisUseCase != null) {
+                        // Remove the use case
+                        buttonView.setBackgroundColor(Color.RED);
+                        CameraX.unbind(imageAnalysisUseCase);
+                        imageAnalysisUseCase = null;
+                    } else {
+                        // Add the use case
+                        buttonView.setBackgroundColor(Color.LTGRAY);
+                        enableImageAnalysisUseCase();
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + imageAnalysisUseCase);
+    }
+
+    void enableImageAnalysisUseCase() {
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder()
+                        .setLensFacing(currentCameraLensFacing)
+                        .setTargetName("ImageAnalysis")
+                        .setCallbackHandler(new Handler(Looper.getMainLooper()))
+                        .build();
+
+        imageAnalysisUseCase = new ImageAnalysisUseCase(configuration);
+
+        TextView textView = this.findViewById(R.id.textView);
+
+        if (!bindToLifecycleSafely(imageAnalysisUseCase, R.id.AnalysisToggle)) {
+            imageAnalysisUseCase = null;
+            return;
+        }
+
+        imageAnalysisUseCase.setAnalyzer(
+                (image, rotationDegrees) -> {
+                    // Since we set the callback handler to a main thread handler, we can call
+                    // setValue()
+                    // here. If we weren't on the main thread, we would have to call postValue()
+                    // instead.
+                    imageAnalysisResult.setValue(Long.toString(image.getTimestamp()));
+                });
+        imageAnalysisResult.observe(
+                this,
+                text -> {
+                    if (imageAnalysisFrameCount.getAndIncrement() % 30 == 0) {
+                        textView.setText(
+                                "ImgCount: " + imageAnalysisFrameCount.get() + " @ts: " + text);
+                    }
+                });
+    }
+
+    /**
+     * Creates an image capture use case.
+     *
+     * <p>This use case takes a picture and saves it to a file, whenever the user clicks a button.
+     */
+    private void createImageCaptureUseCase() {
+
+        Button button = this.findViewById(R.id.PhotoToggle);
+        button.setBackgroundColor(Color.LTGRAY);
+        enableImageCaptureUseCase();
+
+        button.setOnClickListener(
+                view -> {
+                    Button buttonView = (Button) view;
+                    if (imageCaptureUseCase != null) {
+                        // Remove the use case
+                        buttonView.setBackgroundColor(Color.RED);
+                        disableImageCaptureUseCase();
+                    } else {
+                        // Add the use case
+                        buttonView.setBackgroundColor(Color.LTGRAY);
+                        enableImageCaptureUseCase();
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + imageCaptureUseCase);
+    }
+
+    void enableImageCaptureUseCase() {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder()
+                        .setLensFacing(currentCameraLensFacing)
+                        .setTargetName("ImageCapture")
+                        .build();
+
+        imageCaptureUseCase = new ImageCaptureUseCase(configuration);
+
+        if (!bindToLifecycleSafely(imageCaptureUseCase, R.id.PhotoToggle)) {
+            Button button = this.findViewById(R.id.Picture);
+            button.setOnClickListener(null);
+            imageCaptureUseCase = null;
+            return;
+        }
+
+        Button button = this.findViewById(R.id.Picture);
+        final Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS");
+        final File dir = this.getExternalFilesDir(null);
+        button.setOnClickListener(
+                view -> {
+                    imageCaptureUseCase.takePicture(
+                            new File(
+                                    dir,
+                                    formatter.format(Calendar.getInstance().getTime()) + ".jpg"),
+                            new ImageCaptureUseCase.OnImageSavedListener() {
+                                @Override
+                                public void onImageSaved(File file) {
+                                    Log.d(TAG, "Saved image to " + file);
+                                }
+
+                                @Override
+                                public void onError(
+                                        ImageCaptureUseCase.UseCaseError useCaseError,
+                                        String message,
+                                        Throwable cause) {
+                                    Log.e(TAG, "Failed to save image.", cause);
+                                }
+                            });
+                });
+    }
+
+    void disableImageCaptureUseCase() {
+        CameraX.unbind(imageCaptureUseCase);
+
+        imageCaptureUseCase = null;
+        Button button = this.findViewById(R.id.Picture);
+        button.setOnClickListener(null);
+    }
+
+    /**
+     * Creates a video capture use case.
+     *
+     * <p>This use case records a video segment and saves it to a file, in response to user button
+     * clicks.
+     */
+    private void createVideoCaptureUseCase() {
+        Button button = this.findViewById(R.id.VideoToggle);
+        button.setBackgroundColor(Color.LTGRAY);
+        enableVideoCaptureUseCase();
+
+        videoFileSaver = new VideoFileSaver();
+        videoFileSaver.setRootDirectory(
+                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+
+        button.setOnClickListener(
+                view -> {
+                    Button buttonView = (Button) view;
+                    if (videoCaptureUseCase != null) {
+                        // Remove the use case
+                        buttonView.setBackgroundColor(Color.RED);
+                        disableVideoCaptureUseCase();
+                    } else {
+                        // Add the use case
+                        buttonView.setBackgroundColor(Color.LTGRAY);
+                        enableVideoCaptureUseCase();
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + videoCaptureUseCase);
+    }
+
+    void enableVideoCaptureUseCase() {
+        VideoCaptureUseCaseConfiguration configuration =
+                new VideoCaptureUseCaseConfiguration.Builder()
+                        .setLensFacing(currentCameraLensFacing)
+                        .setTargetName("VideoCapture")
+                        .build();
+
+        videoCaptureUseCase = new VideoCaptureUseCase(configuration);
+
+        if (!bindToLifecycleSafely(videoCaptureUseCase, R.id.VideoToggle)) {
+            Button button = this.findViewById(R.id.Video);
+            button.setOnClickListener(null);
+            videoCaptureUseCase = null;
+            return;
+        }
+
+        Button button = this.findViewById(R.id.Video);
+        button.setOnClickListener(
+                view -> {
+                    Button buttonView = (Button) view;
+                    String text = button.getText().toString();
+                    if (text.equals("Record") && !videoFileSaver.isSaving()) {
+                        videoCaptureUseCase.startRecording(
+                                videoFileSaver.getNewVideoFile(), videoFileSaver);
+                        videoFileSaver.setSaving();
+                        buttonView.setText("Stop");
+                    } else if (text.equals("Stop") && videoFileSaver.isSaving()) {
+                        buttonView.setText("Record");
+                        videoCaptureUseCase.stopRecording();
+                    } else if (text.equals("Record") && videoFileSaver.isSaving()) {
+                        buttonView.setText("Stop");
+                        videoFileSaver.setSaving();
+                    } else if (text.equals("Stop") && !videoFileSaver.isSaving()) {
+                        buttonView.setText("Record");
+                    }
+                });
+    }
+
+    void disableVideoCaptureUseCase() {
+        Button button = this.findViewById(R.id.Video);
+        button.setOnClickListener(null);
+        CameraX.unbind(videoCaptureUseCase);
+
+        videoCaptureUseCase = null;
+    }
+
+    /** Creates all the use cases. */
+    private void createUseCases() {
+        createImageCaptureUseCase();
+        createViewFinderUseCase();
+        createImageAnalysisUseCase();
+        createVideoCaptureUseCase();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_camera_xmain);
+
+        StrictMode.VmPolicy policy =
+                new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
+        StrictMode.setVmPolicy(policy);
+
+        // Get params from adb extra string
+        Bundle bundle = this.getIntent().getExtras();
+        if (bundle != null) {
+            String newCameraDirection = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION);
+            if (newCameraDirection != null) {
+                currentCameraDirection = newCameraDirection;
+            }
+        }
+
+        new Thread(
+                () -> {
+                    setupCamera();
+                })
+                .start();
+        setupPermissions();
+    }
+
+    private void setupCamera() {
+        try {
+            // Wait for permissions before proceeding.
+            if (!completableFuture.get()) {
+                Log.d(TAG, "Permissions denied.");
+                return;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Exception occurred getting permission future: " + e);
+        }
+
+        Log.d(TAG, "Camera direction: " + currentCameraDirection);
+        if (currentCameraDirection.equalsIgnoreCase("BACKWARD")) {
+            currentCameraLensFacing = LensFacing.BACK;
+        } else if (currentCameraDirection.equalsIgnoreCase("FORWARD")) {
+            currentCameraLensFacing = LensFacing.FRONT;
+        } else {
+            throw new RuntimeException("Invalid camera direction: " + currentCameraDirection);
+        }
+        Log.d(TAG, "Using camera lens facing: " + currentCameraLensFacing);
+
+        // Run this on the UI thread to manipulate the Textures & Views.
+        CameraXActivity.this.runOnUiThread(
+                () -> {
+                    createUseCases();
+                });
+    }
+
+    private void setupPermissions() {
+        if (!allPermissionsGranted()) {
+            makePermissionRequest();
+        } else {
+            settableResult.set(true);
+            completableFuture.run();
+        }
+    }
+
+    private void makePermissionRequest() {
+        ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSIONS_REQUEST_CODE);
+    }
+
+    /** Returns true if all the necessary permissions have been granted already. */
+    private boolean allPermissionsGranted() {
+        for (String permission : getRequiredPermissions()) {
+            if (ContextCompat.checkSelfPermission(this, permission)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** Tries to acquire all the necessary permissions through a dialog. */
+    private String[] getRequiredPermissions() {
+        PackageInfo info;
+        try {
+            info =
+                    getPackageManager()
+                            .getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
+        } catch (NameNotFoundException exception) {
+            Log.e(TAG, "Failed to obtain all required permissions.", exception);
+            return new String[0];
+        }
+        String[] permissions = info.requestedPermissions;
+        if (permissions != null && permissions.length > 0) {
+            return permissions;
+        } else {
+            return new String[0];
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, String[] permissions, int[] grantResults) {
+        switch (requestCode) {
+            case PERMISSIONS_REQUEST_CODE: {
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    Log.d(TAG, "Permissions Granted.");
+                    settableResult.set(true);
+                    completableFuture.run();
+                } else {
+                    Log.d(TAG, "Permissions Denied.");
+                    settableResult.set(false);
+                    completableFuture.run();
+                }
+                return;
+            }
+            default:
+                // No-op
+        }
+    }
+
+    private boolean bindToLifecycleSafely(BaseUseCase useCase, int buttonViewId) {
+        try {
+            CameraX.bindToLifecycle(this, useCase);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, e.getMessage());
+            Toast.makeText(getApplicationContext(), "Bind too many use cases.", Toast.LENGTH_SHORT)
+                    .show();
+            Button button = this.findViewById(buttonViewId);
+            button.setBackgroundColor(Color.RED);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** A {@link Callable} whose return value can be set. */
+    private static final class SettableCallable<V> implements Callable<V> {
+        private final AtomicReference<V> value = new AtomicReference<>();
+
+        public void set(V value) {
+            this.value.set(value);
+        }
+
+        @Override
+        public V call() {
+            return value.get();
+        }
+    }
+}
diff --git a/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXApplication.java b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXApplication.java
new file mode 100644
index 0000000..0ff2227
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/CameraXApplication.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.hellocamerax;
+
+import android.app.Application;
+
+/** An application for CameraX. */
+public class CameraXApplication extends Application {
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+    }
+}
diff --git a/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/VideoFileSaver.java b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/VideoFileSaver.java
new file mode 100644
index 0000000..c95858b
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/java/androidx/camera/testapp/hellocamerax/VideoFileSaver.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.hellocamerax;
+
+import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.core.VideoCaptureUseCase.UseCaseError;
+
+import java.io.File;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Basic functionality required for interfacing the {@link
+ * androidx.camera.core.VideoCaptureUseCase}.
+ */
+public class VideoFileSaver implements OnVideoSavedListener {
+    private static final String TAG = "VideoFileSaver";
+    private final Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.ENGLISH);
+    private final Object lock = new Object();
+    private File rootDirectory;
+    @GuardedBy("lock")
+    private boolean isSaving = false;
+
+    @Override
+    public void onVideoSaved(File file) {
+
+        Log.d(TAG, "Saved file: " + file.getPath());
+        synchronized (lock) {
+            isSaving = false;
+        }
+    }
+
+    @Override
+    public void onError(UseCaseError useCaseError, String message, @Nullable Throwable cause) {
+
+        Log.e(TAG, "Error: " + useCaseError + ", " + message);
+        if (cause != null) {
+            Log.e(TAG, "Error cause: " + cause.getCause());
+        }
+
+        synchronized (lock) {
+            isSaving = false;
+        }
+    }
+
+    /** Returns a new {@link File} where to save a video. */
+    public File getNewVideoFile() {
+        Date date = Calendar.getInstance().getTime();
+        File file = new File(rootDirectory + "/" + formatter.format(date) + ".mp4");
+        return file;
+    }
+
+    /** Sets the directory for saving files. */
+    public void setRootDirectory(File rootDirectory) {
+        this.rootDirectory = rootDirectory;
+    }
+
+    boolean isSaving() {
+        synchronized (lock) {
+            return isSaving;
+        }
+    }
+
+    /** Sets saving state after video startRecording */
+    void setSaving() {
+        synchronized (lock) {
+            isSaving = true;
+        }
+    }
+}
diff --git a/camera/integration-tests/hellocamerax/src/main/res/layout/activity_camera_xmain.xml b/camera/integration-tests/hellocamerax/src/main/res/layout/activity_camera_xmain.xml
new file mode 100644
index 0000000..eddc73d
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/res/layout/activity_camera_xmain.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/constraintLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="androidx.camera.app.CameraXActivity">
+
+    <TextureView
+        android:id="@+id/textureView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="#FFF"
+        android:elevation="2dp"
+        android:scaleType="fitXY"
+        android:src="@android:drawable/btn_radio"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintDimensionRatio="4:3"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="1.0"
+        app:layout_constraintStart_toStartOf="@+id/guideline"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="1.0" />
+
+    <android.support.constraint.Guideline
+        android:id="@+id/guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="0dp"
+        app:layout_constraintGuide_percent="0.7" />
+
+    <android.support.constraint.Guideline
+        android:id="@+id/takepicture"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="0dp"
+        app:layout_constraintGuide_percent="0.1" />
+
+    <android.support.constraint.Guideline
+        android:id="@+id/takevideo"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="0dp"
+        app:layout_constraintGuide_percent="0.4" />
+
+    <Button
+        android:id="@+id/Picture"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Picture"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="@+id/takepicture"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="1.0" />
+
+    <Button
+        android:id="@+id/Video"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Record"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="@+id/takevideo"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="1.0" />
+
+    <Button
+        android:id="@+id/VideoToggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Video"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0"
+        app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0"
+        />
+
+    <Button
+        android:id="@+id/PhotoToggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Photo"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.333"
+        app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0"
+        />
+
+    <Button
+        android:id="@+id/AnalysisToggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Analysis"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.666"
+        app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0"
+        />
+
+    <Button
+        android:id="@+id/PreviewToggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Preview"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="1.0"
+        app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0"
+        />
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/hellocamerax/src/main/res/values/strings.xml b/camera/integration-tests/hellocamerax/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5fdbbbf
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources />
+    </resources>
diff --git a/camera/integration-tests/hellocamerax/src/main/res/values/style.xml b/camera/integration-tests/hellocamerax/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/hellocamerax/src/main/res/values/style.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+    </style>
+</resources>
diff --git a/camera/integration-tests/javacameraxpermissions/build.gradle b/camera/integration-tests/javacameraxpermissions/build.gradle
new file mode 100644
index 0000000..e59bb5e
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileOptions {
+        sourceCompatibility project.ext.javaVersion
+        targetCompatibility project.ext.javaVersion
+    }
+
+    compileSdkVersion project.ext.compileSdk
+
+    defaultConfig {
+        applicationId "androidx.camera.testapp.javacameraxpermissions"
+        minSdkVersion project.ext.minSdk
+        targetSdkVersion project.ext.targetSdk
+        versionCode 1
+        versionName project.ext.version
+        multiDexEnabled true
+    }
+
+    sourceSets {
+        main.manifest.srcFile 'src/main/AndroidManifest.xml'
+        main.java.srcDirs = ['src/main/java']
+        main.java.excludes = ['**/build/**']
+        main.java.includes = ['**/*.java']
+        main.res.srcDirs = ['src/main/res']
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation project(':camera2')
+    implementation project(':core')
+
+    // Lifecycle and LiveData
+    implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+    // Android Support Library
+    implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support.constraint:constraint-layout:1.0.2"
+    implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/AndroidManifest.xml b/camera/integration-tests/javacameraxpermissions/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c9c6a7b
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.testapp.javacameraxpermissions">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:theme="@style/Theme.AppCompat">
+
+        <activity
+            android:name=".MainActivity"
+            android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/java/androidx/camera/testapp/javacameraxpermissions/MainActivity.java b/camera/integration-tests/javacameraxpermissions/src/main/java/androidx/camera/testapp/javacameraxpermissions/MainActivity.java
new file mode 100644
index 0000000..3eeb1b2
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/java/androidx/camera/testapp/javacameraxpermissions/MainActivity.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.javacameraxpermissions;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.ViewGroup;
+
+import androidx.camera.core.CameraX;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import java.util.concurrent.CompletableFuture;
+
+@SuppressWarnings("AndroidJdkLibsChecker")
+public class MainActivity extends AppCompatActivity {
+    private static final String TAG = MainActivity.class.getName();
+    private static final int CAMERA_REQUEST_CODE = 101;
+    private final CompletableFuture<Integer> cf = new CompletableFuture<>();
+
+    private static void makePermissionRequest(Activity context) {
+        ActivityCompat.requestPermissions(
+                context, new String[]{Manifest.permission.CAMERA}, CAMERA_REQUEST_CODE);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle bundle) {
+        super.onCreate(bundle);
+        setContentView(R.layout.activity_main);
+        new Thread(
+                () -> {
+                    setupCamera();
+                })
+                .start();
+        setupPermissions(this);
+    }
+
+    private void setupCamera() {
+        try {
+            // Wait for permissions before proceeding.
+            if (cf.get() == PackageManager.PERMISSION_DENIED) {
+                Log.i(TAG, "Permission to open camera denied.");
+                return;
+            }
+        } catch (Exception e) {
+            Log.i(TAG, "Exception occurred getting permission future: " + e);
+        }
+
+        // Run this on the UI thread to manipulate the Textures & Views.
+        MainActivity.this.runOnUiThread(
+                () -> {
+                    ViewFinderUseCaseConfiguration vfConfig =
+                            new ViewFinderUseCaseConfiguration.Builder()
+                                    .setTargetName("vf0")
+                                    .build();
+                    TextureView textureView = findViewById(R.id.textureView);
+                    ViewFinderUseCase vfUseCase = new ViewFinderUseCase(vfConfig);
+
+                    vfUseCase.setOnViewFinderOutputUpdateListener(
+                            viewFinderOutput -> {
+                                // If TextureView was already created, need to re-add it to change
+                                // the SurfaceTexture.
+                                ViewGroup v = (ViewGroup) textureView.getParent();
+                                v.removeView(textureView);
+                                v.addView(textureView);
+                                textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                            });
+                    CameraX.bindToLifecycle(/* lifecycleOwner= */ this, vfUseCase);
+                });
+    }
+
+    private void setupPermissions(Activity context) {
+        int permission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA);
+        if (permission != PackageManager.PERMISSION_GRANTED) {
+            makePermissionRequest(context);
+        } else {
+            cf.complete(permission);
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, String[] permissions, int[] grantResults) {
+        switch (requestCode) {
+            case CAMERA_REQUEST_CODE: {
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    Log.i(TAG, "Camera Permission Granted.");
+                } else {
+                    Log.i(TAG, "Camera Permission Denied.");
+                }
+                cf.complete(grantResults[0]);
+                return;
+            }
+            default: {
+            }
+        }
+    }
+}
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/res/layout/activity_main.xml b/camera/integration-tests/javacameraxpermissions/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..aeb586e
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/res/layout/activity_main.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:layout_constraintHeight_min="640dp"
+    tools:context="androidx.camera.app.javacameraxpermissions.MainActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <TextureView
+            android:id="@+id/textureView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:minHeight="177dp" />
+    </LinearLayout>
+
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/javacameraxpermissions/src/main/res/values/strings.xml b/camera/integration-tests/javacameraxpermissions/src/main/res/values/strings.xml
new file mode 100644
index 0000000..3ae3d90
--- /dev/null
+++ b/camera/integration-tests/javacameraxpermissions/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <string name="app_name">CameraXPermissions</string>
+</resources>
diff --git a/camera/integration-tests/timingapp/build.gradle b/camera/integration-tests/timingapp/build.gradle
new file mode 100644
index 0000000..b3939a4
--- /dev/null
+++ b/camera/integration-tests/timingapp/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileOptions {
+        sourceCompatibility project.ext.javaVersion
+        targetCompatibility project.ext.javaVersion
+    }
+
+    compileSdkVersion project.ext.compileSdk
+
+    defaultConfig {
+        applicationId "androidx.camera.testapp.timingapp"
+        minSdkVersion project.ext.minSdk
+        targetSdkVersion project.ext.targetSdk
+        versionCode 1
+        versionName project.ext.version
+        multiDexEnabled true
+    }
+
+    sourceSets {
+        main.manifest.srcFile 'src/main/AndroidManifest.xml'
+        main.java.srcDirs = ['src/main/java']
+        main.java.excludes = ['**/build/**']
+        main.java.includes = ['**/*.java']
+        main.res.srcDirs = ['src/main/res']
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+
+        release {
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation project(':camera2')
+    implementation project(':core')
+
+    // Lifecycle and LiveData
+    implementation "android.arch.lifecycle:extensions:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:livedata:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:runtime:${project.ext.lifecycleVersion}"
+    implementation "android.arch.lifecycle:common-java8:${project.ext.lifecycleVersion}"
+
+    // Android Support Library
+    implementation "com.android.support:appcompat-v7:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support.constraint:constraint-layout:1.0.2"
+    implementation "com.android.support:support-compat:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-annotations:${project.ext.supportLibraryVersion}"
+    implementation "com.android.support:support-v13:${project.ext.supportLibraryVersion}"
+}
+
diff --git a/camera/integration-tests/timingapp/src/main/AndroidManifest.xml b/camera/integration-tests/timingapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5f47d8c
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.testapp.timingapp">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application android:theme="@style/AppTheme">
+        <activity
+            android:name=".TakePhotoActivity"
+            android:label="Taking Photo">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/BaseActivity.java b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/BaseActivity.java
new file mode 100644
index 0000000..e5cacda
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/BaseActivity.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.timingapp;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An activity used to run performance test case.
+ *
+ * <p>To run performance test case, please implement this Activity. Camerax Use Case can be
+ * implement in prepareUseCase and runUseCase. For performance result, you can set currentTimeMillis
+ * to startTime and store the execution time into totalTime. At the end of test case, please call
+ * onUseCaseFinish() to notify the lock.
+ */
+public abstract class BaseActivity extends AppCompatActivity {
+    public static final long MICROS_IN_SECOND = TimeUnit.SECONDS.toMillis(1);
+    public static final long PREVIEW_FILL_BUFFER_TIME = 1500;
+    private static final String TAG = "BaseActivity";
+    public long startTime;
+    public long totalTime;
+    public long openCameraStartTime;
+    public long openCameraTotalTime;
+    public long startRreviewTime;
+    public long startPreviewTotalTime;
+    public long previewFrameRate;
+    public long closeCameraStartTime;
+    public long closeCameraTotalTime;
+    public String imageResolution;
+    public CountDownLatch latch;
+
+    public abstract void prepareUseCase();
+
+    public abstract void runUseCase() throws InterruptedException;
+
+    public void onUseCaseFinish() {
+        latch.countDown();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        latch = new CountDownLatch(1);
+    }
+}
diff --git a/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/CustomLifecycle.java b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/CustomLifecycle.java
new file mode 100644
index 0000000..926d84f
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/CustomLifecycle.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.timingapp;
+
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.Lifecycle.State;
+import android.arch.lifecycle.LifecycleOwner;
+import android.arch.lifecycle.LifecycleRegistry;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+
+/** A customized lifecycle owner which obeys the lifecycle transition rules. */
+public final class CustomLifecycle implements LifecycleOwner {
+    private final LifecycleRegistry lifecycleRegistry;
+    private final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+    public CustomLifecycle() {
+        lifecycleRegistry = new LifecycleRegistry(this);
+        lifecycleRegistry.markState(Lifecycle.State.INITIALIZED);
+        lifecycleRegistry.markState(Lifecycle.State.CREATED);
+    }
+
+    @NonNull
+    @Override
+    public Lifecycle getLifecycle() {
+        return lifecycleRegistry;
+    }
+
+    public void doOnResume() {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mainHandler.post(() -> doOnResume());
+            return;
+        }
+        lifecycleRegistry.markState(State.RESUMED);
+    }
+
+    public void doDestroyed() {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mainHandler.post(() -> doDestroyed());
+            return;
+        }
+        lifecycleRegistry.markState(State.DESTROYED);
+    }
+}
diff --git a/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/TakePhotoActivity.java b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/TakePhotoActivity.java
new file mode 100644
index 0000000..93adba9
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/java/androidx/camera/testapp/timingapp/TakePhotoActivity.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testapp.timingapp;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraDevice;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import androidx.camera.camera2.Camera2Configuration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCase.CaptureMode;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import com.google.common.base.Ascii;
+
+/** This Activity is used to run image capture performance test in mobileharness. */
+public class TakePhotoActivity extends BaseActivity {
+
+    private static final String TAG = "TakePhotoActivity";
+    // How many sample frames we should use to calculate framerate.
+    private static final int FRAMERATE_SAMPLE_WINDOW = 5;
+    private static final String EXTRA_CAPTURE_MODE = "capture_mode";
+    private static final String EXTRA_CAMERA_FACING = "camera_facing";
+    private static final String CAMERA_FACING_FRONT = "FRONT";
+    private static final String CAMERA_FACING_BACK = "BACK";
+    private final String defaultCameraFacing = CAMERA_FACING_BACK;
+    private final CameraDevice.StateCallback deviceStateCallback =
+            new CameraDevice.StateCallback() {
+
+                @Override
+                public void onOpened(CameraDevice cameraDevice) {
+                    openCameraTotalTime = System.currentTimeMillis() - openCameraStartTime;
+                    Log.d(TAG, "[onOpened] openCameraTotalTime: " + openCameraTotalTime);
+                    startRreviewTime = System.currentTimeMillis();
+                }
+
+                @Override
+                public void onClosed(CameraDevice camera) {
+                    super.onClosed(camera);
+                    closeCameraTotalTime = System.currentTimeMillis() - closeCameraStartTime;
+                    Log.d(TAG, "[onClosed] closeCameraTotalTime: " + closeCameraTotalTime);
+                    onUseCaseFinish();
+                }
+
+                @Override
+                public void onDisconnected(CameraDevice cameraDevice) {
+                }
+
+                @Override
+                public void onError(CameraDevice cameraDevice, int i) {
+                    Log.e(TAG, "[onError] open camera failed, error code: " + i);
+                }
+            };
+    private final CameraCaptureSession.StateCallback captureSessionStateCallback =
+            new CameraCaptureSession.StateCallback() {
+
+                @Override
+                public void onActive(CameraCaptureSession session) {
+                    super.onActive(session);
+                    startPreviewTotalTime = System.currentTimeMillis() - startRreviewTime;
+                    Log.d(TAG, "[onActive] previewStartTotalTime: " + startPreviewTotalTime);
+                }
+
+                @Override
+                public void onConfigured(CameraCaptureSession cameraCaptureSession) {
+                    Log.d(TAG, "[onConfigured] CaptureSession configured!");
+                }
+
+                @Override
+                public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
+                    Log.e(TAG, "[onConfigureFailed] CameraX preview initialization failed.");
+                }
+            };
+    /** The default cameraId to use. */
+    private LensFacing currentCameraLensFacing = LensFacing.BACK;
+    private ImageCaptureUseCase imageCaptureUseCase;
+    private ViewFinderUseCase viewFinderUseCase;
+    private int frameCount;
+    private long previewSampleStartTime;
+    private CaptureMode captureMode = CaptureMode.MIN_LATENCY;
+    private CustomLifecycle customLifecycle;
+
+    @Override
+    public void runUseCase() throws InterruptedException {
+
+        // Length of time to let the preview stream run before capturing the first image.
+        // This can help ensure capture latency is real latency and not merely the device
+        // filling the buffer.
+        Thread.sleep(PREVIEW_FILL_BUFFER_TIME);
+
+        startTime = System.currentTimeMillis();
+        imageCaptureUseCase.takePicture(
+                new ImageCaptureUseCase.OnImageCapturedListener() {
+                    @Override
+                    public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                        totalTime = System.currentTimeMillis() - startTime;
+                        if (image != null) {
+                            imageResolution = image.getWidth() + "x" + image.getHeight();
+                        } else {
+                            Log.e(TAG, "[onCaptureSuccess] image is null");
+                        }
+                    }
+                });
+    }
+
+    @Override
+    public void prepareUseCase() {
+        createViewFinderUseCase();
+        createImageCaptureUseCase();
+    }
+
+    void createViewFinderUseCase() {
+        ViewFinderUseCaseConfiguration.Builder configurationBuilder =
+                new ViewFinderUseCaseConfiguration.Builder()
+                        .setLensFacing(currentCameraLensFacing)
+                        .setTargetName("ViewFinder");
+
+        new Camera2Configuration.Extender(configurationBuilder)
+                .setDeviceStateCallback(deviceStateCallback)
+                .setSessionStateCallback(captureSessionStateCallback);
+
+        viewFinderUseCase = new ViewFinderUseCase(configurationBuilder.build());
+        openCameraStartTime = System.currentTimeMillis();
+
+        viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                viewFinderOutput -> {
+                    TextureView textureView = this.findViewById(R.id.textureView);
+                    ViewGroup viewGroup = (ViewGroup) textureView.getParent();
+                    viewGroup.removeView(textureView);
+                    viewGroup.addView(textureView);
+                    textureView.setSurfaceTexture(viewFinderOutput.getSurfaceTexture());
+                    textureView.setSurfaceTextureListener(
+                            new SurfaceTextureListener() {
+                                @Override
+                                public void onSurfaceTextureAvailable(
+                                        SurfaceTexture surfaceTexture, int i, int i1) {
+                                }
+
+                                @Override
+                                public void onSurfaceTextureSizeChanged(
+                                        SurfaceTexture surfaceTexture, int i, int i1) {
+                                }
+
+                                @Override
+                                public boolean onSurfaceTextureDestroyed(
+                                        SurfaceTexture surfaceTexture) {
+                                    return false;
+                                }
+
+                                @Override
+                                public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+                                    Log.d(TAG, "[onSurfaceTextureUpdated]");
+                                    if (0 == totalTime) {
+                                        return;
+                                    }
+
+                                    if (0 == frameCount) {
+                                        previewSampleStartTime = System.currentTimeMillis();
+                                    } else if (FRAMERATE_SAMPLE_WINDOW == frameCount) {
+                                        final long duration =
+                                                System.currentTimeMillis() - previewSampleStartTime;
+                                        previewFrameRate =
+                                                (MICROS_IN_SECOND
+                                                        * FRAMERATE_SAMPLE_WINDOW
+                                                        / duration);
+                                        closeCameraStartTime = System.currentTimeMillis();
+                                        customLifecycle.doDestroyed();
+                                    }
+                                    frameCount++;
+                                }
+                            });
+                });
+
+        CameraX.bindToLifecycle(customLifecycle, viewFinderUseCase);
+    }
+
+    void createImageCaptureUseCase() {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder()
+                        .setTargetName("ImageCapture")
+                        .setLensFacing(currentCameraLensFacing)
+                        .setCaptureMode(captureMode)
+                        .build();
+
+        imageCaptureUseCase = new ImageCaptureUseCase(configuration);
+        CameraX.bindToLifecycle(customLifecycle, imageCaptureUseCase);
+
+        final Button button = this.findViewById(R.id.Picture);
+        button.setOnClickListener(
+                view -> {
+                    startTime = System.currentTimeMillis();
+                    imageCaptureUseCase.takePicture(
+                            new ImageCaptureUseCase.OnImageCapturedListener() {
+                                @Override
+                                public void onCaptureSuccess(
+                                        ImageProxy image, int rotationDegrees) {
+                                    totalTime = System.currentTimeMillis() - startTime;
+                                    if (image != null) {
+                                        imageResolution =
+                                                image.getWidth() + "x" + image.getHeight();
+                                    } else {
+                                        Log.e(TAG, "[onCaptureSuccess] image is null");
+                                    }
+                                }
+                            });
+                });
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        final Bundle bundle = getIntent().getExtras();
+        if (bundle != null) {
+            final String captureModeString = bundle.getString(EXTRA_CAPTURE_MODE);
+            if (captureModeString != null) {
+                captureMode = CaptureMode.valueOf(Ascii.toUpperCase(captureModeString));
+            }
+            final String cameraLensFacing = bundle.getString(EXTRA_CAMERA_FACING);
+            if (cameraLensFacing != null) {
+                setupCamera(cameraLensFacing);
+            } else {
+                setupCamera(defaultCameraFacing);
+            }
+        }
+        customLifecycle = new CustomLifecycle();
+        prepareUseCase();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        customLifecycle.doOnResume();
+    }
+
+    void setupCamera(String cameraFacing) {
+        Log.d(TAG, "Camera Facing: " + cameraFacing);
+        if (Ascii.equalsIgnoreCase(cameraFacing, CAMERA_FACING_BACK)) {
+            currentCameraLensFacing = LensFacing.BACK;
+        } else if (Ascii.equalsIgnoreCase(cameraFacing, CAMERA_FACING_FRONT)) {
+            currentCameraLensFacing = LensFacing.FRONT;
+        } else {
+            throw new RuntimeException("Invalid lens facing: " + cameraFacing);
+        }
+    }
+}
diff --git a/camera/integration-tests/timingapp/src/main/res/layout/activity_main.xml b/camera/integration-tests/timingapp/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..76ee350
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/res/layout/activity_main.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="androidx.camera.app.timingapp">
+
+    <TextureView
+        android:id="@+id/textureView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <android.support.constraint.Guideline
+        android:id="@+id/takepicture"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="0dp"
+        app:layout_constraintGuide_percent="0.1" />
+
+    <Button
+        android:id="@+id/Picture"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="Picture"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="@+id/takepicture"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="1.0" />
+</android.support.constraint.ConstraintLayout>
diff --git a/camera/integration-tests/timingapp/src/main/res/values/strings.xml b/camera/integration-tests/timingapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2df0565
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+</resources>
diff --git a/camera/integration-tests/timingapp/src/main/res/values/style.xml b/camera/integration-tests/timingapp/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/timingapp/src/main/res/values/style.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+    </style>
+</resources>
diff --git a/camera/testing/src/main/AndroidManifest.xml b/camera/testing/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..abd2784
--- /dev/null
+++ b/camera/testing/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2019 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest package="androidx.camera.testing" />
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfiguration.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfiguration.java
new file mode 100644
index 0000000..afd2f8b
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.ExtendableUseCaseConfigFactory;
+import androidx.camera.core.UseCaseConfigurationFactory;
+
+/**
+ * Convenience class for generating a fake {@link androidx.camera.core.AppConfiguration}.
+ *
+ * <p>This {@link AppConfiguration} contains all fake CameraX implementation components.
+ */
+public final class FakeAppConfiguration {
+
+    /** Generates a fake {@link androidx.camera.core.AppConfiguration}. */
+    public static final AppConfiguration create() {
+        CameraFactory cameraFactory = new FakeCameraFactory();
+        CameraDeviceSurfaceManager surfaceManager = new FakeCameraDeviceSurfaceManager();
+        UseCaseConfigurationFactory defaultConfigFactory = new ExtendableUseCaseConfigFactory();
+
+        AppConfiguration.Builder appConfigBuilder =
+                new AppConfiguration.Builder()
+                        .setCameraFactory(cameraFactory)
+                        .setDeviceSurfaceManager(surfaceManager)
+                        .setUseCaseConfigFactory(defaultConfigFactory);
+
+        return appConfigBuilder.build();
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
new file mode 100644
index 0000000..cc45b5d
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CaptureRequestConfiguration;
+
+import java.util.Collection;
+
+/** A fake camera which will not produce any data. */
+public class FakeCamera implements BaseCamera {
+    private final CameraControl cameraControl = CameraControl.defaultEmptyInstance();
+
+    private final CameraInfo cameraInfo;
+
+    FakeCamera() {
+        this(new FakeCameraInfo());
+    }
+
+    FakeCamera(FakeCameraInfo cameraInfo) {
+        this.cameraInfo = cameraInfo;
+    }
+
+    @Override
+    public void open() {
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public void release() {
+    }
+
+    @Override
+    public void addOnlineUseCase(Collection<BaseUseCase> baseUseCases) {
+    }
+
+    @Override
+    public void removeOnlineUseCase(Collection<BaseUseCase> baseUseCases) {
+    }
+
+    @Override
+    public void onUseCaseActive(BaseUseCase useCase) {
+    }
+
+    @Override
+    public void onUseCaseInactive(BaseUseCase useCase) {
+    }
+
+    @Override
+    public void onUseCaseUpdated(BaseUseCase useCase) {
+    }
+
+    @Override
+    public void onUseCaseReset(BaseUseCase useCase) {
+    }
+
+    @Override
+    public void onUseCaseSingleRequest(
+            BaseUseCase useCase, CaptureRequestConfiguration captureRequestConfiguration) {
+    }
+
+    // Returns fixed CameraControl instance in order to verify the instance is correctly attached.
+    @Override
+    public CameraControl getCameraControl() {
+        return cameraControl;
+    }
+
+    @Override
+    public CameraInfo getCameraInfo() {
+        return cameraInfo;
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
new file mode 100644
index 0000000..c3f7473
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraDeviceSurfaceManager.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceSurfaceManager;
+import androidx.camera.core.SurfaceConfiguration;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** A CameraDeviceSurfaceManager which has no supported SurfaceConfigurations. */
+public class FakeCameraDeviceSurfaceManager implements CameraDeviceSurfaceManager {
+
+    private static final Size MAX_OUTPUT_SIZE = new Size(0, 0);
+    private static final Size PREVIEW_SIZE = new Size(1920, 1080);
+
+    @Override
+    public boolean checkSupported(
+            String cameraId, List<SurfaceConfiguration> surfaceConfigurationList) {
+        return false;
+    }
+
+    @Override
+    public SurfaceConfiguration transformSurfaceConfiguration(
+            String cameraId, int imageFormat, Size size) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public Size getMaxOutputSize(String cameraId, int imageFormat) {
+        return MAX_OUTPUT_SIZE;
+    }
+
+    @Override
+    public Map<BaseUseCase, Size> getSuggestedResolutions(
+            String cameraId, List<BaseUseCase> originalUseCases, List<BaseUseCase> newUseCases) {
+        Map<BaseUseCase, Size> suggestedSizes = new HashMap<>();
+        for (BaseUseCase useCase : newUseCases) {
+            suggestedSizes.put(useCase, MAX_OUTPUT_SIZE);
+        }
+
+        return suggestedSizes;
+    }
+
+    @Override
+    public Size getPreviewSize() {
+        return PREVIEW_SIZE;
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
new file mode 100644
index 0000000..2618741
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link CameraFactory} implementation that contains and produces fake cameras.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeCameraFactory implements CameraFactory {
+
+    private static final String BACK_ID = "0";
+    private static final String FRONT_ID = "1";
+
+    private final Set<String> cameraIds;
+
+    private final Map<String, BaseCamera> cameraMap = new HashMap<>();
+
+    public FakeCameraFactory() {
+        HashSet<String> camIds = new HashSet<>();
+        camIds.add(BACK_ID);
+        camIds.add(FRONT_ID);
+
+        cameraIds = Collections.unmodifiableSet(camIds);
+    }
+
+    @Override
+    public BaseCamera getCamera(String cameraId) {
+        if (cameraIds.contains(cameraId)) {
+            BaseCamera camera = cameraMap.get(cameraId);
+            if (camera == null) {
+                camera = new FakeCamera();
+                cameraMap.put(cameraId, camera);
+            }
+            return camera;
+        }
+        throw new IllegalArgumentException("Unknown camera: " + cameraId);
+    }
+
+    @Override
+    public Set<String> getAvailableCameraIds() {
+        return cameraIds;
+    }
+
+    @Nullable
+    @Override
+    public String cameraIdForLensFacing(LensFacing lensFacing) {
+        switch (lensFacing) {
+            case FRONT:
+                return FRONT_ID;
+            case BACK:
+                return BACK_ID;
+        }
+
+        throw new IllegalArgumentException("Unknown lensFacing: " + lensFacing);
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java
new file mode 100644
index 0000000..8d201db
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+/**
+ * Information for a fake camera.
+ *
+ * <p>This camera info can be constructed with fake values.
+ */
+class FakeCameraInfo implements CameraInfo {
+
+    private final int sensorRotation;
+    private final LensFacing lensFacing;
+
+    FakeCameraInfo() {
+        this(/*sensorRotation=*/ 0, /*lensFacing=*/ LensFacing.BACK);
+    }
+
+    FakeCameraInfo(int sensorRotation, LensFacing lensFacing) {
+        this.sensorRotation = sensorRotation;
+        this.lensFacing = lensFacing;
+    }
+
+    @Nullable
+    @Override
+    public LensFacing getLensFacing() {
+        return lensFacing;
+    }
+
+    @Override
+    public int getSensorRotationDegrees(@RotationValue int relativeRotation) {
+        return sensorRotation;
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeConfiguration.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeConfiguration.java
new file mode 100644
index 0000000..d7e7791
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeConfiguration.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.MutableConfiguration;
+import androidx.camera.core.MutableOptionsBundle;
+import androidx.camera.core.OptionsBundle;
+
+/** Wrapper for an empty Configuration */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeConfiguration implements Configuration.Reader {
+
+    private final Configuration config;
+
+    FakeConfiguration(Configuration config) {
+        this.config = config;
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return config;
+    }
+
+    /** Builder for an empty Configuration */
+    public static final class Builder implements Configuration.Builder<FakeConfiguration, Builder> {
+
+        private final MutableOptionsBundle optionsBundle;
+
+        public Builder() {
+            optionsBundle = MutableOptionsBundle.create();
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return optionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public FakeConfiguration build() {
+            return new FakeConfiguration(OptionsBundle.from(optionsBundle));
+        }
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeLifecycleOwner.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeLifecycleOwner.java
new file mode 100644
index 0000000..a255142
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeLifecycleOwner.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/**
+ * A fake lifecycle owner which obeys the lifecycle transition rules.
+ *
+ * @hide
+ * @see <a href="https://developer.android.com/topic/libraries/architecture/lifecycle">lifecycle</a>
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeLifecycleOwner implements LifecycleOwner {
+    private final LifecycleRegistry lifecycleRegistry;
+
+    /**
+     * Creates a new lifecycle owner.
+     *
+     * <p>The lifecycle is initial put into the INITIALIZED and CREATED states.
+     */
+    public FakeLifecycleOwner() {
+        lifecycleRegistry = new LifecycleRegistry(this);
+        lifecycleRegistry.markState(Lifecycle.State.INITIALIZED);
+        lifecycleRegistry.markState(Lifecycle.State.CREATED);
+    }
+
+    /**
+     * Starts and resumes the lifecycle.
+     *
+     * <p>The lifecycle is put into the STARTED and RESUMED states. The lifecycle must already be in
+     * the CREATED state or an exception is thrown.
+     */
+    public void startAndResume() {
+        if (lifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        lifecycleRegistry.markState(Lifecycle.State.STARTED);
+        lifecycleRegistry.markState(Lifecycle.State.RESUMED);
+    }
+
+    /**
+     * Starts the lifecycle.
+     *
+     * <p>The lifecycle is put into the START state. The lifecycle must already be in the CREATED
+     * state or an exception is thrown.
+     */
+    public void start() {
+        if (lifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        lifecycleRegistry.markState(Lifecycle.State.STARTED);
+    }
+
+    /**
+     * Pauses and stops the lifecycle.
+     *
+     * <p>The lifecycle is put into the STARTED and CREATED states. The lifecycle must already be in
+     * the RESUMED state or an exception is thrown.
+     */
+    public void pauseAndStop() {
+        if (lifecycleRegistry.getCurrentState() != Lifecycle.State.RESUMED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        lifecycleRegistry.markState(Lifecycle.State.STARTED);
+        lifecycleRegistry.markState(Lifecycle.State.CREATED);
+    }
+
+    /**
+     * Stops the lifecycle.
+     *
+     * <p>The lifecycle is put into the CREATED state. The lifecycle must already be in the STARTED
+     * state or an exception is thrown.
+     */
+    public void stop() {
+        if (lifecycleRegistry.getCurrentState() != Lifecycle.State.STARTED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        lifecycleRegistry.markState(Lifecycle.State.CREATED);
+    }
+
+    /**
+     * Destroys the lifecycle.
+     *
+     * <p>The lifecycle is put into the DESTROYED state. The lifecycle must already be in the
+     * CREATED state or an exception is thrown.
+     */
+    public void destroy() {
+        if (lifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        lifecycleRegistry.markState(Lifecycle.State.DESTROYED);
+    }
+
+    /** Returns the number of observers of this lifecycle. */
+    public int getObserverCount() {
+        return lifecycleRegistry.getObserverCount();
+    }
+
+    @Override
+    public Lifecycle getLifecycle() {
+        return lifecycleRegistry;
+    }
+}
diff --git a/camera/view/proguard.flags b/camera/view/proguard.flags
new file mode 100644
index 0000000..d8edd2e
--- /dev/null
+++ b/camera/view/proguard.flags
@@ -0,0 +1,66 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# You can comment this out if you're not interested in stack traces.
+
+-keepparameternames
+-keepattributes Exceptions,InnerClasses,Signature,Deprecated,
+                SourceFile,LineNumberTable,EnclosingMethod
+
+# Preserve all annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all public classes, and their public and protected fields and
+# methods.
+
+-keep public class * {
+    public protected *;
+}
+
+# Preserve all .class method names.
+
+-keepclassmembernames class * {
+    java.lang.Class class$(java.lang.String);
+    java.lang.Class class$(java.lang.String, boolean);
+}
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+# Preserve the special static methods that are required in all enumeration
+# classes.
+
+-keepclassmembers class * extends java.lang.Enum {
+    public static **[] values();
+    public static ** valueOf(java.lang.String);
+}
+
+# Explicitly preserve all serialization members. The Serializable interface
+# is only a marker interface, so it wouldn't save them.
+# You can comment this out if your library doesn't use serialization.
+# If your code contains serializable classes that have to be backward
+# compatible, please refer to the manual.
+
+-keepclassmembers class * implements java.io.Serializable {
+    static final long serialVersionUID;
+    static final java.io.ObjectStreamField[] serialPersistentFields;
+    private void writeObject(java.io.ObjectOutputStream);
+    private void readObject(java.io.ObjectInputStream);
+    java.lang.Object writeReplace();
+    java.lang.Object readResolve();
+}
diff --git a/camera/view/src/main/AndroidManifest.xml b/camera/view/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ae10d71
--- /dev/null
+++ b/camera/view/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.view">
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="27" />
+
+</manifest>
diff --git a/camera/view/src/main/java/androidx/camera/view/CameraView.java b/camera/view/src/main/java/androidx/camera/view/CameraView.java
new file mode 100644
index 0000000..7f434e1
--- /dev/null
+++ b/camera/view/src/main/java/androidx/camera/view/CameraView.java
@@ -0,0 +1,1160 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view;
+
+import android.Manifest.permission;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Size;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.BaseInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.ImageCaptureUseCase.OnImageCapturedListener;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.io.File;
+
+/**
+ * A {@link View} that displays a preview of the camera with methods {@link
+ * #takePicture(OnImageCapturedListener)}, {@link #takePicture(File, OnImageSavedListener)}, {@link
+ * #startRecording(File, OnVideoSavedListener)} and {@link #stopRecording()}.
+ *
+ * <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
+ * be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
+ * LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
+ */
+public final class CameraView extends ViewGroup {
+    static final String TAG = androidx.camera.view.CameraView.class.getSimpleName();
+    static final boolean DEBUG = false;
+
+    static final int INDEFINITE_VIDEO_DURATION = -1;
+    static final int INDEFINITE_VIDEO_SIZE = -1;
+
+    private static final String EXTRA_SUPER = "super";
+    private static final String EXTRA_QUALITY = "quality";
+    private static final String EXTRA_ZOOM_LEVEL = "zoom_level";
+    private static final String EXTRA_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled";
+    private static final String EXTRA_FLASH = "flash";
+    private static final String EXTRA_MAX_VIDEO_DURATION = "max_video_duration";
+    private static final String EXTRA_MAX_VIDEO_SIZE = "max_video_size";
+    private static final String EXTRA_SCALE_TYPE = "scale_type";
+    private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
+    private static final String EXTRA_CAPTURE_MODE = "captureMode";
+
+    private static final int LENS_FACING_NONE = 0;
+    private static final int LENS_FACING_FRONT = 1;
+    private static final int LENS_FACING_BACK = 2;
+    private static final int FLASH_MODE_AUTO = 1;
+    private static final int FLASH_MODE_ON = 2;
+    private static final int FLASH_MODE_OFF = 4;
+    private final Rect focusingRect = new Rect();
+    private final Rect meteringRect = new Rect();
+    // For tap-to-focus
+    private long downEventTimestamp;
+    // For pinch-to-zoom
+    private PinchToZoomGestureDetector pinchToZoomGestureDetector;
+    private boolean isPinchToZoomEnabled = true;
+    CameraXModule cameraModule;
+    private final DisplayManager.DisplayListener displayListener =
+            new DisplayListener() {
+                @Override
+                public void onDisplayAdded(int displayId) {
+                }
+
+                @Override
+                public void onDisplayRemoved(int displayId) {
+                }
+
+                @Override
+                public void onDisplayChanged(int displayId) {
+                    cameraModule.invalidateView();
+                }
+            };
+    private TextureView cameraTextureView;
+    private Size viewFinderSrcSize = new Size(0, 0);
+    private ScaleType scaleType = ScaleType.CENTER_CROP;
+    // For accessibility event
+    private MotionEvent upEvent;
+    private @Nullable
+    Paint layerPaint;
+
+    public CameraView(Context context) {
+        this(context, null);
+    }
+
+    public CameraView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CameraView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context, attrs);
+    }
+
+    @TargetApi(21)
+    public CameraView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs);
+    }
+
+    /** Debug logging that can be enabled. */
+    private static void log(String msg) {
+        if (DEBUG) {
+            Log.i(TAG, msg);
+        }
+    }
+
+    /** Utility method for converting an displayRotation int into a human readable string. */
+    private static String displayRotationToString(int displayRotation) {
+        if (displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_180) {
+            return "Portrait-" + (displayRotation * 90);
+        } else if (displayRotation == Surface.ROTATION_90
+                || displayRotation == Surface.ROTATION_270) {
+            return "Landscape-" + (displayRotation * 90);
+        } else {
+            return "Unknown";
+        }
+    }
+
+    /**
+     * Binds control of the camera used by this view to the given lifecycle.
+     *
+     * <p>This links opening/closing the camera to the given lifecycle. The camera will not operate
+     * unless this method is called with a valid {@link LifecycleOwner} that is not in the {@link
+     * android.arch.lifecycle.Lifecycle.State#DESTROYED} state. Call this method only once camera
+     * permissions have been obtained.
+     *
+     * <p>Once the provided lifecycle has transitioned to a {@link
+     * android.arch.lifecycle.Lifecycle.State#DESTROYED} state, CameraView must be bound to a new
+     * lifecycle through this method in order to operate the camera.
+     *
+     * @param lifecycleOwner The lifecycle that will control this view's camera
+     * @throws IllegalArgumentException if provided lifecycle is in a {@link
+     *                                  android.arch.lifecycle.Lifecycle.State#DESTROYED} state.
+     * @throws IllegalStateException    if camera permissions are not granted.
+     */
+    @RequiresPermission(permission.CAMERA)
+    public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
+        cameraModule.bindToLifecycle(lifecycleOwner);
+    }
+
+    private void init(Context context, @Nullable AttributeSet attrs) {
+        addView(cameraTextureView = new TextureView(getContext()), 0 /* view position */);
+        cameraTextureView.setLayerPaint(layerPaint);
+        cameraModule = new CameraXModule(this);
+
+        if (isInEditMode()) {
+            onViewfinderSourceDimensUpdated(640, 480);
+        }
+
+        if (attrs != null) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
+            setScaleType(
+                    ScaleType.fromId(
+                            a.getInteger(R.styleable.CameraView_scaleType, getScaleType().id)));
+            setQuality(
+                    Quality.fromId(a.getInteger(R.styleable.CameraView_quality, getQuality().id)));
+            setPinchToZoomEnabled(
+                    a.getBoolean(
+                            R.styleable.CameraView_pinchToZoomEnabled, isPinchToZoomEnabled()));
+            setCaptureMode(
+                    CaptureMode.fromId(
+                            a.getInteger(R.styleable.CameraView_captureMode, getCaptureMode().id)));
+
+            int lensFacing = a.getInt(R.styleable.CameraView_lensFacing, LENS_FACING_BACK);
+            switch (lensFacing) {
+                case LENS_FACING_NONE:
+                    setCameraByLensFacing(null);
+                    break;
+                case LENS_FACING_FRONT:
+                    setCameraByLensFacing(LensFacing.FRONT);
+                    break;
+                case LENS_FACING_BACK:
+                    setCameraByLensFacing(LensFacing.BACK);
+                    break;
+                default:
+                    // Unhandled event.
+            }
+
+            int flashMode = a.getInt(R.styleable.CameraView_flash, 0);
+            switch (flashMode) {
+                case FLASH_MODE_AUTO:
+                    setFlash(FlashMode.AUTO);
+                    break;
+                case FLASH_MODE_ON:
+                    setFlash(FlashMode.ON);
+                    break;
+                case FLASH_MODE_OFF:
+                    setFlash(FlashMode.OFF);
+                    break;
+                default:
+                    // Unhandled event.
+            }
+
+            a.recycle();
+        }
+
+        if (getBackground() == null) {
+            setBackgroundColor(0xFF111111);
+        }
+
+        pinchToZoomGestureDetector = new PinchToZoomGestureDetector(context);
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        // TODO(b/113884082): Decide what belongs here or what should be invalidated on
+        // configuration
+        // change
+        Bundle state = new Bundle();
+        state.putParcelable(EXTRA_SUPER, super.onSaveInstanceState());
+        state.putInt(EXTRA_SCALE_TYPE, getScaleType().id);
+        state.putInt(EXTRA_QUALITY, getQuality().id);
+        state.putFloat(EXTRA_ZOOM_LEVEL, getZoomLevel());
+        state.putBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED, isPinchToZoomEnabled());
+        state.putString(EXTRA_FLASH, getFlash().name());
+        state.putLong(EXTRA_MAX_VIDEO_DURATION, getMaxVideoDuration());
+        state.putLong(EXTRA_MAX_VIDEO_SIZE, getMaxVideoSize());
+        if (getCameraLensFacing() != null) {
+            state.putString(EXTRA_CAMERA_DIRECTION, getCameraLensFacing().name());
+        }
+        state.putInt(EXTRA_CAPTURE_MODE, getCaptureMode().id);
+        return state;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable savedState) {
+        // TODO(b/113884082): Decide what belongs here or what should be invalidated on
+        // configuration
+        // change
+        if (savedState instanceof Bundle) {
+            Bundle state = (Bundle) savedState;
+            super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
+            setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
+            setQuality(Quality.fromId(state.getInt(EXTRA_QUALITY)));
+            setZoomLevel(state.getFloat(EXTRA_ZOOM_LEVEL));
+            setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
+            setFlash(FlashMode.valueOf(state.getString(EXTRA_FLASH)));
+            setMaxVideoDuration(state.getLong(EXTRA_MAX_VIDEO_DURATION));
+            setMaxVideoSize(state.getLong(EXTRA_MAX_VIDEO_SIZE));
+            String lensFacingString = state.getString(EXTRA_CAMERA_DIRECTION);
+            setCameraByLensFacing(
+                    TextUtils.isEmpty(lensFacingString)
+                            ? null
+                            : LensFacing.valueOf(lensFacingString));
+            setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
+        } else {
+            super.onRestoreInstanceState(savedState);
+        }
+    }
+
+    /**
+     * Sets the paint on the viewfinder.
+     *
+     * <p>This only affects the viewfinder, and does not affect captured images/video.
+     *
+     * @param paint The paint object to apply to the viewfinder.
+     * @hide This may not work once {@link android.view.SurfaceView} is supported along with {@link
+     * TextureView}.
+     */
+    @Override
+    public void setLayerPaint(@Nullable Paint paint) {
+        super.setLayerPaint(paint);
+        layerPaint = paint;
+        cameraTextureView.setLayerPaint(paint);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        DisplayManager dpyMgr =
+                (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+        dpyMgr.registerDisplayListener(displayListener, new Handler(Looper.getMainLooper()));
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        DisplayManager dpyMgr =
+                (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+        dpyMgr.unregisterDisplayListener(displayListener);
+    }
+
+    // TODO(b/124269166): Rethink how we can handle permissions here.
+    @SuppressLint("MissingPermission")
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
+
+        int displayRotation = getDisplay().getRotation();
+
+        if (viewFinderSrcSize.getHeight() == 0 || viewFinderSrcSize.getWidth() == 0) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            cameraTextureView.measure(viewWidth, viewHeight);
+        } else {
+            Size scaled =
+                    calculateViewfinderViewDimens(
+                            viewFinderSrcSize, viewWidth, viewHeight, displayRotation, scaleType);
+            super.setMeasuredDimension(
+                    Math.min(scaled.getWidth(), viewWidth),
+                    Math.min(scaled.getHeight(), viewHeight));
+            cameraTextureView.measure(scaled.getWidth(), scaled.getHeight());
+        }
+
+        // Since bindToLifecycle will depend on the measured dimension, only call it when measured
+        // dimension is not 0x0
+        if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
+            cameraModule.bindToLifecycleAfterViewMeasured();
+        }
+    }
+
+    // TODO(b/124269166): Rethink how we can handle permissions here.
+    @SuppressLint("MissingPermission")
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        // In case that the CameraView size is always set as 0x0, we still need to trigger to force
+        // binding to lifecycle
+        cameraModule.bindToLifecycleAfterViewMeasured();
+
+        // If we don't know the src buffer size yet, set the viewfinder to be the parent size
+        if (viewFinderSrcSize.getWidth() == 0 || viewFinderSrcSize.getHeight() == 0) {
+            cameraTextureView.layout(left, top, right, bottom);
+            return;
+        }
+
+        // Compute the viewfinder ui size based on the available width, height, and ui orientation.
+        int viewWidth = (right - left);
+        int viewHeight = (bottom - top);
+        int displayRotation = getDisplay().getRotation();
+        Size scaled =
+                calculateViewfinderViewDimens(
+                        viewFinderSrcSize, viewWidth, viewHeight, displayRotation, scaleType);
+
+        // Compute the center of the view.
+        int centerX = viewWidth / 2;
+        int centerY = viewHeight / 2;
+
+        // Compute the left / top / right / bottom values such that viewfinder is centered.
+        int layoutL = centerX - (scaled.getWidth() / 2);
+        int layoutT = centerY - (scaled.getHeight() / 2);
+        int layoutR = layoutL + scaled.getWidth();
+        int layoutB = layoutT + scaled.getHeight();
+
+        // Layout debugging
+        log("layout: viewWidth:  " + viewWidth);
+        log("layout: viewHeight: " + viewHeight);
+        log("layout: viewRatio:  " + (viewWidth / (float) viewHeight));
+        log("layout: sizeWidth:  " + viewFinderSrcSize.getWidth());
+        log("layout: sizeHeight: " + viewFinderSrcSize.getHeight());
+        log(
+                "layout: sizeRatio:  "
+                        + (viewFinderSrcSize.getWidth() / (float) viewFinderSrcSize.getHeight()));
+        log("layout: scaledWidth:  " + scaled.getWidth());
+        log("layout: scaledHeight: " + scaled.getHeight());
+        log("layout: scaledRatio:  " + (scaled.getWidth() / (float) scaled.getHeight()));
+        log(
+                "layout: size:       "
+                        + scaled
+                        + " ("
+                        + (scaled.getWidth() / (float) scaled.getHeight())
+                        + " - "
+                        + scaleType
+                        + "-"
+                        + displayRotationToString(displayRotation)
+                        + ")");
+        log("layout: final       " + layoutL + ", " + layoutT + ", " + layoutR + ", " + layoutB);
+
+        cameraTextureView.layout(layoutL, layoutT, layoutR, layoutB);
+
+        cameraModule.invalidateView();
+    }
+
+    /** Records the size of the viewfinder's buffers. */
+    @UiThread
+    void onViewfinderSourceDimensUpdated(int srcWidth, int srcHeight) {
+        if (srcWidth != viewFinderSrcSize.getWidth()
+                || srcHeight != viewFinderSrcSize.getHeight()) {
+            viewFinderSrcSize = new Size(srcWidth, srcHeight);
+            requestLayout();
+        }
+    }
+
+    private Size calculateViewfinderViewDimens(
+            Size srcSize,
+            int parentWidth,
+            int parentHeight,
+            int displayRotation,
+            ScaleType scaleType) {
+        int inWidth = srcSize.getWidth();
+        int inHeight = srcSize.getHeight();
+        if (displayRotation == Surface.ROTATION_90 || displayRotation == Surface.ROTATION_270) {
+            // Need to reverse the width and height since we're in landscape orientation.
+            inWidth = srcSize.getHeight();
+            inHeight = srcSize.getWidth();
+        }
+
+        int outWidth = parentWidth;
+        int outHeight = parentHeight;
+        if (inWidth != 0 && inHeight != 0) {
+            float vfRatio = inWidth / (float) inHeight;
+            float parentRatio = parentWidth / (float) parentHeight;
+
+            switch (scaleType) {
+                case CENTER_INSIDE:
+                    // Match longest sides together.
+                    if (vfRatio > parentRatio) {
+                        outWidth = parentWidth;
+                        outHeight = Math.round(parentWidth / vfRatio);
+                    } else {
+                        outWidth = Math.round(parentHeight * vfRatio);
+                        outHeight = parentHeight;
+                    }
+                    break;
+                case CENTER_CROP:
+                    // Match shortest sides together.
+                    if (vfRatio < parentRatio) {
+                        outWidth = parentWidth;
+                        outHeight = Math.round(parentWidth / vfRatio);
+                    } else {
+                        outWidth = Math.round(parentHeight * vfRatio);
+                        outHeight = parentHeight;
+                    }
+                    break;
+            }
+        }
+
+        return new Size(outWidth, outHeight);
+    }
+
+    /**
+     * @return One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, {@link
+     * Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+     */
+    int getDisplaySurfaceRotation() {
+        Display display = getDisplay();
+
+        // Null when the View is detached. If we were in the middle of a background operation,
+        // better to not NPE. When the background operation finishes, it'll realize that the camera
+        // was closed.
+        if (display == null) {
+            return 0;
+        }
+
+        return display.getRotation();
+    }
+
+    @UiThread
+    SurfaceTexture getSurfaceTexture() {
+        if (cameraTextureView != null) {
+            return cameraTextureView.getSurfaceTexture();
+        }
+
+        return null;
+    }
+
+    @UiThread
+    void setSurfaceTexture(SurfaceTexture surfaceTexture) {
+        if (cameraTextureView.getSurfaceTexture() != surfaceTexture) {
+            if (cameraTextureView.isAvailable()) {
+                // Remove the old TextureView to properly detach the old SurfaceTexture from the GL
+                // Context.
+                removeView(cameraTextureView);
+                addView(cameraTextureView = new TextureView(getContext()), 0);
+                cameraTextureView.setLayerPaint(layerPaint);
+                requestLayout();
+            }
+
+            cameraTextureView.setSurfaceTexture(surfaceTexture);
+        }
+    }
+
+    @UiThread
+    Matrix getTransform(Matrix matrix) {
+        return cameraTextureView.getTransform(matrix);
+    }
+
+    @UiThread
+    int getViewFinderWidth() {
+        return cameraTextureView.getWidth();
+    }
+
+    @UiThread
+    int getViewFinderHeight() {
+        return cameraTextureView.getHeight();
+    }
+
+    @UiThread
+    void setTransform(final Matrix matrix) {
+        if (cameraTextureView != null) {
+            cameraTextureView.setTransform(matrix);
+        }
+    }
+
+    /**
+     * Returns the scale type used to scale the viewfinder.
+     *
+     * @return The current {@link ScaleType}.
+     */
+    public ScaleType getScaleType() {
+        return scaleType;
+    }
+
+    /**
+     * Sets the view finder scale type.
+     *
+     * <p>This controls how the view finder should be scaled and positioned within the view.
+     *
+     * @param scaleType The desired {@link ScaleType}.
+     */
+    public void setScaleType(ScaleType scaleType) {
+        if (scaleType != this.scaleType) {
+            this.scaleType = scaleType;
+            requestLayout();
+        }
+    }
+
+    /**
+     * Gets the current quality for image and video outputs.
+     *
+     * @return The current {@link Quality}. Currently only {@link Quality#HIGH} is supported.
+     * @hide Not currently connected to use cases.
+     */
+    public Quality getQuality() {
+        return cameraModule.getQuality();
+    }
+
+    /**
+     * Sets the quality for image and video outputs.
+     *
+     * @param quality The {@link Quality} used for image and video. Currently only {@link
+     *                Quality#HIGH} is supported.
+     * @throws UnsupportedOperationException if any quality other than HIGH is set.
+     * @hide Not currently connected to use cases.
+     */
+    public void setQuality(Quality quality) {
+        cameraModule.setQuality(quality);
+    }
+
+    /**
+     * Returns the scale type used to scale the viewfinder.
+     *
+     * @return The current {@link CaptureMode}.
+     */
+    public CaptureMode getCaptureMode() {
+        return cameraModule.getCaptureMode();
+    }
+
+    /**
+     * Sets the CameraView capture mode
+     *
+     * <p>This controls only image or video capture function is enabled or both are enabled.
+     *
+     * @param captureMode The desired {@link CaptureMode}.
+     */
+    public void setCaptureMode(CaptureMode captureMode) {
+        cameraModule.setCaptureMode(captureMode);
+    }
+
+    /**
+     * Returns the maximum duration of videos, or {@link #INDEFINITE_VIDEO_DURATION} if there is no
+     * timeout.
+     *
+     * @hide Not currently implemented.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public long getMaxVideoDuration() {
+        return cameraModule.getMaxVideoDuration();
+    }
+
+    /**
+     * Sets the maximum video duration before {@link OnVideoSavedListener#onVideoSaved(File)} is
+     * called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
+     */
+    private void setMaxVideoDuration(long duration) {
+        cameraModule.setMaxVideoDuration(duration);
+    }
+
+    /**
+     * Returns the maximum size of videos in bytes, or {@link #INDEFINITE_VIDEO_SIZE} if there is no
+     * timeout.
+     */
+    private long getMaxVideoSize() {
+        return cameraModule.getMaxVideoSize();
+    }
+
+    /**
+     * Sets the maximum video size in bytes before {@link OnVideoSavedListener#onVideoSaved(File)}
+     * is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
+     */
+    private void setMaxVideoSize(long size) {
+        cameraModule.setMaxVideoSize(size);
+    }
+
+    /**
+     * Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)}
+     * once when done.
+     *
+     * @param listener Listener which will receive success or failure callbacks.
+     */
+    public void takePicture(OnImageCapturedListener listener) {
+        cameraModule.takePicture(listener);
+    }
+
+    /**
+     * Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done.
+     *
+     * @param file     The destination.
+     * @param listener Listener which will receive success or failure callbacks.
+     */
+    public void takePicture(File file, OnImageSavedListener listener) {
+        cameraModule.takePicture(file, listener);
+    }
+
+    /**
+     * Takes a video and calls {@link OnVideoSavedListener#onVideoSaved(File)} when done.
+     *
+     * @param file The destination.
+     */
+    public void startRecording(File file, OnVideoSavedListener listener) {
+        cameraModule.startRecording(file, listener);
+    }
+
+    /** Stops an in progress video. */
+    public void stopRecording() {
+        cameraModule.stopRecording();
+    }
+
+    /** @return True if currently recording. */
+    public boolean isRecording() {
+        return cameraModule.isRecording();
+    }
+
+    /**
+     * Queries whether the current device has a camera with the specified direction.
+     *
+     * @return True if the device supports the direction.
+     * @throws IllegalStateException if the CAMERA permission is not currently granted.
+     */
+    @RequiresPermission(permission.CAMERA)
+    public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
+        return cameraModule.hasCameraWithLensFacing(lensFacing);
+    }
+
+    /**
+     * Toggles between the primary front facing camera and the primary back facing camera.
+     *
+     * <p>This will have no effect if not already bound to a lifecycle via {@link
+     * #bindToLifecycle(LifecycleOwner)}.
+     */
+    public void toggleCamera() {
+        cameraModule.toggleCamera();
+    }
+
+    /**
+     * Sets the desired camera lensFacing.
+     *
+     * <p>This will choose the primary camera with the specified camera lensFacing.
+     *
+     * <p>If called before {@link #bindToLifecycle(LifecycleOwner)}, this will set the camera to be
+     * used when first bound to the lifecycle. If the specified lensFacing is not supported by the
+     * device, as determined by {@link #hasCameraWithLensFacing(LensFacing)}, the first supported
+     * lensFacing will be chosen when {@link #bindToLifecycle(LifecycleOwner)} is called.
+     *
+     * <p>If called with {@code null} AFTER binding to the lifecycle, the behavior would be
+     * equivalent to unbind the use cases without the lifecycle having to be destroyed.
+     *
+     * @param lensFacing The desired camera lensFacing.
+     */
+    public void setCameraByLensFacing(@Nullable LensFacing lensFacing) {
+        cameraModule.setCameraByLensFacing(lensFacing);
+    }
+
+    /** Returns the currently selected {@link LensFacing}. */
+    @Nullable
+    public LensFacing getCameraLensFacing() {
+        return cameraModule.getLensFacing();
+    }
+
+    /**
+     * Focuses the camera on the given area.
+     *
+     * <p>Sets the focus and exposure metering rectangles. Coordinates for both X and Y dimensions
+     * are Limited from -1000 to 1000, where (0, 0) is the center of the image and the width/height
+     * represent the values from -1000 to 1000.
+     *
+     * @param focus    Area used to focus the camera.
+     * @param metering Area used for exposure metering.
+     */
+    public void focus(Rect focus, Rect metering) {
+        cameraModule.focus(focus, metering);
+    }
+
+    /** Gets the active flash strategy. */
+    public FlashMode getFlash() {
+        return cameraModule.getFlash();
+    }
+
+    /** Sets the active flash strategy. */
+    public void setFlash(FlashMode flashMode) {
+        cameraModule.setFlash(flashMode);
+    }
+
+    private int getRelativeCameraOrientation(boolean compensateForMirroring) {
+        return cameraModule.getRelativeCameraOrientation(compensateForMirroring);
+    }
+
+    private long delta() {
+        return System.currentTimeMillis() - downEventTimestamp;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
+        if (cameraModule.isPaused()) {
+            return false;
+        }
+        // Only forward the event to the pinch-to-zoom gesture detector when pinch-to-zoom is
+        // enabled.
+        if (isPinchToZoomEnabled()) {
+            pinchToZoomGestureDetector.onTouchEvent(event);
+        }
+        if (event.getPointerCount() == 2 && isPinchToZoomEnabled() && isZoomSupported()) {
+            return true;
+        }
+
+        // Camera focus
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                downEventTimestamp = System.currentTimeMillis();
+                break;
+            case MotionEvent.ACTION_UP:
+                if (delta() < ViewConfiguration.getLongPressTimeout()) {
+                    upEvent = event;
+                    performClick();
+                }
+                break;
+            default:
+                // Unhandled event.
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * Focus the position of the touch event, or focus the center of the viewfinder for
+     * accessibility events
+     */
+    @Override
+    public boolean performClick() {
+        super.performClick();
+
+        final float x = (upEvent != null) ? upEvent.getX() : getX() + getWidth() / 2f;
+        final float y = (upEvent != null) ? upEvent.getY() : getY() + getHeight() / 2f;
+        upEvent = null;
+        calculateTapArea(focusingRect, x, y, 1f);
+        calculateTapArea(meteringRect, x, y, 1.5f);
+        if (area(focusingRect) > 0 && area(meteringRect) > 0) {
+            focus(focusingRect, meteringRect);
+        }
+
+        return true;
+    }
+
+    /** Returns the width * height of the given rect */
+    private int area(Rect rect) {
+        return rect.width() * rect.height();
+    }
+
+    /** The area must be between -1000,-1000 and 1000,1000 */
+    private void calculateTapArea(Rect rect, float x, float y, float coefficient) {
+        int max = 1000;
+        int min = -1000;
+
+        // Default to 300 (1/6th the total area) and scale by the coefficient
+        int areaSize = (int) (300 * coefficient);
+
+        // Rotate the coordinates if the camera orientation is different
+        int width = getWidth();
+        int height = getHeight();
+
+        // Compensate orientation as it's mirrored on preview for forward facing cameras
+        boolean compensateForMirroring = (getCameraLensFacing() == LensFacing.FRONT);
+        int relativeCameraOrientation = getRelativeCameraOrientation(compensateForMirroring);
+        int temp;
+        float tempf;
+        switch (relativeCameraOrientation) {
+            case 90:
+                // Fall-through
+            case 270:
+                // We're horizontal. Swap width/height. Swap x/y.
+                temp = width;
+                //noinspection SuspiciousNameCombination
+                width = height;
+                height = temp;
+
+                tempf = x;
+                //noinspection SuspiciousNameCombination
+                x = y;
+                y = tempf;
+                break;
+            default:
+                break;
+        }
+
+        switch (relativeCameraOrientation) {
+            // Map to correct coordinates according to relativeCameraOrientation
+            case 90:
+                y = height - y;
+                break;
+            case 180:
+                x = width - x;
+                y = height - y;
+                break;
+            case 270:
+                x = width - x;
+                break;
+            default:
+                break;
+        }
+
+        // Swap x if it's a mirrored preview
+        if (compensateForMirroring) {
+            x = width - x;
+        }
+
+        // Grab the x, y position from within the View and normalize it to -1000 to 1000
+        x = min + distance(max, min) * (x / width);
+        y = min + distance(max, min) * (y / height);
+
+        // Modify the rect to the bounding area
+        rect.top = (int) y - areaSize / 2;
+        rect.left = (int) x - areaSize / 2;
+        rect.bottom = rect.top + areaSize;
+        rect.right = rect.left + areaSize;
+
+        // Cap at -1000 to 1000
+        rect.top = rangeLimit(rect.top, max, min);
+        rect.left = rangeLimit(rect.left, max, min);
+        rect.bottom = rangeLimit(rect.bottom, max, min);
+        rect.right = rangeLimit(rect.right, max, min);
+    }
+
+    private int rangeLimit(int val, int max, int min) {
+        return Math.min(Math.max(val, min), max);
+    }
+
+    float rangeLimit(float val, float max, float min) {
+        return Math.min(Math.max(val, min), max);
+    }
+
+    private int distance(int a, int b) {
+        return Math.abs(a - b);
+    }
+
+    /**
+     * Returns whether the view allows pinch-to-zoom.
+     *
+     * @return True if pinch to zoom is enabled.
+     */
+    public boolean isPinchToZoomEnabled() {
+        return isPinchToZoomEnabled;
+    }
+
+    /**
+     * Sets whether the view should allow pinch-to-zoom.
+     *
+     * <p>When enabled, the user can pinch the camera to zoom in/out. This only has an effect if the
+     * bound camera supports zoom.
+     *
+     * @param enabled True to enable pinch-to-zoom.
+     */
+    public void setPinchToZoomEnabled(boolean enabled) {
+        isPinchToZoomEnabled = enabled;
+    }
+
+    /**
+     * Returns the current zoom level.
+     *
+     * @return The current zoom level.
+     */
+    public float getZoomLevel() {
+        return cameraModule.getZoomLevel();
+    }
+
+    /**
+     * Sets the current zoom level.
+     *
+     * <p>Valid zoom values range from 1 to {@link #getMaxZoomLevel()}.
+     *
+     * @param zoomLevel The requested zoom level.
+     */
+    public void setZoomLevel(float zoomLevel) {
+        cameraModule.setZoomLevel(zoomLevel);
+    }
+
+    /**
+     * Returns the minimum zoom level.
+     *
+     * <p>For most cameras this should return a zoom level of 1. A zoom level of 1 corresponds to a
+     * non-zoomed image.
+     *
+     * @return The minimum zoom level.
+     */
+    public float getMinZoomLevel() {
+        return cameraModule.getMinZoomLevel();
+    }
+
+    /**
+     * Returns the maximum zoom level.
+     *
+     * <p>The zoom level corresponds to the ratio between both the widths and heights of a
+     * non-zoomed image and a maximally zoomed image for the selected camera.
+     *
+     * @return The maximum zoom level.
+     */
+    public float getMaxZoomLevel() {
+        return cameraModule.getMaxZoomLevel();
+    }
+
+    /**
+     * Returns whether the bound camera supports zooming.
+     *
+     * @return True if the camera supports zooming.
+     */
+    public boolean isZoomSupported() {
+        return cameraModule.isZoomSupported();
+    }
+
+    /**
+     * Turns on/off torch.
+     *
+     * @param torch True to turn on torch, false to turn off torch.
+     */
+    public void enableTorch(boolean torch) {
+        cameraModule.enableTorch(torch);
+    }
+
+    /**
+     * Returns current torch status.
+     *
+     * @return true if torch is on , otherwise false
+     */
+    public boolean isTorchOn() {
+        return cameraModule.isTorchOn();
+    }
+
+    /** Options for scaling the bounds of the view finder to the bounds of this view. */
+    public enum ScaleType {
+        /**
+         * Scale the view finder, maintaining the source aspect ratio, so the view finder fills the
+         * entire view. This will cause the view finder to crop the source image if the camera
+         * aspect ratio does not match the view aspect ratio.
+         */
+        CENTER_CROP(0),
+        /**
+         * Scale the view finder, maintaining the source aspect ratio, so the view finder is
+         * entirely contained within the view.
+         */
+        CENTER_INSIDE(1);
+
+        final int id;
+
+        ScaleType(int id) {
+            this.id = id;
+        }
+
+        static ScaleType fromId(int id) {
+            for (ScaleType st : values()) {
+                if (st.id == id) {
+                    return st;
+                }
+            }
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * Determines the resolution of CameraView's outputs. All resolutions are best attempts, and
+     * will fall to lower qualities if the Android device cannot support them. Resolutions also may
+     * change in the future (if, say, Android adds 8k resolution).
+     *
+     * <p>(*) {@link Quality#MAX} will output at 4k. (*) {@link Quality#HIGH} will output at 1080p.
+     * (*) {@link Quality#MEDIUM} will output at 720. (*) {@link Quality#LOW} will output at 480.
+     *
+     * @hide Not currently connected to use cases.
+     */
+    public enum Quality {
+        MAX(0),
+        HIGH(1),
+        MEDIUM(2),
+        LOW(3);
+
+        final int id;
+
+        Quality(int id) {
+            this.id = id;
+        }
+
+        static Quality fromId(int id) {
+            for (Quality f : values()) {
+                if (f.id == id) {
+                    return f;
+                }
+            }
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * The capture mode used by CameraView.
+     *
+     * <p>This enum can be used to determine which capture mode will be enabled for {@link
+     * CameraView}.
+     */
+    public enum CaptureMode {
+        /** A mode where image capture is enabled. */
+        IMAGE(0),
+        /** A mode where video capture is enabled. */
+        VIDEO(1),
+        /**
+         * A mode where both image capture and video capture are simultaneously enabled. Note that
+         * this mode may not be available on every device.
+         */
+        MIXED(2);
+
+        final int id;
+
+        CaptureMode(int id) {
+            this.id = id;
+        }
+
+        static CaptureMode fromId(int id) {
+            for (CaptureMode f : values()) {
+                if (f.id == id) {
+                    return f;
+                }
+            }
+            throw new IllegalArgumentException();
+        }
+    }
+
+    static class S extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+        private ScaleGestureDetector.OnScaleGestureListener listener;
+
+        void setRealGestureDetector(ScaleGestureDetector.OnScaleGestureListener l) {
+            listener = l;
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            return listener.onScale(detector);
+        }
+    }
+
+    private class PinchToZoomGestureDetector extends ScaleGestureDetector
+            implements ScaleGestureDetector.OnScaleGestureListener {
+        private static final float SCALE_MULTIPIER = 0.75f;
+        private final BaseInterpolator interpolator = new DecelerateInterpolator(2f);
+        private float normalizedScaleFactor = 0;
+
+        PinchToZoomGestureDetector(Context context) {
+            this(context, new S());
+        }
+
+        PinchToZoomGestureDetector(Context context, S s) {
+            super(context, s);
+            s.setRealGestureDetector(this);
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            normalizedScaleFactor += (detector.getScaleFactor() - 1f) * SCALE_MULTIPIER;
+            // Since the scale factor is normalized, it should always be in the range [0, 1]
+            normalizedScaleFactor = rangeLimit(normalizedScaleFactor, 1f, 0);
+
+            // Apply decelerate interpolation. This will cause the differences to seem less
+            // pronounced
+            // at higher zoom levels.
+            float transformedScale = interpolator.getInterpolation(normalizedScaleFactor);
+
+            // Transform back from normalized coordinates to the zoom scale
+            float zoomLevel =
+                    (getMaxZoomLevel() == getMinZoomLevel())
+                            ? getMinZoomLevel()
+                            : getMinZoomLevel()
+                                    + transformedScale * (getMaxZoomLevel() - getMinZoomLevel());
+
+            setZoomLevel(rangeLimit(zoomLevel, getMaxZoomLevel(), getMinZoomLevel()));
+            return true;
+        }
+
+        @Override
+        public boolean onScaleBegin(ScaleGestureDetector detector) {
+            float initialZoomLevel = getZoomLevel();
+            normalizedScaleFactor =
+                    (getMaxZoomLevel() == getMinZoomLevel())
+                            ? 0
+                            : (initialZoomLevel - getMinZoomLevel())
+                                    / (getMaxZoomLevel() - getMinZoomLevel());
+            return true;
+        }
+
+        @Override
+        public void onScaleEnd(ScaleGestureDetector detector) {
+        }
+    }
+}
diff --git a/camera/view/src/main/java/androidx/camera/view/CameraXModule.java b/camera/view/src/main/java/androidx/camera/view/CameraXModule.java
new file mode 100644
index 0000000..9871825
--- /dev/null
+++ b/camera/view/src/main/java/androidx/camera/view/CameraXModule.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view;
+
+import android.Manifest.permission;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresPermission;
+import androidx.annotation.UiThread;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraOrientationUtil;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCase.OnImageCapturedListener;
+import androidx.camera.core.ImageCaptureUseCase.OnImageSavedListener;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCase.OnVideoSavedListener;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.camera.view.CameraView.CaptureMode;
+import androidx.camera.view.CameraView.Quality;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** CameraX use case operation built on @{link androidx.camera.core}. */
+final class CameraXModule {
+    public static final String TAG = "CameraXModule";
+
+    private static final int MAX_VIEW_DIMENSION = 2000;
+    private static final float UNITY_ZOOM_SCALE = 1f;
+    private static final float ZOOM_NOT_SUPPORTED = UNITY_ZOOM_SCALE;
+    private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
+    private static final Rational ASPECT_RATIO_4_3 = new Rational(4, 3);
+
+    private final CameraManager cameraManager;
+    private final ViewFinderUseCaseConfiguration.Builder viewFinderConfigBuilder;
+    private final VideoCaptureUseCaseConfiguration.Builder videoCaptureConfigBuilder;
+    private final ImageCaptureUseCaseConfiguration.Builder imageCaptureConfigBuilder;
+    private final CameraView cameraView;
+    final AtomicBoolean videoIsRecording = new AtomicBoolean(false);
+    private CameraView.Quality quality = CameraView.Quality.HIGH;
+    private CameraView.CaptureMode captureMode = CaptureMode.IMAGE;
+    private long maxVideoDuration = CameraView.INDEFINITE_VIDEO_DURATION;
+    private long maxVideoSize = CameraView.INDEFINITE_VIDEO_SIZE;
+    private FlashMode flash = FlashMode.OFF;
+    @Nullable
+    private ImageCaptureUseCase imageCaptureUseCase;
+    @Nullable
+    private VideoCaptureUseCase videoCaptureUseCase;
+    @Nullable
+    ViewFinderUseCase viewFinderUseCase;
+    @Nullable
+    LifecycleOwner currentLifecycle;
+    private final LifecycleObserver currentLifecycleObserver =
+            new DefaultLifecycleObserver() {
+                @Override
+                public void onDestroy(LifecycleOwner owner) {
+                    if (owner == currentLifecycle) {
+                        clearCurrentLifecycle();
+                        viewFinderUseCase.removeViewFinderOutputListener();
+                    }
+                }
+            };
+    @Nullable
+    private LifecycleOwner newLifecycle;
+    private float zoomLevel = UNITY_ZOOM_SCALE;
+    @Nullable
+    private Rect cropRegion;
+    @Nullable
+    private CameraX.LensFacing cameraLensFacing = LensFacing.BACK;
+
+    public CameraXModule(CameraView view) {
+        this.cameraView = view;
+
+        cameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
+
+        viewFinderConfigBuilder =
+                new ViewFinderUseCaseConfiguration.Builder().setTargetName("ViewFinder");
+
+        imageCaptureConfigBuilder =
+                new ImageCaptureUseCaseConfiguration.Builder().setTargetName("ImageCapture");
+
+        videoCaptureConfigBuilder =
+                new VideoCaptureUseCaseConfiguration.Builder().setTargetName("VideoCapture");
+    }
+
+    /**
+     * Rescales view rectangle with dimensions in [-1000, 1000] to a corresponding rectangle in the
+     * sensor coordinate frame.
+     */
+    private static Rect rescaleViewRectToSensorRect(Rect view, Rect sensor) {
+        // Scale width and height.
+        int newWidth = Math.round(view.width() * sensor.width() / (float) MAX_VIEW_DIMENSION);
+        int newHeight = Math.round(view.height() * sensor.height() / (float) MAX_VIEW_DIMENSION);
+
+        // Scale top/left corner.
+        int halfViewDimension = MAX_VIEW_DIMENSION / 2;
+        int leftOffset =
+                Math.round(
+                        (view.left + halfViewDimension)
+                                * sensor.width()
+                                / (float) MAX_VIEW_DIMENSION)
+                        + sensor.left;
+        int topOffset =
+                Math.round(
+                        (view.top + halfViewDimension)
+                                * sensor.height()
+                                / (float) MAX_VIEW_DIMENSION)
+                        + sensor.top;
+
+        // Now, produce the scaled rect.
+        Rect scaled = new Rect();
+        scaled.left = leftOffset;
+        scaled.top = topOffset;
+        scaled.right = scaled.left + newWidth;
+        scaled.bottom = scaled.top + newHeight;
+        return scaled;
+    }
+
+    @RequiresPermission(permission.CAMERA)
+    public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
+        newLifecycle = lifecycleOwner;
+
+        if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
+            bindToLifecycleAfterViewMeasured();
+        }
+    }
+
+    @RequiresPermission(permission.CAMERA)
+    void bindToLifecycleAfterViewMeasured() {
+        if (newLifecycle == null) {
+            return;
+        }
+
+        clearCurrentLifecycle();
+        currentLifecycle = newLifecycle;
+        newLifecycle = null;
+        if (currentLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
+            currentLifecycle = null;
+            throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
+        }
+
+        int cameraOrientation;
+        try {
+            String cameraId;
+            Set<LensFacing> available = getAvailableCameraLensFacing();
+
+            if (available.isEmpty()) {
+                Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
+                cameraLensFacing = null;
+            }
+
+            // Ensure the current camera exists, or default to another camera
+            if (cameraLensFacing != null && !available.contains(cameraLensFacing)) {
+                Log.w(TAG, "Camera does not exist with direction " + cameraLensFacing);
+
+                // Default to the first available camera direction
+                cameraLensFacing = available.iterator().next();
+
+                Log.w(TAG, "Defaulting to primary camera with direction " + cameraLensFacing);
+            }
+
+            // Do not attempt to create use cases for a null cameraLensFacing. This could occur if
+            // the
+            // user explicitly sets the LensFacing to null, or if we determined there
+            // were no available cameras, which should be logged in the logic above.
+            if (cameraLensFacing == null) {
+                return;
+            }
+
+            cameraId = CameraX.getCameraWithLensFacing(cameraLensFacing);
+            if (cameraId == null) {
+                return;
+            }
+            CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+            cameraOrientation = cameraInfo.getSensorRotationDegrees();
+        } catch (Exception e) {
+            throw new IllegalStateException("Unable to bind to lifecycle.", e);
+        }
+
+        // Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
+        // ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
+        // is
+        // in CENTER_INSIDE mode.
+        if (getCaptureMode() == CaptureMode.IMAGE) {
+            imageCaptureConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_4_3);
+            viewFinderConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_4_3);
+        } else {
+            imageCaptureConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_16_9);
+            viewFinderConfigBuilder.setTargetAspectRatio(ASPECT_RATIO_16_9);
+        }
+
+        imageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
+        imageCaptureConfigBuilder.setLensFacing(cameraLensFacing);
+        imageCaptureUseCase = new ImageCaptureUseCase(imageCaptureConfigBuilder.build());
+
+        videoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
+        videoCaptureConfigBuilder.setLensFacing(cameraLensFacing);
+        videoCaptureUseCase = new VideoCaptureUseCase(videoCaptureConfigBuilder.build());
+        viewFinderConfigBuilder.setLensFacing(cameraLensFacing);
+
+        int relativeCameraOrientation = getRelativeCameraOrientation(false);
+
+        if (relativeCameraOrientation == 90 || relativeCameraOrientation == 270) {
+            viewFinderConfigBuilder.setTargetResolution(
+                    new Size(getMeasuredHeight(), getMeasuredWidth()));
+        } else {
+            viewFinderConfigBuilder.setTargetResolution(
+                    new Size(getMeasuredWidth(), getMeasuredHeight()));
+        }
+
+        viewFinderUseCase = new ViewFinderUseCase(viewFinderConfigBuilder.build());
+        viewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                output -> {
+                    boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180;
+                    int textureWidth =
+                            needReverse
+                                    ? output.getTextureSize().getHeight()
+                                    : output.getTextureSize().getWidth();
+                    int textureHeight =
+                            needReverse
+                                    ? output.getTextureSize().getWidth()
+                                    : output.getTextureSize().getHeight();
+                    onViewfinderSourceDimensUpdated(textureWidth, textureHeight);
+                    setSurfaceTexture(output.getSurfaceTexture());
+                });
+
+        if (getCaptureMode() == CaptureMode.IMAGE) {
+            CameraX.bindToLifecycle(currentLifecycle, imageCaptureUseCase, viewFinderUseCase);
+        } else if (getCaptureMode() == CaptureMode.VIDEO) {
+            CameraX.bindToLifecycle(currentLifecycle, videoCaptureUseCase, viewFinderUseCase);
+        } else {
+            CameraX.bindToLifecycle(
+                    currentLifecycle, imageCaptureUseCase, videoCaptureUseCase, viewFinderUseCase);
+        }
+        setZoomLevel(zoomLevel);
+        currentLifecycle.getLifecycle().addObserver(currentLifecycleObserver);
+        // Enable flash setting in ImageCaptureUseCase after use cases are created and binded.
+        setFlash(getFlash());
+    }
+
+    public void open() {
+        throw new UnsupportedOperationException(
+                "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
+    }
+
+    public void close() {
+        throw new UnsupportedOperationException(
+                "Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
+    }
+
+    public void takePicture(OnImageCapturedListener listener) {
+        if (imageCaptureUseCase == null) {
+            return;
+        }
+
+        if (getCaptureMode() == CaptureMode.VIDEO) {
+            throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
+        }
+
+        if (listener == null) {
+            throw new IllegalArgumentException("OnImageCapturedListener should not be empty");
+        }
+
+        imageCaptureUseCase.takePicture(listener);
+    }
+
+    public void takePicture(File saveLocation, OnImageSavedListener listener) {
+        if (imageCaptureUseCase == null) {
+            return;
+        }
+
+        if (getCaptureMode() == CaptureMode.VIDEO) {
+            throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
+        }
+
+        if (listener == null) {
+            throw new IllegalArgumentException("OnImageSavedListener should not be empty");
+        }
+
+        ImageCaptureUseCase.Metadata metadata = new ImageCaptureUseCase.Metadata();
+        metadata.isReversedHorizontal = cameraLensFacing == LensFacing.FRONT;
+        imageCaptureUseCase.takePicture(saveLocation, listener, metadata);
+    }
+
+    public void startRecording(File file, OnVideoSavedListener listener) {
+        if (videoCaptureUseCase == null) {
+            return;
+        }
+
+        if (getCaptureMode() == CaptureMode.IMAGE) {
+            throw new IllegalStateException("Can not record video under IMAGE capture mode.");
+        }
+
+        if (listener == null) {
+            throw new IllegalArgumentException("OnVideoSavedListener should not be empty");
+        }
+
+        videoIsRecording.set(true);
+        videoCaptureUseCase.startRecording(
+                file,
+                new VideoCaptureUseCase.OnVideoSavedListener() {
+                    @Override
+                    public void onVideoSaved(File savedFile) {
+                        videoIsRecording.set(false);
+                        listener.onVideoSaved(savedFile);
+                    }
+
+                    @Override
+                    public void onError(
+                            VideoCaptureUseCase.UseCaseError useCaseError,
+                            String message,
+                            @Nullable Throwable cause) {
+                        videoIsRecording.set(false);
+                        Log.e(TAG, message, cause);
+                        listener.onError(useCaseError, message, cause);
+                    }
+                });
+    }
+
+    public void stopRecording() {
+        if (videoCaptureUseCase == null) {
+            return;
+        }
+
+        videoCaptureUseCase.stopRecording();
+    }
+
+    public boolean isRecording() {
+        return videoIsRecording.get();
+    }
+
+    // TODO(b/124269166): Rethink how we can handle permissions here.
+    @SuppressLint("MissingPermission")
+    public void setCameraByLensFacing(@Nullable LensFacing lensFacing) {
+        // Setting same lens facing is a no-op, so check for that first
+        if (cameraLensFacing != lensFacing) {
+            // If we're not bound to a lifecycle, just update the camera that will be opened when we
+            // attach to a lifecycle.
+            cameraLensFacing = lensFacing;
+
+            if (currentLifecycle != null) {
+                // Re-bind to lifecycle with new camera
+                bindToLifecycle(currentLifecycle);
+            }
+        }
+    }
+
+    @RequiresPermission(permission.CAMERA)
+    public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
+        String cameraId;
+        try {
+            cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalStateException("Unable to query lens facing.", e);
+        }
+
+        return cameraId != null;
+    }
+
+    @Nullable
+    public LensFacing getLensFacing() {
+        return cameraLensFacing;
+    }
+
+    public void toggleCamera() {
+        // TODO(b/124269166): Rethink how we can handle permissions here.
+        @SuppressLint("MissingPermission")
+        Set<LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
+
+        if (availableCameraLensFacing.isEmpty()) {
+            return;
+        }
+
+        if (cameraLensFacing == null) {
+            setCameraByLensFacing(availableCameraLensFacing.iterator().next());
+            return;
+        }
+
+        if (cameraLensFacing == LensFacing.BACK
+                && availableCameraLensFacing.contains(LensFacing.FRONT)) {
+            setCameraByLensFacing(LensFacing.FRONT);
+            return;
+        }
+
+        if (cameraLensFacing == LensFacing.FRONT
+                && availableCameraLensFacing.contains(LensFacing.BACK)) {
+            setCameraByLensFacing(LensFacing.BACK);
+            return;
+        }
+    }
+
+    public void focus(Rect focus, Rect metering) {
+        if (viewFinderUseCase == null) {
+            // Nothing to focus on since we don't yet have a viewfinder
+            return;
+        }
+
+        Rect rescaledFocus;
+        Rect rescaledMetering;
+        try {
+            Rect sensorRegion;
+            if (cropRegion != null) {
+                sensorRegion = cropRegion;
+            } else {
+                sensorRegion = getSensorSize(getActiveCamera());
+            }
+            rescaledFocus = rescaleViewRectToSensorRect(focus, sensorRegion);
+            rescaledMetering = rescaleViewRectToSensorRect(metering, sensorRegion);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to rescale the focus and metering rectangles.", e);
+            return;
+        }
+
+        viewFinderUseCase.focus(rescaledFocus, rescaledMetering);
+    }
+
+    public float getZoomLevel() {
+        return zoomLevel;
+    }
+
+    public void setZoomLevel(float zoomLevel) {
+        // Set the zoom level in case it is set before binding to a lifecycle
+        this.zoomLevel = zoomLevel;
+
+        if (viewFinderUseCase == null) {
+            // Nothing to zoom on yet since we don't have a viewfinder. Defer calculating crop
+            // region.
+            return;
+        }
+
+        Rect sensorSize;
+        try {
+            sensorSize = getSensorSize(getActiveCamera());
+            if (sensorSize == null) {
+                Log.e(TAG, "Failed to get the sensor size.");
+                return;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get the sensor size.", e);
+            return;
+        }
+
+        float minZoom = getMinZoomLevel();
+        float maxZoom = getMaxZoomLevel();
+
+        if (this.zoomLevel < minZoom) {
+            Log.e(TAG, "Requested zoom level is less than minimum zoom level.");
+        }
+        if (this.zoomLevel > maxZoom) {
+            Log.e(TAG, "Requested zoom level is greater than maximum zoom level.");
+        }
+        this.zoomLevel = Math.max(minZoom, Math.min(maxZoom, this.zoomLevel));
+
+        float zoomScaleFactor =
+                (maxZoom == minZoom) ? minZoom : (this.zoomLevel - minZoom) / (maxZoom - minZoom);
+        int minWidth = Math.round(sensorSize.width() / maxZoom);
+        int minHeight = Math.round(sensorSize.height() / maxZoom);
+        int diffWidth = sensorSize.width() - minWidth;
+        int diffHeight = sensorSize.height() - minHeight;
+        float cropWidth = diffWidth * zoomScaleFactor;
+        float cropHeight = diffHeight * zoomScaleFactor;
+
+        Rect cropRegion =
+                new Rect(
+                        /*left=*/ (int) Math.ceil(cropWidth / 2 - 0.5f),
+                        /*top=*/ (int) Math.ceil(cropHeight / 2 - 0.5f),
+                        /*right=*/ (int) Math.floor(sensorSize.width() - cropWidth / 2 + 0.5f),
+                        /*bottom=*/ (int) Math.floor(sensorSize.height() - cropHeight / 2 + 0.5f));
+
+        if (cropRegion.width() < 50 || cropRegion.height() < 50) {
+            Log.e(TAG, "Crop region is too small to compute 3A stats, so ignoring further zoom.");
+            return;
+        }
+        this.cropRegion = cropRegion;
+
+        viewFinderUseCase.zoom(cropRegion);
+    }
+
+    public float getMinZoomLevel() {
+        return UNITY_ZOOM_SCALE;
+    }
+
+    public float getMaxZoomLevel() {
+        try {
+            CameraCharacteristics characteristics =
+                    cameraManager.getCameraCharacteristics(getActiveCamera());
+            Float maxZoom =
+                    characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
+            if (maxZoom == null) {
+                return ZOOM_NOT_SUPPORTED;
+            }
+            if (maxZoom == ZOOM_NOT_SUPPORTED) {
+                return ZOOM_NOT_SUPPORTED;
+            }
+            return maxZoom;
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get SCALER_AVAILABLE_MAX_DIGITAL_ZOOM.", e);
+        }
+        return ZOOM_NOT_SUPPORTED;
+    }
+
+    public boolean isZoomSupported() {
+        return getMaxZoomLevel() != ZOOM_NOT_SUPPORTED;
+    }
+
+    // TODO(b/124269166): Rethink how we can handle permissions here.
+    @SuppressLint("MissingPermission")
+    private void rebindToLifecycle() {
+        if (currentLifecycle != null) {
+            bindToLifecycle(currentLifecycle);
+        }
+    }
+
+    int getRelativeCameraOrientation(boolean compensateForMirroring) {
+        int rotationDegrees;
+        try {
+            String cameraId = CameraX.getCameraWithLensFacing(getLensFacing());
+            CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+            rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation());
+            if (compensateForMirroring) {
+                rotationDegrees = (360 - rotationDegrees) % 360;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to query camera", e);
+            rotationDegrees = 0;
+        }
+
+        return rotationDegrees;
+    }
+
+    public CameraView.Quality getQuality() {
+        return quality;
+    }
+
+    public void setQuality(Quality quality) {
+        if (quality != Quality.HIGH) {
+            throw new UnsupportedOperationException("Only supported Quality is HIGH");
+        }
+        this.quality = quality;
+    }
+
+    public void invalidateView() {
+        transformPreview();
+        updateViewInfo();
+    }
+
+    void clearCurrentLifecycle() {
+        if (currentLifecycle != null) {
+            // Remove previous use cases
+            CameraX.unbind(imageCaptureUseCase, videoCaptureUseCase, viewFinderUseCase);
+        }
+
+        currentLifecycle = null;
+    }
+
+    private Rect getSensorSize(String cameraId) throws CameraAccessException {
+        CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
+        return characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+    }
+
+    String getActiveCamera() throws CameraInfoUnavailableException {
+        return CameraX.getCameraWithLensFacing(cameraLensFacing);
+    }
+
+    @UiThread
+    private void transformPreview() {
+        int viewfinderWidth = getViewFinderWidth();
+        int viewfinderHeight = getViewFinderHeight();
+        int displayOrientation = getDisplayRotationDegrees();
+
+        Matrix matrix = new Matrix();
+
+        // Apply rotation of the display
+        int rotation = -displayOrientation;
+
+        int px = (int) Math.round(viewfinderWidth / 2d);
+        int py = (int) Math.round(viewfinderHeight / 2d);
+
+        matrix.postRotate(rotation, px, py);
+
+        if (displayOrientation == 90 || displayOrientation == 270) {
+            // Swap width and height
+            float xScale = viewfinderWidth / (float) viewfinderHeight;
+            float yScale = viewfinderHeight / (float) viewfinderWidth;
+
+            matrix.postScale(xScale, yScale, px, py);
+        }
+
+        setTransform(matrix);
+    }
+
+    // Update view related information used in use cases
+    private void updateViewInfo() {
+        if (imageCaptureUseCase != null) {
+            imageCaptureUseCase.setTargetAspectRatio(new Rational(getWidth(), getHeight()));
+            imageCaptureUseCase.setTargetRotation(getDisplaySurfaceRotation());
+        }
+
+        if (videoCaptureUseCase != null) {
+            videoCaptureUseCase.setTargetRotation(getDisplaySurfaceRotation());
+        }
+    }
+
+    @RequiresPermission(permission.CAMERA)
+    private Set<LensFacing> getAvailableCameraLensFacing() {
+        // Start with all camera directions
+        Set<LensFacing> available = new LinkedHashSet<>(Arrays.asList(LensFacing.values()));
+
+        // If we're bound to a lifecycle, remove unavailable cameras
+        if (currentLifecycle != null) {
+            if (!hasCameraWithLensFacing(LensFacing.BACK)) {
+                available.remove(LensFacing.BACK);
+            }
+
+            if (!hasCameraWithLensFacing(LensFacing.FRONT)) {
+                available.remove(LensFacing.FRONT);
+            }
+        }
+
+        return available;
+    }
+
+    public FlashMode getFlash() {
+        return flash;
+    }
+
+    public void setFlash(FlashMode flash) {
+        this.flash = flash;
+
+        if (imageCaptureUseCase == null) {
+            // Do nothing if there is no imageCaptureUseCase
+            return;
+        }
+
+        imageCaptureUseCase.setFlashMode(flash);
+    }
+
+    public void enableTorch(boolean torch) {
+        if (viewFinderUseCase == null) {
+            return;
+        }
+        viewFinderUseCase.enableTorch(torch);
+    }
+
+    public boolean isTorchOn() {
+        if (viewFinderUseCase == null) {
+            return false;
+        }
+        return viewFinderUseCase.isTorchOn();
+    }
+
+    public Context getContext() {
+        return cameraView.getContext();
+    }
+
+    public int getWidth() {
+        return cameraView.getWidth();
+    }
+
+    public int getHeight() {
+        return cameraView.getHeight();
+    }
+
+    public int getDisplayRotationDegrees() {
+        return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
+    }
+
+    protected int getDisplaySurfaceRotation() {
+        return cameraView.getDisplaySurfaceRotation();
+    }
+
+    public void setSurfaceTexture(SurfaceTexture st) {
+        cameraView.setSurfaceTexture(st);
+    }
+
+    private int getViewFinderWidth() {
+        return cameraView.getViewFinderWidth();
+    }
+
+    private int getViewFinderHeight() {
+        return cameraView.getViewFinderHeight();
+    }
+
+    private int getMeasuredWidth() {
+        return cameraView.getMeasuredWidth();
+    }
+
+    private int getMeasuredHeight() {
+        return cameraView.getMeasuredHeight();
+    }
+
+    void setTransform(final Matrix matrix) {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            cameraView.post(
+                    new Runnable() {
+                        @Override
+                        public void run() {
+                            setTransform(matrix);
+                        }
+                    });
+        } else {
+            cameraView.setTransform(matrix);
+        }
+    }
+
+    /**
+     * Notify the view that the source dimensions have changed.
+     *
+     * <p>This will allow the view to layout the viewfinder to display the correct aspect ratio.
+     *
+     * @param width  width of camera source buffers.
+     * @param height height of camera source buffers.
+     */
+    private void onViewfinderSourceDimensUpdated(int width, int height) {
+        cameraView.onViewfinderSourceDimensUpdated(width, height);
+    }
+
+    public CameraView.CaptureMode getCaptureMode() {
+        return captureMode;
+    }
+
+    public void setCaptureMode(CameraView.CaptureMode captureMode) {
+        this.captureMode = captureMode;
+        rebindToLifecycle();
+    }
+
+    public long getMaxVideoDuration() {
+        return maxVideoDuration;
+    }
+
+    public void setMaxVideoDuration(long duration) {
+        maxVideoDuration = duration;
+    }
+
+    public long getMaxVideoSize() {
+        return maxVideoSize;
+    }
+
+    public void setMaxVideoSize(long size) {
+        maxVideoSize = size;
+    }
+
+    public boolean isPaused() {
+        return false;
+    }
+}
diff --git a/camera/view/src/main/res-public/values/public_attrs.xml b/camera/view/src/main/res-public/values/public_attrs.xml
new file mode 100644
index 0000000..3202640
--- /dev/null
+++ b/camera/view/src/main/res-public/values/public_attrs.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!-- Definitions of attributes to be exposed as public -->
+<resources>
+    <!-- CameraView -->
+    <public name="scaleType" type="attr" />
+    <public name="direction" type="attr" />
+    <public name="captureMode" type="attr" />
+    <public name="flash" type="attr" />
+</resources>
diff --git a/camera/view/src/main/res/values/attrs.xml b/camera/view/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..9b01df2
--- /dev/null
+++ b/camera/view/src/main/res/values/attrs.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<resources>
+    <declare-styleable name="CameraView">
+        <attr name="scaleType" format="enum">
+            <enum name="centerCrop" value="0" />
+            <enum name="centerInside" value="1" />
+        </attr>
+        <attr name="lensFacing" format="enum">
+            <enum name="none" value="0" />
+            <enum name="front" value="1" />
+            <enum name="back" value="2" />
+        </attr>
+        <attr name="quality" format="enum">
+            <enum name="max" value="0" />
+            <enum name="high" value="1" />
+            <enum name="medium" value="2" />
+            <enum name="low" value="3" />
+        </attr>
+        <attr name="captureMode" format="enum">
+            <enum name="image" value="0" />
+            <enum name="video" value="1" />
+            <enum name="mixed" value="2" />
+        </attr>
+        <attr name="flash" format="enum">
+            <enum name="auto" value="1" />
+            <enum name="on" value="2" />
+            <enum name="off" value="4" />
+        </attr>
+
+        <attr name="pinchToZoomEnabled" format="boolean" />
+    </declare-styleable>
+</resources>