[go: nahoru, domu]

Merge changes I0feef20b,Idda6df2f,I07a66c46 into androidx-master-dev
am: 74222accd6

Change-Id: I09fdec75305380556518d74dd5711bd3f0301bf3
diff --git a/appcompat/res/values-as/strings.xml b/appcompat/res/values-as/strings.xml
deleted file mode 100644
index 700c8fb..0000000
--- a/appcompat/res/values-as/strings.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--  Copyright (C) 2012 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 xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="abc_action_mode_done" msgid="2571498368472823490">"সম্পন্ন হ’ল"</string>
-    <string name="abc_action_bar_home_description" msgid="7903180715631665059">"গৃহ পৃষ্ঠালৈ যাওক"</string>
-    <string name="abc_action_bar_up_description" msgid="6794660482873516081">"ওপৰলৈ যাওক"</string>
-    <string name="abc_action_menu_overflow_description" msgid="1155814932213556626">"অধিক বিকল্প"</string>
-    <string name="abc_toolbar_collapse_description" msgid="6389460216547290468">"সংকোচন কৰক"</string>
-    <string name="abc_searchview_description_search" msgid="5466662225065974044">"সন্ধান"</string>
-    <string name="abc_search_hint" msgid="940844115270746197">"সন্ধান কৰক…"</string>
-    <string name="abc_searchview_description_query" msgid="908784302972860853">"সন্ধান কৰা প্ৰশ্ন"</string>
-    <string name="abc_searchview_description_clear" msgid="1769270744562318534">"সন্ধান কৰা প্ৰশ্ন মচক"</string>
-    <string name="abc_searchview_description_submit" msgid="8203855622131699655">"প্ৰশ্ন দাখিল কৰক"</string>
-    <string name="abc_searchview_description_voice" msgid="3478748990613108725">"কণ্ঠধ্বনিৰ দ্বাৰা সন্ধান"</string>
-    <string name="abc_activitychooserview_choose_application" msgid="1798588241954930982">"কোনো এপ্ বাছনি কৰক"</string>
-    <string name="abc_activity_chooser_view_see_all" msgid="3732416590524162402">"সকলো চাওক"</string>
-    <string name="abc_shareactionprovider_share_with_application" msgid="9009661856846212431">"<xliff:g id="APPLICATION_NAME">%s</xliff:g>ৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
-    <string name="abc_shareactionprovider_share_with" msgid="2650565705514630347">"ইয়াৰ জৰিয়তে শ্বেয়াৰ কৰক"</string>
-    <string name="abc_capital_on" msgid="7831734969929204599">"অন"</string>
-    <string name="abc_capital_off" msgid="3403923230105792483">"অফ"</string>
-    <string name="search_menu_title" msgid="730395136688082741">"সন্ধান"</string>
-    <string name="abc_prepend_shortcut_label" msgid="3570106412128999382">"Menu+"</string>
-    <string name="abc_menu_meta_shortcut_label" msgid="8046416353848716905">"Meta+"</string>
-    <string name="abc_menu_ctrl_shortcut_label" msgid="944415252197684443">"Ctrl+"</string>
-    <string name="abc_menu_alt_shortcut_label" msgid="5725160506500770567">"Alt+"</string>
-    <string name="abc_menu_shift_shortcut_label" msgid="3271697756921353410">"Shift+"</string>
-    <string name="abc_menu_sym_shortcut_label" msgid="8327365089695024394">"Sym+"</string>
-    <string name="abc_menu_function_shortcut_label" msgid="4974283687810130415">"Function+"</string>
-    <string name="abc_menu_space_shortcut_label" msgid="2304645930658438191">"space"</string>
-    <string name="abc_menu_enter_shortcut_label" msgid="6840127756824236027">"enter"</string>
-    <string name="abc_menu_delete_shortcut_label" msgid="129742188101734366">"delete"</string>
-</resources>
diff --git a/appcompat/res/values-ca/strings.xml b/appcompat/res/values-ca/strings.xml
index b558afa2..705de23 100644
--- a/appcompat/res/values-ca/strings.xml
+++ b/appcompat/res/values-ca/strings.xml
@@ -18,7 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="abc_action_mode_done" msgid="2571498368472823490">"Fet"</string>
     <string name="abc_action_bar_home_description" msgid="7903180715631665059">"Navega a la pàgina d\'inici"</string>
-    <string name="abc_action_bar_up_description" msgid="6794660482873516081">"Navega cap amunt"</string>
+    <string name="abc_action_bar_up_description" msgid="6794660482873516081">"Navega cap a dalt"</string>
     <string name="abc_action_menu_overflow_description" msgid="1155814932213556626">"Més opcions"</string>
     <string name="abc_toolbar_collapse_description" msgid="6389460216547290468">"Replega"</string>
     <string name="abc_searchview_description_search" msgid="5466662225065974044">"Cerca"</string>
diff --git a/biometric/res/values-ar/strings.xml b/biometric/res/values-ar/strings.xml
index 09bb290..a879778 100644
--- a/biometric/res/values-ar/strings.xml
+++ b/biometric/res/values-ar/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"ليست هناك بصمات إصبع مسجَّلة."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"لا يحتوي هذا الجهاز على جهاز استشعار بصمات الأصابع."</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"تم إلغاء تشغيل بصمة الإصبع بواسطة المستخدم."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"خطأ غير معروف"</string>
 </resources>
diff --git a/biometric/res/values-ca/strings.xml b/biometric/res/values-ca/strings.xml
index 7f5c4ac..01d08ef 100644
--- a/biometric/res/values-ca/strings.xml
+++ b/biometric/res/values-ca/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"No s\'ha registrat cap empremta digital."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Aquest dispositiu no té sensor d\'empremtes digitals"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"L\'usuari ha cancel·lat l\'operació d\'empremta digital."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Error desconegut"</string>
 </resources>
diff --git a/biometric/res/values-de/strings.xml b/biometric/res/values-de/strings.xml
index 2c38bd3..8c4a890 100644
--- a/biometric/res/values-de/strings.xml
+++ b/biometric/res/values-de/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Keine Fingerabdrücke erfasst."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Dieses Gerät hat keinen Fingerabdrucksensor"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Vorgang der Fingerabdruckauthentifizierung vom Nutzer abgebrochen."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Unbekannter Fehler"</string>
 </resources>
diff --git a/biometric/res/values-es/strings.xml b/biometric/res/values-es/strings.xml
index f507aab..9f5183a 100644
--- a/biometric/res/values-es/strings.xml
+++ b/biometric/res/values-es/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"No se ha registrado ninguna huella digital."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"El dispositivo no tiene ningún sensor de huellas digitales"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"El usuario ha cancelado la operación de huella digital."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Error desconocido"</string>
 </resources>
diff --git a/biometric/res/values-eu/strings.xml b/biometric/res/values-eu/strings.xml
index e81f79a..cc0ac0a 100644
--- a/biometric/res/values-eu/strings.xml
+++ b/biometric/res/values-eu/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Ez da erregistratu hatz-markarik."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Gailu honek ez du hatz-marken sentsorerik"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Erabiltzaileak bertan behera utzi du hatz-marka bidezko eragiketa."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Errore ezezaguna"</string>
 </resources>
diff --git a/biometric/res/values-fr-rCA/strings.xml b/biometric/res/values-fr-rCA/strings.xml
index eaf5ca0..5ecac9e 100644
--- a/biometric/res/values-fr-rCA/strings.xml
+++ b/biometric/res/values-fr-rCA/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Aucune empreinte digitale enregistrée."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Cet appareil ne possède pas de capteur d\'empreintes digitales"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"L\'opération d\'authentification par empreinte digitale a été annulée par l\'utilisateur."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Erreur inconnue"</string>
 </resources>
diff --git a/biometric/res/values-fr/strings.xml b/biometric/res/values-fr/strings.xml
index aea80df..7511ebc 100644
--- a/biometric/res/values-fr/strings.xml
+++ b/biometric/res/values-fr/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Aucune empreinte digitale enregistrée."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Aucun lecteur d\'empreinte digitale n\'est installé sur cet appareil"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Opération d\'authentification par empreinte digitale annulée par l\'utilisateur."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Erreur inconnue"</string>
 </resources>
diff --git a/biometric/res/values-hy/strings.xml b/biometric/res/values-hy/strings.xml
index 2475c3b..3edc356 100644
--- a/biometric/res/values-hy/strings.xml
+++ b/biometric/res/values-hy/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Գրանցված մատնահետքեր չկան:"</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Սարքը չունի մատնահետքերի սկաներ"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Մատնահետքով նույնականացման գործողությունը չեղարկվել է օգտատիրոջ կողմից:"</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Անհայտ սխալ"</string>
 </resources>
diff --git a/biometric/res/values-kk/strings.xml b/biometric/res/values-kk/strings.xml
index 64cb3a5..4c6fb3e 100644
--- a/biometric/res/values-kk/strings.xml
+++ b/biometric/res/values-kk/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Саусақ іздері тіркелмеген."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Бұл құрылғыда саусақ ізін оқу сканері жоқ"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Пайдаланушы саусақ ізі операциясынан бас тартты."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Белгісіз қате"</string>
 </resources>
diff --git a/biometric/res/values-kn/strings.xml b/biometric/res/values-kn/strings.xml
index e9dd213..f773393 100644
--- a/biometric/res/values-kn/strings.xml
+++ b/biometric/res/values-kn/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"ಯಾವುದೇ ಫಿಂಗರ್‌ಪ್ರಿಂಟ್‌ ಅನ್ನು ನೋಂದಣಿ ಮಾಡಿಲ್ಲ."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"ಈ ಸಾಧನವು ಫಿಂಗರ್‌ಪ್ರಿಂಟ್‌ ಸೆನ್ಸಾರ್ ಅನ್ನು ಹೊಂದಿಲ್ಲ"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"ಬಳಕೆದಾರರಿಂದ ಫಿಂಗರ್‌ ಫ್ರಿಂಟ್‌ ಕಾರ್ಯಾಚರಣೆಯನ್ನು ರದ್ದುಪಡಿಸಲಾಗಿದೆ."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"ಅಪರಿಚಿತ ದೋಷ"</string>
 </resources>
diff --git a/biometric/res/values-mr/strings.xml b/biometric/res/values-mr/strings.xml
index 06e9a0a..d3f28b15 100644
--- a/biometric/res/values-mr/strings.xml
+++ b/biometric/res/values-mr/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"कोणत्याही फिंगरप्रिंटची नोंद झाली नाही."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"या डिव्हाइसवर फिंगरप्रिंट सेन्सर नाही"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"वापरकर्त्याने फिंगरप्रिंट ऑपरेशन रद्द केले."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"अज्ञात एरर"</string>
 </resources>
diff --git a/biometric/res/values-pt-rPT/strings.xml b/biometric/res/values-pt-rPT/strings.xml
index 0790e10..1215739 100644
--- a/biometric/res/values-pt-rPT/strings.xml
+++ b/biometric/res/values-pt-rPT/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Nenhuma impressão digital registada."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Este dispositivo não tem sensor de impressões digitais."</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Operação de impressão digital cancelada pelo utilizador."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Erro desconhecido."</string>
 </resources>
diff --git a/biometric/res/values-sk/strings.xml b/biometric/res/values-sk/strings.xml
index 8ea6570..8644574 100644
--- a/biometric/res/values-sk/strings.xml
+++ b/biometric/res/values-sk/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Neregistrovali ste žiadne odtlačky prstov."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Toto zariadenie nemá senzor odtlačkov prstov"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Overenie odtlačku prsta zrušil používateľ."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Neznáma chyba"</string>
 </resources>
diff --git a/biometric/res/values-ta/strings.xml b/biometric/res/values-ta/strings.xml
index 6f2dc1b..ad90b59 100644
--- a/biometric/res/values-ta/strings.xml
+++ b/biometric/res/values-ta/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"கைரேகைப் பதிவுகள் எதுவுமில்லை."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"இந்தச் சாதனத்தில் கைரேகை சென்சார் இல்லை"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"கைரேகைச் சரிபார்ப்பு பயனரால் ரத்துசெய்யப்பட்டது."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"அறியப்படாத பிழை"</string>
 </resources>
diff --git a/biometric/res/values-vi/strings.xml b/biometric/res/values-vi/strings.xml
index 6e04b1c..793d91c 100644
--- a/biometric/res/values-vi/strings.xml
+++ b/biometric/res/values-vi/strings.xml
@@ -24,7 +24,5 @@
     <string name="fingerprint_error_no_fingerprints" msgid="3350805046152877040">"Chưa đăng ký vân tay."</string>
     <string name="fingerprint_error_hw_not_present" msgid="1176237289575184578">"Thiết bị này không có cảm biến vân tay"</string>
     <string name="fingerprint_error_user_canceled" msgid="3421037373085129417">"Người dùng đã hủy thao tác dùng dấu vân tay."</string>
-    <!-- no translation found for fingerprint_error_lockout (1651062876313169162) -->
-    <skip />
     <string name="default_error_msg" msgid="7497355367608150274">"Lỗi không xác định"</string>
 </resources>
diff --git a/buildSrc/src/main/kotlin/androidx/build/UnpublishedLibraryGroups.kt b/buildSrc/src/main/kotlin/androidx/build/UnpublishedLibraryGroups.kt
new file mode 100644
index 0000000..a721610
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/UnpublishedLibraryGroups.kt
@@ -0,0 +1,11 @@
+package androidx.build
+
+/**
+ * Library groups for unpublished libraries.
+ *
+ * <p>This is being used temporarily for development of androidx.camera.
+ * TODO(b/124783972): Merge into LibraryGroups.kt when ready.
+ */
+object UnpublishedLibraryGroups {
+    val CAMERA = LibraryGroup("androidx.camera", false)
+}
diff --git a/buildSrc/src/main/kotlin/androidx/build/UnpublishedLibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/UnpublishedLibraryVersions.kt
new file mode 100644
index 0000000..4ac562e
--- /dev/null
+++ b/buildSrc/src/main/kotlin/androidx/build/UnpublishedLibraryVersions.kt
@@ -0,0 +1,11 @@
+package androidx.build
+
+/**
+ * Library versions for unpublished libraries.
+ *
+ * <p>This is being used temporarily for development of androidx.camera.
+ * TODO(b/124783972): Merge into LibraryVersions.kt when ready.
+ */
+object UnpublishedLibraryVersions {
+    val CAMERA = Version("1.0.0-alpha01")
+}
\ No newline at end of file
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/build.gradle b/camera/camera2/build.gradle
new file mode 100644
index 0000000..a90f50a
--- /dev/null
+++ b/camera/camera2/build.gradle
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+// TODO(b/124783972): Switch to androidx.build.LibraryVersions and androidx.build.LibraryGroups when ready
+import androidx.build.UnpublishedLibraryVersions
+import androidx.build.UnpublishedLibraryGroups
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    api(project(":camera:camera-core"))
+    implementation("androidx.core:core:1.0.0")
+    implementation("androidx.annotation:annotation:1.0.0")
+
+    implementation(GUAVA_LISTENABLE_FUTURE)
+
+    annotationProcessor(AUTO_VALUE)
+
+    testImplementation(TEST_CORE)
+    testImplementation(JUNIT)
+    testImplementation(TEST_RUNNER)
+    testImplementation(TRUTH)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+    testImplementation(project(":camera:camera-testing"))
+
+    androidTestImplementation(ARCH_LIFECYCLE_LIVEDATA_CORE)
+    androidTestImplementation(TEST_EXT_JUNIT)
+    androidTestImplementation(TEST_CORE)
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(TEST_RULES)
+    androidTestImplementation(TRUTH)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
+    androidTestImplementation(project(":camera:camera-testing"))
+}
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+}
+supportLibrary {
+    name = "Jetpack Camera Library Camera2 Implementation/Extensions"
+    publish = true
+    mavenVersion = UnpublishedLibraryVersions.CAMERA
+    mavenGroup = UnpublishedLibraryGroups.CAMERA
+    inceptionYear = "2019"
+    description = "Camera2 implementation and extensions for the Jetpack Camera Library, a " +
+            "library providing a consistent and reliable camera foundation that enables great " +
+            "camera driven experiences across all of Android."
+}
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..47dd518
--- /dev/null
+++ b/camera/camera2/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?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.test">
+
+    <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <application>
+        <activity
+            android:name="androidx.camera.testing.fakes.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/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultTest.java
new file mode 100644
index 0000000..9789a2c
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraCaptureResultTest.java
@@ -0,0 +1,277 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class Camera2CameraCaptureResultTest {
+
+    private CaptureResult mCaptureResult;
+    private Camera2CameraCaptureResult mCamera2CameraCaptureResult;
+    private Object mTag = null;
+
+    @Before
+    public void setUp() {
+        mCaptureResult = Mockito.mock(CaptureResult.class);
+        mCamera2CameraCaptureResult = new Camera2CameraCaptureResult(mTag, mCaptureResult);
+    }
+
+    @Test
+    public void getAfMode_withNull() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_MODE)).thenReturn(null);
+        assertThat(mCamera2CameraCaptureResult.getAfMode()).isEqualTo(AfMode.UNKNOWN);
+    }
+
+    @Test
+    public void getAfMode_withAfModeOff() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_OFF);
+        assertThat(mCamera2CameraCaptureResult.getAfMode()).isEqualTo(AfMode.OFF);
+    }
+
+    @Test
+    public void getAfMode_withAfModeEdof() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_EDOF);
+        assertThat(mCamera2CameraCaptureResult.getAfMode()).isEqualTo(AfMode.OFF);
+    }
+
+    @Test
+    public void getAfMode_withAfModeAuto() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_AUTO);
+        assertThat(mCamera2CameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_MANUAL_AUTO);
+    }
+
+    @Test
+    public void getAfMode_withAfModeMacro() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_MACRO);
+        assertThat(mCamera2CameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_MANUAL_AUTO);
+    }
+
+    @Test
+    public void getAfMode_withAfModeContinuousPicture() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+        assertThat(mCamera2CameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_CONTINUOUS_AUTO);
+    }
+
+    @Test
+    public void getAfMode_withAfModeContinuousVideo() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_MODE))
+                .thenReturn(CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO);
+        assertThat(mCamera2CameraCaptureResult.getAfMode()).isEqualTo(AfMode.ON_CONTINUOUS_AUTO);
+    }
+
+    @Test
+    public void getAfState_withNull() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(null);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.UNKNOWN);
+    }
+
+    @Test
+    public void getAfState_withAfStateInactive() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_INACTIVE);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.INACTIVE);
+    }
+
+    @Test
+    public void getAfState_withAfStateActiveScan() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+    }
+
+    @Test
+    public void getAfState_withAfStatePassiveScan() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+    }
+
+    @Test
+    public void getAfState_withAfStatePassiveUnfocused() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.SCANNING);
+    }
+
+    @Test
+    public void getAfState_withAfStatePassiveFocused() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.FOCUSED);
+    }
+
+    @Test
+    public void getAfState_withAfStateFocusedLocked() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.LOCKED_FOCUSED);
+    }
+
+    @Test
+    public void getAfState_withAfStateNotFocusedLocked() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AF_STATE))
+                .thenReturn(CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED);
+        assertThat(mCamera2CameraCaptureResult.getAfState()).isEqualTo(AfState.LOCKED_NOT_FOCUSED);
+    }
+
+    @Test
+    public void getAeState_withNull() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(null);
+        assertThat(mCamera2CameraCaptureResult.getAeState()).isEqualTo(AeState.UNKNOWN);
+    }
+
+    @Test
+    public void getAeState_withAeStateInactive() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_INACTIVE);
+        assertThat(mCamera2CameraCaptureResult.getAeState()).isEqualTo(AeState.INACTIVE);
+    }
+
+    @Test
+    public void getAeState_withAeStateSearching() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_SEARCHING);
+        assertThat(mCamera2CameraCaptureResult.getAeState()).isEqualTo(AeState.SEARCHING);
+    }
+
+    @Test
+    public void getAeState_withAeStatePrecapture() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_PRECAPTURE);
+        assertThat(mCamera2CameraCaptureResult.getAeState()).isEqualTo(AeState.SEARCHING);
+    }
+
+    @Test
+    public void getAeState_withAeStateFlashRequired() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED);
+        assertThat(mCamera2CameraCaptureResult.getAeState()).isEqualTo(AeState.FLASH_REQUIRED);
+    }
+
+    @Test
+    public void getAeState_withAeStateConverged() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_CONVERGED);
+        assertThat(mCamera2CameraCaptureResult.getAeState()).isEqualTo(AeState.CONVERGED);
+    }
+
+    @Test
+    public void getAeState_withAeStateLocked() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AE_STATE))
+                .thenReturn(CaptureResult.CONTROL_AE_STATE_LOCKED);
+        assertThat(mCamera2CameraCaptureResult.getAeState()).isEqualTo(AeState.LOCKED);
+    }
+
+    @Test
+    public void getAwbState_withNull() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AWB_STATE)).thenReturn(null);
+        assertThat(mCamera2CameraCaptureResult.getAwbState()).isEqualTo(AwbState.UNKNOWN);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateInactive() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_INACTIVE);
+        assertThat(mCamera2CameraCaptureResult.getAwbState()).isEqualTo(AwbState.INACTIVE);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateSearching() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_SEARCHING);
+        assertThat(mCamera2CameraCaptureResult.getAwbState()).isEqualTo(AwbState.METERING);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateConverged() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_CONVERGED);
+        assertThat(mCamera2CameraCaptureResult.getAwbState()).isEqualTo(AwbState.CONVERGED);
+    }
+
+    @Test
+    public void getAwbState_withAwbStateLocked() {
+        when(mCaptureResult.get(CaptureResult.CONTROL_AWB_STATE))
+                .thenReturn(CaptureResult.CONTROL_AWB_STATE_LOCKED);
+        assertThat(mCamera2CameraCaptureResult.getAwbState()).isEqualTo(AwbState.LOCKED);
+    }
+
+    @Test
+    public void getFlashState_withNull() {
+        when(mCaptureResult.get(CaptureResult.FLASH_STATE)).thenReturn(null);
+        assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.UNKNOWN);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateUnavailable() {
+        when(mCaptureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_UNAVAILABLE);
+        assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.NONE);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateCharging() {
+        when(mCaptureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_CHARGING);
+        assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.NONE);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateReady() {
+        when(mCaptureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_READY);
+        assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.READY);
+    }
+
+    @Test
+    public void getFlashState_withFlashStateFired() {
+        when(mCaptureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_FIRED);
+        assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
+    }
+
+    @Test
+    public void getFlashState_withFlashStatePartial() {
+        when(mCaptureResult.get(CaptureResult.FLASH_STATE))
+                .thenReturn(CaptureResult.FLASH_STATE_PARTIAL);
+        assertThat(mCamera2CameraCaptureResult.getFlashState()).isEqualTo(FlashState.FIRED);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlTest.java
new file mode 100644
index 0000000..dba124b
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CameraControlTest.java
@@ -0,0 +1,505 @@
+/*
+ * 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.Mockito.mock;
+import static org.mockito.Mockito.reset;
+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 android.os.Looper;
+
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.SessionConfiguration;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class Camera2CameraControlTest {
+
+    private Camera2CameraControl mCamera2CameraControl;
+    private CameraControl.ControlUpdateListener mControlUpdateListener;
+    private ArgumentCaptor<SessionConfiguration> mSessionConfigurationArgumentCaptor =
+            ArgumentCaptor.forClass(SessionConfiguration.class);
+    private ArgumentCaptor<CaptureRequestConfiguration> mCaptureRequestConfigurationArgumentCaptor =
+            ArgumentCaptor.forClass(CaptureRequestConfiguration.class);
+
+    @Before
+    @UiThreadTest
+    public void setUp() {
+        mControlUpdateListener = mock(CameraControl.ControlUpdateListener.class);
+        mCamera2CameraControl = new Camera2CameraControl(mControlUpdateListener, new Handler(
+                Looper.getMainLooper()));
+        // Reset the method call onCameraControlUpdateSessionConfiguration() in
+        // Camera2CameraControl constructor.
+        reset(mControlUpdateListener);
+    }
+
+    @Test
+    @UiThreadTest
+    public void setCropRegion_cropRectSetAndRepeatingRequestUpdated() {
+        Rect rect = new Rect(0, 0, 10, 10);
+
+        mCamera2CameraControl.setCropRegion(rect);
+        verify(mControlUpdateListener, times(1)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration = mSessionConfigurationArgumentCaptor.getValue();
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(repeatingConfig.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null))
+                .isEqualTo(rect);
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(mCamera2CameraControl.getSharedOptions());
+        assertThat(singleConfig.getCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, null))
+                .isEqualTo(rect);
+    }
+
+    @Test
+    @UiThreadTest
+    public void focus_focusRectSetAndRequestsExecuted() {
+        Rect focusRect = new Rect(0, 0, 10, 10);
+        Rect meteringRect = new Rect(20, 20, 30, 30);
+
+        mCamera2CameraControl.focus(focusRect, meteringRect);
+
+        verify(mControlUpdateListener, times(1)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration = mSessionConfigurationArgumentCaptor.getValue();
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(focusRect,
+                                        MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(mCamera2CameraControl.getSharedOptions());
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(focusRect,
+                                        MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, null))
+                .isEqualTo(
+                        new MeteringRectangle[]{
+                                new MeteringRectangle(
+                                        meteringRect, MeteringRectangle.METERING_WEIGHT_MAX)
+                        });
+
+        assertThat(mCamera2CameraControl.isFocusLocked()).isTrue();
+
+        verify(mControlUpdateListener).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+
+        assertThat(resultCaptureConfig.getCaptureRequestOption(CaptureRequest.CONTROL_AF_TRIGGER,
+                null)).isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START);
+    }
+
+    @Test
+    @UiThreadTest
+    public void cancelFocus_regionRestored() {
+        Rect focusRect = new Rect(0, 0, 10, 10);
+        Rect meteringRect = new Rect(20, 20, 30, 30);
+
+        mCamera2CameraControl.focus(focusRect, meteringRect);
+        mCamera2CameraControl.cancelFocus();
+
+        verify(mControlUpdateListener, times(2)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration =
+                mSessionConfigurationArgumentCaptor.getAllValues().get(1);
+        Camera2Configuration repeatingConfig =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        MeteringRectangle zeroRegion =
+                new MeteringRectangle(new Rect(), MeteringRectangle.METERING_WEIGHT_DONT_CARE);
+
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                repeatingConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(mCamera2CameraControl.getSharedOptions());
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_REGIONS, null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_REGIONS, null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AWB_REGIONS, null))
+                .isEqualTo(new MeteringRectangle[]{zeroRegion});
+
+        assertThat(mCamera2CameraControl.isFocusLocked()).isFalse();
+
+        verify(mControlUpdateListener, times(2)).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getAllValues().get(1);
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+
+        assertThat(resultCaptureConfig.getCaptureRequestOption(CaptureRequest.CONTROL_AF_TRIGGER,
+                null)).isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+    }
+
+    @Test
+    @UiThreadTest
+    public void defaultAFAWBMode_ShouldBeCAFWhenNotFocusLocked() {
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(mCamera2CameraControl.getSharedOptions());
+        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
+    @UiThreadTest
+    public void focus_afModeSetToAuto() {
+        Rect focusRect = new Rect(0, 0, 10, 10);
+        mCamera2CameraControl.focus(focusRect, focusRect);
+
+        Camera2Configuration singleConfig =
+                new Camera2Configuration(mCamera2CameraControl.getSharedOptions());
+        assertThat(
+                singleConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        mCamera2CameraControl.cancelFocus();
+
+        Camera2Configuration singleConfig2 =
+                new Camera2Configuration(mCamera2CameraControl.getSharedOptions());
+        assertThat(
+                singleConfig2.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
+    }
+
+    @Test
+    @UiThreadTest
+    public void setFlashModeAuto_aeModeSetAndRequestUpdated() {
+        mCamera2CameraControl.setFlashMode(FlashMode.AUTO);
+
+        verify(mControlUpdateListener, times(1)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration = mSessionConfigurationArgumentCaptor.getValue();
+        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(mCamera2CameraControl.getFlashMode()).isEqualTo(FlashMode.AUTO);
+    }
+
+    @Test
+    @UiThreadTest
+    public void setFlashModeOff_aeModeSetAndRequestUpdated() {
+        mCamera2CameraControl.setFlashMode(FlashMode.OFF);
+
+        verify(mControlUpdateListener, times(1)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration = mSessionConfigurationArgumentCaptor.getValue();
+        Camera2Configuration camera2Configuration =
+                new Camera2Configuration(sessionConfiguration.getImplementationOptions());
+        assertThat(
+                camera2Configuration.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, CONTROL_AE_MODE_OFF))
+                .isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON);
+        assertThat(mCamera2CameraControl.getFlashMode()).isEqualTo(FlashMode.OFF);
+    }
+
+    @Test
+    @UiThreadTest
+    public void setFlashModeOn_aeModeSetAndRequestUpdated() {
+        mCamera2CameraControl.setFlashMode(FlashMode.ON);
+
+        verify(mControlUpdateListener, times(1)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration = mSessionConfigurationArgumentCaptor.getValue();
+        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(mCamera2CameraControl.getFlashMode()).isEqualTo(FlashMode.ON);
+    }
+
+    @Test
+    @UiThreadTest
+    public void enableTorch_aeModeSetAndRequestUpdated() {
+        mCamera2CameraControl.enableTorch(true);
+
+        verify(mControlUpdateListener, times(1)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration = mSessionConfigurationArgumentCaptor.getValue();
+        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(mCamera2CameraControl.isTorchOn()).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void disableTorchFlashModeAuto_aeModeSetAndRequestUpdated() {
+        mCamera2CameraControl.setFlashMode(FlashMode.AUTO);
+        mCamera2CameraControl.enableTorch(false);
+
+        verify(mControlUpdateListener, times(2)).onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationArgumentCaptor.capture());
+        SessionConfiguration sessionConfiguration =
+                mSessionConfigurationArgumentCaptor.getAllValues().get(0);
+        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(mCamera2CameraControl.isTorchOn()).isFalse();
+
+        verify(mControlUpdateListener, times(1)).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_MODE, null))
+                .isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON);
+    }
+
+    @Test
+    @UiThreadTest
+    public void triggerAf_singleRequestSent() {
+        mCamera2CameraControl.triggerAf();
+
+        verify(mControlUpdateListener).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_TRIGGER, null))
+                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_START);
+    }
+
+    @Test
+    @UiThreadTest
+    public void triggerAePrecapture_singleRequestSent() {
+        mCamera2CameraControl.triggerAePrecapture();
+
+        verify(mControlUpdateListener).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null))
+                .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+    }
+
+    @Test
+    @UiThreadTest
+    public void cancelAfAeTrigger_singleRequestSent() {
+        mCamera2CameraControl.cancelAfAeTrigger(true, true);
+
+        verify(mControlUpdateListener).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_TRIGGER, null))
+                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null))
+                .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+    }
+
+    @Test
+    @UiThreadTest
+    public void cancelAfTrigger_singleRequestSent() {
+        mCamera2CameraControl.cancelAfAeTrigger(true, false);
+
+        verify(mControlUpdateListener).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_TRIGGER, null))
+                .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null))
+                .isNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void cancelAeTrigger_singleRequestSent() {
+        mCamera2CameraControl.cancelAfAeTrigger(false, true);
+
+        verify(mControlUpdateListener).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AF_TRIGGER, null))
+                .isNull();
+        assertThat(
+                resultCaptureConfig.getCaptureRequestOption(
+                        CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null))
+                .isEqualTo(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+    }
+
+    @Test
+    @UiThreadTest
+    public void submitSingleRequest_overrideBySharedOptions() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+        Camera2Configuration.Builder configBuilder = new Camera2Configuration.Builder();
+        configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AF_MODE,
+                CaptureRequest.CONTROL_AF_MODE_MACRO);
+        builder.setImplementationOptions(configBuilder.build());
+        mCamera2CameraControl.submitSingleRequest(builder.build());
+
+        verify(mControlUpdateListener).onCameraControlSingleRequest(
+                mCaptureRequestConfigurationArgumentCaptor.capture());
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mCaptureRequestConfigurationArgumentCaptor.getValue();
+        Camera2Configuration resultCaptureConfig =
+                new Camera2Configuration(captureRequestConfiguration.getImplementationOptions());
+
+        Camera2Configuration sharedOptions =
+                new Camera2Configuration(mCamera2CameraControl.getSharedOptions());
+
+        assertThat(resultCaptureConfig.getCaptureRequestOption(CaptureRequest.CONTROL_AF_MODE,
+                null)).isEqualTo(
+                sharedOptions.getCaptureRequestOption(CaptureRequest.CONTROL_AF_MODE, null));
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksTest.java
new file mode 100644
index 0000000..2079edc
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacksTest.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.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 androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class Camera2CaptureSessionCaptureCallbacksTest {
+
+    @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/Camera2ConfigurationTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationTest.java
new file mode 100644
index 0000000..bf316fa
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ConfigurationTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.core.Configuration;
+import androidx.camera.testing.fakes.FakeConfiguration;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class Camera2ConfigurationTest {
+    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",
+                new Configuration.OptionMatcher() {
+                    @Override
+                    public boolean onOptionMatched(Configuration.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/Camera2ImplCameraRepositoryTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryTest.java
new file mode 100644
index 0000000..557961e
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraRepositoryTest.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.camera2;
+
+import android.Manifest;
+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.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseGroup;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+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.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class Camera2ImplCameraRepositoryTest {
+    private CameraRepository mCameraRepository;
+    private UseCaseGroup mUseCaseGroup;
+    private FakeUseCaseConfiguration mConfiguration;
+    private CallbackAttachingFakeUseCase mUseCase;
+    private CameraFactory mCameraFactory;
+
+    private String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
+        try {
+            return mCameraFactory.cameraIdForLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + lensFacing, e);
+        }
+    }
+
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(
+            Manifest.permission.CAMERA);
+
+    @Before
+    public void setUp() {
+        mCameraRepository = new CameraRepository();
+        mCameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
+        mCameraRepository.init(mCameraFactory);
+        mUseCaseGroup = new UseCaseGroup();
+        mConfiguration =
+                new FakeUseCaseConfiguration.Builder().setLensFacing(LensFacing.BACK).build();
+        String cameraId = getCameraIdForLensFacingUnchecked(mConfiguration.getLensFacing());
+        mUseCase = new CallbackAttachingFakeUseCase(mConfiguration, cameraId);
+        mUseCaseGroup.addUseCase(mUseCase);
+    }
+
+    @Test
+    public void cameraDeviceCallsAreForwardedToCallback() throws InterruptedException {
+        mUseCase.addStateChangeListener(
+                mCameraRepository.getCamera(
+                        getCameraIdForLensFacingUnchecked(mConfiguration.getLensFacing())));
+        mUseCase.doNotifyActive();
+        mCameraRepository.onGroupActive(mUseCaseGroup);
+
+        // Wait for the CameraDevice.onOpened callback.
+        mUseCase.mDeviceStateCallback.waitForOnOpened(1);
+
+        mCameraRepository.onGroupInactive(mUseCaseGroup);
+
+        // Wait for the CameraDevice.onClosed callback.
+        mUseCase.mDeviceStateCallback.waitForOnClosed(1);
+    }
+
+    @Test
+    public void cameraSessionCallsAreForwardedToCallback() throws InterruptedException {
+        mUseCase.addStateChangeListener(
+                mCameraRepository.getCamera(
+                        getCameraIdForLensFacingUnchecked(mConfiguration.getLensFacing())));
+        mUseCase.doNotifyActive();
+        mCameraRepository.onGroupActive(mUseCaseGroup);
+
+        // Wait for the CameraCaptureSession.onConfigured callback.
+        mUseCase.mSessionStateCallback.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 mDeviceStateCallback = new DeviceStateCallback();
+        private final SessionStateCallback mSessionStateCallback = new SessionStateCallback();
+        private final SurfaceTexture mSurfaceTexture = 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(mSurfaceTexture)));
+            builder.setDeviceStateCallback(mDeviceStateCallback);
+            builder.setSessionStateCallback(mSessionStateCallback);
+
+            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/Camera2ImplCameraXTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXTest.java
new file mode 100644
index 0000000..337840d
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2ImplCameraXTest.java
@@ -0,0 +1,369 @@
+/*
+ * 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.Manifest;
+import android.content.Context;
+import android.hardware.camera2.CameraDevice;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+
+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.core.ImageProxy;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+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.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class Camera2ImplCameraXTest {
+    private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
+    private final MutableLiveData<Long> mAnalysisResult = new MutableLiveData<>();
+    private final ImageAnalysisUseCase.Analyzer mImageAnalyzer =
+            new ImageAnalysisUseCase.Analyzer() {
+                @Override
+                public void analyze(ImageProxy image, int rotationDegrees) {
+                    mAnalysisResult.postValue(image.getTimestamp());
+                }
+            };
+    private FakeLifecycleOwner mLifecycle;
+    private HandlerThread mHandlerThread;
+    private Handler mMainThreadHandler;
+
+    private CameraDevice.StateCallback mMockStateCallback;
+
+    private static Observer<Long> createCountIncrementingObserver(final AtomicLong counter) {
+        return new Observer<Long>() {
+            @Override
+            public void onChanged(Long value) {
+                counter.incrementAndGet();
+            }
+        };
+    }
+
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(
+            Manifest.permission.CAMERA);
+
+    @Before
+    public void setUp() {
+        Context context = ApplicationProvider.getApplicationContext();
+        CameraX.init(context, Camera2AppConfiguration.create(context));
+        mLifecycle = new FakeLifecycleOwner();
+        mHandlerThread = new HandlerThread("ErrorHandlerThread");
+        mHandlerThread.start();
+        mMainThreadHandler = new Handler(Looper.getMainLooper());
+        mMockStateCallback = Mockito.mock(CameraDevice.StateCallback.class);
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        CameraX.unbindAll();
+        mHandlerThread.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 {
+        final AtomicLong observedCount = new AtomicLong(0);
+        mMainThreadHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                ImageAnalysisUseCaseConfiguration configuration =
+                        new ImageAnalysisUseCaseConfiguration.Builder()
+                                .setLensFacing(DEFAULT_LENS_FACING)
+                                .build();
+                ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+                CameraX.bindToLifecycle(mLifecycle, useCase);
+
+                useCase.setAnalyzer(mImageAnalyzer);
+                mAnalysisResult.observe(mLifecycle, createCountIncrementingObserver(observedCount));
+
+                mLifecycle.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 {
+        final AtomicLong observedCount = new AtomicLong(0);
+        mMainThreadHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                ImageAnalysisUseCaseConfiguration configuration =
+                        new ImageAnalysisUseCaseConfiguration.Builder()
+                                .setLensFacing(DEFAULT_LENS_FACING)
+                                .build();
+                ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+                CameraX.bindToLifecycle(mLifecycle, useCase);
+                useCase.setAnalyzer(mImageAnalyzer);
+                mAnalysisResult.observe(mLifecycle, createCountIncrementingObserver(observedCount));
+                assertThat(observedCount.get()).isEqualTo(0);
+
+                CameraX.unbind(useCase);
+
+                mLifecycle.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 {
+        final AtomicLong observedCount = new AtomicLong(0);
+        final SessionCaptureCallback sessionCaptureCallback = new SessionCaptureCallback();
+        final DeviceStateCallback deviceStateCallback = new DeviceStateCallback();
+        mMainThreadHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                ImageAnalysisUseCaseConfiguration.Builder configurationBuilder =
+                        new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(
+                                DEFAULT_LENS_FACING);
+                new Camera2Configuration.Extender(configurationBuilder)
+                        .setDeviceStateCallback(deviceStateCallback)
+                        .setSessionCaptureCallback(sessionCaptureCallback);
+                ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(
+                        configurationBuilder.build());
+                CameraX.bindToLifecycle(mLifecycle, useCase);
+                useCase.setAnalyzer(mImageAnalyzer);
+                mAnalysisResult.observe(mLifecycle, createCountIncrementingObserver(observedCount));
+
+                mLifecycle.startAndResume();
+            }
+        });
+
+        // Wait a little bit for the camera to open and stream frames.
+        sessionCaptureCallback.waitForOnCaptureCompleted(5);
+
+        mMainThreadHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mLifecycle.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(mMockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration = builder.build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(mLifecycle, useCase);
+        useCase.setAnalyzer(mImageAnalyzer);
+        mLifecycle.startAndResume();
+
+        verify(mMockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+    }
+
+    @Test
+    public void bind_opensCamera_withOutAnalyzer() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mMockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration = builder.build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(mLifecycle, useCase);
+        mLifecycle.startAndResume();
+
+        verify(mMockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+    }
+
+    @Test
+    public void bind_unbind_loopWithOutAnalyzer() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mMockStateCallback);
+        mLifecycle.startAndResume();
+
+        for (int i = 0; i < 2; i++) {
+            ImageAnalysisUseCaseConfiguration configuration = builder.build();
+            ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+            CameraX.bindToLifecycle(mLifecycle, useCase);
+
+            verify(mMockStateCallback, timeout(5000)).onOpened(any(CameraDevice.class));
+
+            CameraX.unbind(useCase);
+
+            verify(mMockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+        }
+    }
+
+    @Test
+    public void bind_unbind_loopWithAnalyzer() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mMockStateCallback);
+        mLifecycle.startAndResume();
+
+        for (int i = 0; i < 2; i++) {
+            ImageAnalysisUseCaseConfiguration configuration = builder.build();
+            ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+            CameraX.bindToLifecycle(mLifecycle, useCase);
+            useCase.setAnalyzer(mImageAnalyzer);
+
+            verify(mMockStateCallback, timeout(5000)).onOpened(any(CameraDevice.class));
+
+            CameraX.unbind(useCase);
+
+            verify(mMockStateCallback, timeout(3000)).onClosed(any(CameraDevice.class));
+        }
+    }
+
+    @Test
+    public void unbindAll_closesAllCameras() {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                new ImageAnalysisUseCaseConfiguration.Builder().setLensFacing(DEFAULT_LENS_FACING);
+        new Camera2Configuration.Extender(builder).setDeviceStateCallback(mMockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration = builder.build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(mLifecycle, useCase);
+        mLifecycle.startAndResume();
+
+        verify(mMockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+
+        CameraX.unbindAll();
+
+        verify(mMockStateCallback, 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(mMockStateCallback);
+        ImageAnalysisUseCaseConfiguration configuration = builder.build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        CameraX.bindToLifecycle(mLifecycle, useCase);
+        mLifecycle.startAndResume();
+
+        verify(mMockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+
+        CameraX.unbind(useCase);
+
+        verify(mMockStateCallback, 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(mMockStateCallback);
+        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(mLifecycle, useCase0, useCase1);
+        mLifecycle.startAndResume();
+
+        verify(mMockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+
+        CameraX.unbind(useCase1);
+
+        Thread.sleep(3000);
+
+        verify(mMockStateCallback, 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(mMockStateCallback);
+        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(mLifecycle, useCase0, useCase1);
+        mLifecycle.startAndResume();
+
+        verify(mMockStateCallback, timeout(3000)).onOpened(any(CameraDevice.class));
+
+        CameraX.unbind(useCase0);
+        CameraX.unbind(useCase1);
+
+        verify(mMockStateCallback, timeout(3000).times(1)).onClosed(any(CameraDevice.class));
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerTest.java
new file mode 100644
index 0000000..eff3d2f
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/Camera2InitializerTest.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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.camera.testing.fakes.FakeActivity;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link Camera2Initializer}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class Camera2InitializerTest {
+
+    @Rule
+    public ActivityTestRule<FakeActivity> activityRule =
+            new ActivityTestRule<>(
+                    FakeActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
+    private Context mAppContext;
+
+    @Before
+    public void setUp() {
+        mAppContext = ApplicationProvider.getApplicationContext();
+    }
+
+    @Test
+    public void cameraXIsInitialized_beforeActivityIsCreated() {
+        activityRule.launchActivity(new Intent(mAppContext, FakeActivity.class));
+        FakeActivity activity = activityRule.getActivity();
+
+        assertThat(activity.isCameraXInitializedAtOnCreate()).isTrue();
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraTest.java
new file mode 100644
index 0000000..3640a30
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CameraTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CameraTest {
+    private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
+    static CameraFactory sCameraFactory;
+
+    BaseCamera mCamera;
+
+    UseCase mFakeUseCase;
+    OnImageAvailableListener mMockOnImageAvailableListener;
+    String mCameraId;
+
+    private static String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
+        try {
+            return sCameraFactory.cameraIdForLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + lensFacing, e);
+        }
+    }
+
+    @BeforeClass
+    public static void classSetup() {
+        sCameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
+    }
+
+    @Before
+    public void setup() {
+        mMockOnImageAvailableListener = Mockito.mock(ImageReader.OnImageAvailableListener.class);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .build();
+        mCameraId = getCameraIdForLensFacingUnchecked(DEFAULT_LENS_FACING);
+        mFakeUseCase = new UseCase(configuration, mMockOnImageAvailableListener);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, new Size(640, 480));
+        mFakeUseCase.updateSuggestedResolution(suggestedResolutionMap);
+
+        mCamera = sCameraFactory.getCamera(mCameraId);
+    }
+
+    @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 (mCamera != null) {
+            mCamera.release();
+            mCamera = 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 (mFakeUseCase != null) {
+            mFakeUseCase.close();
+            mFakeUseCase = null;
+        }
+    }
+
+    @Test
+    public void onlineUseCase() {
+        mCamera.open();
+
+        mCamera.addOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+
+        verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+
+        mCamera.release();
+    }
+
+    @Test
+    public void activeUseCase() {
+        mCamera.open();
+
+        mCamera.onUseCaseActive(mFakeUseCase);
+
+        verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+
+        mCamera.release();
+    }
+
+    @Test
+    public void onlineAndActiveUseCase() throws InterruptedException {
+        mCamera.open();
+
+        mCamera.addOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+        mCamera.onUseCaseActive(mFakeUseCase);
+
+        verify(mMockOnImageAvailableListener, timeout(4000).atLeastOnce())
+                .onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void removeOnlineUseCase() {
+        mCamera.open();
+
+        mCamera.addOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+        mCamera.removeOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+        mCamera.onUseCaseActive(mFakeUseCase);
+
+        verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void unopenedCamera() {
+        mCamera.addOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+        mCamera.removeOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+
+        verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void closedCamera() {
+        mCamera.open();
+
+        mCamera.close();
+        mCamera.addOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+        mCamera.removeOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+
+        verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void releaseUnopenedCamera() {
+        mCamera.release();
+        mCamera.open();
+
+        mCamera.addOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+        mCamera.onUseCaseActive(mFakeUseCase);
+
+        verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    @Test
+    public void releasedOpenedCamera() {
+        mCamera.release();
+        mCamera.open();
+
+        mCamera.addOnlineUseCase(Collections.<BaseUseCase>singletonList(mFakeUseCase));
+        mCamera.onUseCaseActive(mFakeUseCase);
+
+        verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
+    }
+
+    private static class UseCase extends FakeUseCase {
+        private final ImageReader.OnImageAvailableListener mImageAvailableListener;
+        HandlerThread mHandlerThread = new HandlerThread("HandlerThread");
+        Handler mHandler;
+        ImageReader mImageReader;
+
+        UseCase(
+                FakeUseCaseConfiguration configuration,
+                ImageReader.OnImageAvailableListener listener) {
+            super(configuration);
+            mImageAvailableListener = listener;
+            mHandlerThread.start();
+            mHandler = new Handler(mHandlerThread.getLooper());
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            String cameraId = getCameraIdForLensFacingUnchecked(configuration.getLensFacing());
+            suggestedResolutionMap.put(cameraId, new Size(640, 480));
+            updateSuggestedResolution(suggestedResolutionMap);
+        }
+
+        void close() {
+            mHandler.removeCallbacksAndMessages(null);
+            mHandlerThread.quitSafely();
+            if (mImageReader != null) {
+                mImageReader.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);
+            mImageReader =
+                    ImageReader.newInstance(
+                            resolution.getWidth(),
+                            resolution.getHeight(),
+                            ImageFormat.YUV_420_888, /*maxImages*/
+                            2);
+            mImageReader.setOnImageAvailableListener(mImageAvailableListener, mHandler);
+            builder.addSurface(new ImmediateSurface(mImageReader.getSurface()));
+
+            attachToCamera(cameraId, builder.build());
+            return suggestedResolutionMap;
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerTest.java
new file mode 100644
index 0000000..26e9324
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureCallbackContainerTest.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.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CaptureCallbackContainerTest {
+
+    @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/CaptureRequestParameterTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureRequestParameterTest.java
new file mode 100644
index 0000000..7440893
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureRequestParameterTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.camera.core.CaptureRequestParameter;
+import androidx.camera.testing.CameraUtil;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CaptureRequestParameterTest {
+    private CameraDevice mCameraDevice;
+
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(
+            Manifest.permission.CAMERA);
+
+    @Before
+    public void setup() throws CameraAccessException, InterruptedException {
+        mCameraDevice = CameraUtil.getCameraDevice();
+    }
+
+    @After
+    public void teardown() {
+        if (mCameraDevice != null) {
+            CameraUtil.releaseCameraDevice(mCameraDevice);
+        }
+    }
+
+    @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 =
+                mCameraDevice.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/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionTest.java
new file mode 100644
index 0000000..adef852
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/CaptureSessionTest.java
@@ -0,0 +1,383 @@
+/*
+ * 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.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.CaptureRequestConfiguration;
+import androidx.camera.core.DeferrableSurface;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.testing.CameraUtil;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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.Executor;
+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}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CaptureSessionTest {
+    private CaptureSessionTestParameters mTestParameters0;
+    private CaptureSessionTestParameters mTestParameters1;
+
+    private CameraDevice mCameraDevice;
+
+    @Before
+    public void setup() throws CameraAccessException, InterruptedException {
+        mTestParameters0 = new CaptureSessionTestParameters("mTestParameters0");
+        mTestParameters1 = new CaptureSessionTestParameters("mTestParameters1");
+        mCameraDevice = CameraUtil.getCameraDevice();
+    }
+
+    @After
+    public void tearDown() {
+        mTestParameters0.tearDown();
+        mTestParameters1.tearDown();
+        CameraUtil.releaseCameraDevice(mCameraDevice);
+    }
+
+    @Test
+    public void setCaptureSessionSucceed() {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+
+        assertThat(captureSession.getSessionConfiguration())
+                .isEqualTo(mTestParameters0.mSessionConfiguration);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void setCaptureSessionOnClosedSession_throwsException() {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        SessionConfiguration newSessionConfiguration = mTestParameters0.mSessionConfiguration;
+
+        captureSession.close();
+
+        // Should throw IllegalStateException
+        captureSession.setSessionConfiguration(newSessionConfiguration);
+    }
+
+    @Test
+    public void openCaptureSessionSucceed() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+
+        captureSession.open(mTestParameters0.mSessionConfiguration, mCameraDevice);
+
+        mTestParameters0.waitForData();
+
+        assertThat(captureSession.getState()).isEqualTo(State.OPENED);
+
+        // StateCallback.onConfigured() should be called to signal the session is configured.
+        verify(mTestParameters0.mSessionStateCallback, times(1))
+                .onConfigured(any(CameraCaptureSession.class));
+
+        // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+        verify(mTestParameters0.mSessionCameraCaptureCallback, timeout(3000).atLeast(1))
+                .onCaptureCompleted(any(CameraCaptureResult.class));
+    }
+
+    @Test
+    public void closeUnopenedSession() {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+
+        captureSession.close();
+
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+    }
+
+    @Test
+    public void releaseUnopenedSession() {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+
+        captureSession.release();
+
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+    }
+
+    @Test
+    public void closeOpenedSession() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+        captureSession.open(mTestParameters0.mSessionConfiguration, mCameraDevice);
+
+        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(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+        captureSession.open(mTestParameters0.mSessionConfiguration, mCameraDevice);
+        captureSession.release();
+
+        Thread.sleep(3000);
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+
+        // StateCallback.onClosed() should be called to signal the session is closed.
+        verify(mTestParameters0.mSessionStateCallback, times(1))
+                .onClosed(any(CameraCaptureSession.class));
+    }
+
+    @Test
+    public void openSecondSession() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+
+        // First session is opened
+        captureSession.open(mTestParameters0.mSessionConfiguration, mCameraDevice);
+        captureSession.close();
+
+        // Open second session, which should cause first one to be released
+        CaptureSession captureSession1 = new CaptureSession(mTestParameters1.mHandler);
+        captureSession1.setSessionConfiguration(mTestParameters1.mSessionConfiguration);
+        captureSession1.open(mTestParameters1.mSessionConfiguration, mCameraDevice);
+
+        mTestParameters1.waitForData();
+
+        assertThat(captureSession1.getState()).isEqualTo(State.OPENED);
+        assertThat(captureSession.getState()).isEqualTo(State.RELEASED);
+
+        // First session should have StateCallback.onConfigured(), onClosed() calls.
+        verify(mTestParameters0.mSessionStateCallback, times(1))
+                .onConfigured(any(CameraCaptureSession.class));
+        verify(mTestParameters0.mSessionStateCallback, times(1))
+                .onClosed(any(CameraCaptureSession.class));
+
+        // Second session should have StateCallback.onConfigured() call.
+        verify(mTestParameters1.mSessionStateCallback, times(1))
+                .onConfigured(any(CameraCaptureSession.class));
+
+        // Second session should have CameraCaptureCallback.onCaptureCompleted() call.
+        verify(mTestParameters1.mSessionCameraCaptureCallback, timeout(3000).atLeast(1))
+                .onCaptureCompleted(any(CameraCaptureResult.class));
+    }
+
+    @Test
+    public void issueSingleCaptureRequest() throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+        captureSession.open(mTestParameters0.mSessionConfiguration, mCameraDevice);
+
+        mTestParameters0.waitForData();
+
+        assertThat(captureSession.getState()).isEqualTo(State.OPENED);
+
+        captureSession.issueSingleCaptureRequest(mTestParameters0.mCaptureRequestConfiguration);
+
+        mTestParameters0.waitForCameraCaptureCallback();
+
+        // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+        verify(mTestParameters0.mCameraCaptureCallback, timeout(3000).times(1))
+                .onCaptureCompleted(any(CameraCaptureResult.class));
+    }
+
+    @Test
+    public void issueSingleCaptureRequestBeforeCaptureSessionOpened()
+            throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+
+        captureSession.issueSingleCaptureRequest(mTestParameters0.mCaptureRequestConfiguration);
+        captureSession.open(mTestParameters0.mSessionConfiguration, mCameraDevice);
+
+        mTestParameters0.waitForCameraCaptureCallback();
+
+        // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+        verify(mTestParameters0.mCameraCaptureCallback, timeout(3000).times(1))
+                .onCaptureCompleted(any(CameraCaptureResult.class));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void issueSingleCaptureRequestOnClosedSession_throwsException() {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+
+        captureSession.close();
+
+        // Should throw IllegalStateException
+        captureSession.issueSingleCaptureRequest(mTestParameters0.mCaptureRequestConfiguration);
+    }
+
+    @Test
+    public void surfaceOnDetachedListenerIsCalledWhenSessionIsClose()
+            throws CameraAccessException, InterruptedException {
+        CaptureSession captureSession = new CaptureSession(mTestParameters0.mHandler);
+        captureSession.setSessionConfiguration(mTestParameters0.mSessionConfiguration);
+
+        captureSession.open(mTestParameters0.mSessionConfiguration, mCameraDevice);
+
+        mTestParameters0.waitForData();
+
+        DeferrableSurface.OnSurfaceDetachedListener listener =
+                Mockito.mock(DeferrableSurface.OnSurfaceDetachedListener.class);
+        Executor executor = new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        };
+        mTestParameters0.mDeferrableSurface.setOnSurfaceDetachedListener(executor, listener);
+
+        captureSession.release();
+
+        Thread.sleep(3000);
+
+        Mockito.verify(listener, times(1)).onSurfaceDetached();
+    }
+
+    /**
+     * 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 mHandlerThread;
+        /** Handler for all asynchronous calls. */
+        private final Handler mHandler;
+        /** Latch to wait for first image data to appear. */
+        private final CountDownLatch mDataLatch = new CountDownLatch(1);
+
+        /** Latch to wait for camera capture callback to be invoked. */
+        private final CountDownLatch mCameraCaptureCallbackLatch = new CountDownLatch(1);
+
+        /** Image reader that unlocks the latch waiting for the first image data to appear. */
+        private final OnImageAvailableListener mOnImageAvailableListener =
+                new OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReader reader) {
+                        Image image = reader.acquireNextImage();
+                        if (image != null) {
+                            image.close();
+                            mDataLatch.countDown();
+                        }
+                    }
+                };
+
+        private final ImageReader mImageReader;
+        private final SessionConfiguration mSessionConfiguration;
+        private final CaptureRequestConfiguration mCaptureRequestConfiguration;
+
+        private final CameraCaptureSession.StateCallback mSessionStateCallback =
+                Mockito.mock(CameraCaptureSession.StateCallback.class);
+        private final CameraCaptureCallback mSessionCameraCaptureCallback =
+                Mockito.mock(CameraCaptureCallback.class);
+        private final CameraCaptureCallback mCameraCaptureCallback =
+                Mockito.mock(CameraCaptureCallback.class);
+
+        private final DeferrableSurface mDeferrableSurface;
+        /**
+         * 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 mComboCameraCaptureCallback =
+                CameraCaptureCallbacks.createComboCallback(
+                        mCameraCaptureCallback,
+                        new CameraCaptureCallback() {
+                            @Override
+                            public void onCaptureCompleted(@NonNull CameraCaptureResult result) {
+                                mCameraCaptureCallbackLatch.countDown();
+                            }
+                        });
+
+        CaptureSessionTestParameters(String name) {
+            mHandlerThread = new HandlerThread(name);
+            mHandlerThread.start();
+            mHandler = new Handler(mHandlerThread.getLooper());
+
+            mImageReader =
+                    ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, /*maxImages*/ 2);
+            mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);
+
+            SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            mDeferrableSurface = new ImmediateSurface(mImageReader.getSurface());
+            builder.addSurface(mDeferrableSurface);
+            builder.setSessionStateCallback(mSessionStateCallback);
+            builder.setCameraCaptureCallback(mSessionCameraCaptureCallback);
+
+            mSessionConfiguration = builder.build();
+
+            CaptureRequestConfiguration.Builder captureRequestConfigBuilder =
+                    new CaptureRequestConfiguration.Builder();
+            captureRequestConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            captureRequestConfigBuilder.addSurface(new ImmediateSurface(mImageReader.getSurface()));
+            captureRequestConfigBuilder.setCameraCaptureCallback(mComboCameraCaptureCallback);
+
+            mCaptureRequestConfiguration = 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 {
+            mDataLatch.await(TIME_TO_WAIT_FOR_DATA_SECONDS, TimeUnit.SECONDS);
+        }
+
+        void waitForCameraCaptureCallback() throws InterruptedException {
+            mCameraCaptureCallbackLatch.await(TIME_TO_WAIT_FOR_DATA_SECONDS, TimeUnit.SECONDS);
+        }
+
+        /** Clean up resources. */
+        void tearDown() {
+            mImageReader.close();
+            mHandlerThread.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..f472a5f
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/FakeRepeatingUseCase.java
@@ -0,0 +1,101 @@
+/*
+ * 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.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseConfiguration;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+
+import java.util.Map;
+
+/** A fake {@link FakeUseCase} which contain a repeating surface. */
+public class FakeRepeatingUseCase extends FakeUseCase {
+
+    /** The repeating surface. */
+    private final ImageReader mImageReader =
+            ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2);
+
+    public FakeRepeatingUseCase(FakeUseCaseConfiguration configuration) {
+        super(configuration);
+
+        FakeUseCaseConfiguration configWithDefaults =
+                (FakeUseCaseConfiguration) getUseCaseConfiguration();
+        mImageReader.setOnImageAvailableListener(
+                new ImageReader.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReader 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(mImageReader.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(LensFacing lensFacing) {
+        return new FakeUseCaseConfiguration.Builder()
+                .setLensFacing(lensFacing)
+                .setOptionUnpacker(
+                        new SessionConfiguration.OptionUnpacker() {
+                            @Override
+                            public void unpack(UseCaseConfiguration<?> useCaseConfig,
+                                    SessionConfiguration.Builder sessionConfigBuilder) {
+                                // Set the template since it is currently required by implementation
+                                sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+                            }
+                        });
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        mImageReader.close();
+    }
+
+    @Override
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        return suggestedResolutionMap;
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseTest.java
new file mode 100644
index 0000000..25736c5
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageAnalysisUseCaseTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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.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.camera.testing.CameraUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ImageAnalysisUseCaseTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(640, 480);
+    private final ImageAnalysisUseCaseConfiguration mDefaultConfiguration =
+            ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration(null);
+    private final StateChangeListener mMockListener = Mockito.mock(StateChangeListener.class);
+    private final Analyzer mMockAnalyzer = Mockito.mock(Analyzer.class);
+    private Set<ImageProperties> mAnalysisResults;
+    private Analyzer mAnalyzer;
+    private BaseCamera mCamera;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private Semaphore mAnalysisResultsSemaphore;
+    private String mCameraId;
+
+    @Before
+    public void setUp() {
+        mAnalysisResults = new HashSet<>();
+        mAnalysisResultsSemaphore = new Semaphore(/*permits=*/ 0);
+        mAnalyzer =
+                new Analyzer() {
+                    @Override
+                    public void analyze(ImageProxy image, int rotationDegrees) {
+                        mAnalysisResults.add(new ImageProperties(image, rotationDegrees));
+                        mAnalysisResultsSemaphore.release();
+                    }
+                };
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration config = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = config.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            mCameraId = cameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + LensFacing.BACK, e);
+        }
+        mCamera = cameraFactory.getCamera(mCameraId);
+
+        CameraX.init(context, config);
+
+        mHandlerThread = new HandlerThread("AnalysisThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        mHandlerThread.quitSafely();
+        mCamera.release();
+    }
+
+    @Test
+    public void analyzerCanBeSetAndRetrieved() {
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+
+        Analyzer initialAnalyzer = useCase.getAnalyzer();
+
+        useCase.setAnalyzer(mMockAnalyzer);
+
+        Analyzer retrievedAnalyzer = useCase.getAnalyzer();
+
+        // The observer is bound to the lifecycle.
+        assertThat(initialAnalyzer).isNull();
+        assertThat(retrievedAnalyzer).isSameAs(mMockAnalyzer);
+    }
+
+    @Test
+    public void becomesActive_whenHasAnalyzer() {
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(mMockListener);
+
+        useCase.setAnalyzer(mMockAnalyzer);
+
+        verify(mMockListener, times(1)).onUseCaseActive(useCase);
+    }
+
+    @Test
+    public void becomesInactive_whenNoAnalyzer() {
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(mMockListener);
+        useCase.setAnalyzer(mMockAnalyzer);
+        useCase.removeAnalyzer();
+
+        verify(mMockListener, times(1)).onUseCaseInactive(useCase);
+    }
+
+    @Test
+    public void analyzerAnalyzesImages_whenCameraIsOpen()
+            throws InterruptedException, CameraInfoUnavailableException {
+        final int imageFormat = ImageFormat.YUV_420_888;
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(
+                        mHandler).build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase);
+        useCase.setAnalyzer(mAnalyzer);
+
+        int sensorRotation = CameraX.getCameraInfo(mCameraId).getSensorRotationDegrees();
+        // The frames should have properties which match the configuration.
+        for (ImageProperties properties : mAnalysisResults) {
+            assertThat(properties.mResolution).isEqualTo(DEFAULT_RESOLUTION);
+            assertThat(properties.mFormat).isEqualTo(imageFormat);
+            assertThat(properties.mRotationDegrees).isEqualTo(sensorRotation);
+        }
+    }
+
+    @Test
+    public void analyzerDoesNotAnalyzeImages_whenCameraIsNotOpen() throws InterruptedException {
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder().setCallbackHandler(
+                        mHandler).build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.setAnalyzer(mAnalyzer);
+        // Keep the lifecycle in an inactive state.
+        // Wait a little while for frames to be analyzed.
+        mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
+
+        // No frames should have been analyzed.
+        assertThat(mAnalysisResults).isEmpty();
+    }
+
+    @Test
+    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(
+                        mHandler).build();
+        ImageAnalysisUseCase useCase = new ImageAnalysisUseCase(configuration);
+        useCase.setAnalyzer(mAnalyzer);
+
+        for (Size size : sizes) {
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            suggestedResolutionMap.put(mCameraId, size);
+            useCase.updateSuggestedResolution(suggestedResolutionMap);
+            CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase);
+
+            // Clear previous results
+            mAnalysisResults.clear();
+            // Wait a little while for frames to be analyzed.
+            mAnalysisResultsSemaphore.tryAcquire(5, TimeUnit.SECONDS);
+
+            // The frames should have properties which match the configuration.
+            for (ImageProperties properties : mAnalysisResults) {
+                assertThat(properties.mResolution).isEqualTo(size);
+                assertThat(properties.mFormat).isEqualTo(imageFormat);
+            }
+
+            // Detach use case from camera device to run next resolution setting
+            CameraUtil.detachUseCaseFromCamera(mCamera, useCase);
+        }
+    }
+
+    @Test
+    public void defaultsIncludeImageReaderMode() {
+        ImageAnalysisUseCaseConfiguration defaultConfig =
+                ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration(null);
+
+        // 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(null);
+
+        // 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 mResolution;
+        final int mFormat;
+        final long mTimestamp;
+        final int mRotationDegrees;
+
+        ImageProperties(ImageProxy image, int rotationDegrees) {
+            this.mResolution = new Size(image.getWidth(), image.getHeight());
+            this.mFormat = image.getFormat();
+            this.mTimestamp = image.getTimestamp();
+            this.mRotationDegrees = 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 mResolution.equals(otherProperties.mResolution)
+                    && mFormat == otherProperties.mFormat
+                    && otherProperties.mTimestamp == mTimestamp
+                    && otherProperties.mRotationDegrees == mRotationDegrees;
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 7;
+            hash = 31 * hash + mResolution.getWidth();
+            hash = 31 * hash + mResolution.getHeight();
+            hash = 31 * hash + mFormat;
+            hash = 31 * hash + (int) mTimestamp;
+            hash = 31 * hash + mRotationDegrees;
+            return hash;
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseTest.java
new file mode 100644
index 0000000..bd332e0
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageCaptureUseCaseTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.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.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.Exif;
+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.camera.testing.CameraUtil;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ImageCaptureUseCaseTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+    private static final LensFacing BACK_LENS_FACING = LensFacing.BACK;
+
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private BaseCamera mCamera;
+    private ImageCaptureUseCaseConfiguration mDefaultConfiguration;
+    private ImageCaptureUseCase.OnImageCapturedListener mOnImageCapturedListener;
+    private ImageCaptureUseCase.OnImageCapturedListener mMockImageCapturedListener;
+    private ImageCaptureUseCase.OnImageSavedListener mOnImageSavedListener;
+    private ImageCaptureUseCase.OnImageSavedListener mMockImageSavedListener;
+    private ImageProxy mCapturedImage;
+    private Semaphore mSemaphore;
+    private FakeRepeatingUseCase mRepeatingUseCase;
+    private FakeUseCaseConfiguration mFakeUseCaseConfiguration;
+    private String mCameraId;
+
+    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() {
+        mHandlerThread = new HandlerThread("CaptureThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = appConfig.getCameraFactory(null);
+        CameraX.init(context, appConfig);
+        try {
+            mCameraId = cameraFactory.cameraIdForLensFacing(BACK_LENS_FACING);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + BACK_LENS_FACING, e);
+        }
+        mDefaultConfiguration = new ImageCaptureUseCaseConfiguration.Builder().build();
+
+        mCamera = cameraFactory.getCamera(mCameraId);
+        mCapturedImage = null;
+        mSemaphore = new Semaphore(/*permits=*/ 0);
+        mOnImageCapturedListener =
+                new OnImageCapturedListener() {
+                    @Override
+                    public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                        mCapturedImage = image;
+                        // Signal that the image has been captured.
+                        mSemaphore.release();
+                    }
+                };
+        mMockImageCapturedListener = Mockito.mock(OnImageCapturedListener.class);
+        mMockImageSavedListener = Mockito.mock(OnImageSavedListener.class);
+        mOnImageSavedListener =
+                new OnImageSavedListener() {
+                    @Override
+                    public void onImageSaved(File file) {
+                        mMockImageSavedListener.onImageSaved(file);
+                        // Signal that an image was saved
+                        mSemaphore.release();
+                    }
+
+                    @Override
+                    public void onError(
+                            UseCaseError error, String message, @Nullable Throwable cause) {
+                        mMockImageSavedListener.onError(error, message, cause);
+                        // Signal that there was an error
+                        mSemaphore.release();
+                    }
+                };
+
+        mFakeUseCaseConfiguration = new FakeUseCaseConfiguration.Builder().build();
+        mRepeatingUseCase = new FakeRepeatingUseCase(mFakeUseCaseConfiguration);
+    }
+
+    @After
+    public void tearDown() {
+        mHandlerThread.quitSafely();
+        mCamera.close();
+        if (mCapturedImage != null) {
+            mCapturedImage.close();
+        }
+    }
+
+    @Test
+    public void capturedImageHasCorrectProperties() throws InterruptedException {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder().setCallbackHandler(mHandler).build();
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        useCase.takePicture(mOnImageCapturedListener);
+        // Wait for the signal that the image has been captured.
+        mSemaphore.acquire();
+
+        assertThat(new Size(mCapturedImage.getWidth(), mCapturedImage.getHeight()))
+                .isEqualTo(DEFAULT_RESOLUTION);
+        assertThat(mCapturedImage.getFormat()).isEqualTo(useCase.getImageFormat());
+    }
+
+    @Test
+    public void canCaptureMultipleImages() throws InterruptedException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        int numImages = 5;
+        for (int i = 0; i < numImages; ++i) {
+            useCase.takePicture(
+                    new ImageCaptureUseCase.OnImageCapturedListener() {
+                        @Override
+                        public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                            mMockImageCapturedListener.onCaptureSuccess(image, rotationDegrees);
+                            image.close();
+
+                            // Signal that an image has been captured.
+                            mSemaphore.release();
+                        }
+                    });
+        }
+
+        // Wait for the signal that all images have been captured.
+        mSemaphore.acquire(numImages);
+
+        verify(mMockImageCapturedListener, times(numImages)).onCaptureSuccess(any(ImageProxy.class),
+                anyInt());
+    }
+
+    @Test
+    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(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        int numImages = 5;
+        for (int i = 0; i < numImages; ++i) {
+            useCase.takePicture(
+                    new ImageCaptureUseCase.OnImageCapturedListener() {
+                        @Override
+                        public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+                            mMockImageCapturedListener.onCaptureSuccess(image, rotationDegrees);
+                            image.close();
+
+                            // Signal that an image has been captured.
+                            mSemaphore.release();
+                        }
+                    });
+        }
+
+        // Wait for the signal that all images have been captured.
+        mSemaphore.acquire(numImages);
+
+        verify(mMockImageCapturedListener, times(numImages)).onCaptureSuccess(any(ImageProxy.class),
+                anyInt());
+    }
+
+    @Test
+    public void saveCanSucceed() throws InterruptedException, IOException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        useCase.takePicture(saveLocation, mOnImageSavedListener);
+
+        // Wait for the signal that the image has been saved.
+        mSemaphore.acquire();
+
+        verify(mMockImageSavedListener).onImageSaved(eq(saveLocation));
+    }
+
+    @Test
+    public void canSaveFile_withRotation()
+            throws InterruptedException, IOException, CameraInfoUnavailableException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Metadata metadata = new Metadata();
+        useCase.takePicture(saveLocation, mOnImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        mSemaphore.acquire();
+
+        // Retrieve the sensor orientation
+        int rotationDegrees = CameraX.getCameraInfo(mCameraId).getSensorRotationDegrees();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.getRotation()).isEqualTo(rotationDegrees);
+    }
+
+    @Test
+    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(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Metadata metadata = new Metadata();
+        metadata.isReversedHorizontal = true;
+        useCase.takePicture(saveLocation, mOnImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        mSemaphore.acquire();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.isFlippedHorizontally()).isTrue();
+    }
+
+    @Test
+    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(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Metadata metadata = new Metadata();
+        metadata.isReversedVertical = true;
+        useCase.takePicture(saveLocation, mOnImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        mSemaphore.acquire();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.isFlippedVertically()).isTrue();
+    }
+
+    @Test
+    public void canSaveFile_withAttachedLocation() throws InterruptedException, IOException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+        Location location = new Location("ImageCaptureUseCaseTest");
+        Metadata metadata = new Metadata();
+        metadata.location = location;
+        useCase.takePicture(saveLocation, mOnImageSavedListener, metadata);
+
+        // Wait for the signal that the image has been saved.
+        mSemaphore.acquire();
+
+        // Retrieve the exif from the image
+        Exif exif = Exif.createFromFile(saveLocation);
+        assertThat(exif.getLocation().getProvider()).isEqualTo(location.getProvider());
+    }
+
+    @Test
+    public void canSaveMultipleFiles() throws InterruptedException, IOException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        int numImages = 5;
+        for (int i = 0; i < numImages; ++i) {
+            File saveLocation = File.createTempFile("test" + i, ".jpg");
+            saveLocation.deleteOnExit();
+
+            useCase.takePicture(saveLocation, mOnImageSavedListener);
+        }
+
+        // Wait for the signal that all images have been saved.
+        mSemaphore.acquire(numImages);
+
+        verify(mMockImageSavedListener, times(numImages)).onImageSaved(any(File.class));
+    }
+
+    @Test
+    public void saveWillFail_whenInvalidFilePathIsUsed() throws InterruptedException {
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+        useCase.addStateChangeListener(mCamera);
+
+        // Note the invalid path
+        File saveLocation = new File("/not/a/real/path.jpg");
+        useCase.takePicture(saveLocation, mOnImageSavedListener);
+
+        // Wait for the signal that an error occurred.
+        mSemaphore.acquire();
+
+        verify(mMockImageSavedListener)
+                .onError(eq(UseCaseError.FILE_IO_ERROR), anyString(), any(Throwable.class));
+    }
+
+    @Test
+    public void updateSessionConfigurationWithSuggestedResolution() throws InterruptedException {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder().setCallbackHandler(mHandler).build();
+        ImageCaptureUseCase useCase = new ImageCaptureUseCase(configuration);
+        useCase.addStateChangeListener(mCamera);
+        final Size[] sizes = {new Size(1920, 1080), new Size(640, 480)};
+
+        for (Size size : sizes) {
+            Map<String, Size> suggestedResolutionMap = new HashMap<>();
+            suggestedResolutionMap.put(mCameraId, size);
+            // Update SessionConfiguration with resolution setting
+            useCase.updateSuggestedResolution(suggestedResolutionMap);
+            CameraUtil.openCameraWithUseCase(mCameraId, mCamera, useCase, mRepeatingUseCase);
+
+            useCase.takePicture(mOnImageCapturedListener);
+            // Wait for the signal that the image has been captured.
+            mSemaphore.acquire();
+
+            assertThat(new Size(mCapturedImage.getWidth(), mCapturedImage.getHeight()))
+                    .isEqualTo(size);
+
+            // Detach use case from camera device to run next resolution setting
+            CameraUtil.detachUseCaseFromCamera(mCamera, useCase);
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysTest.java
new file mode 100644
index 0000000..f23544e
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ImageReaderProxysTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.CameraX;
+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.camera.testing.CameraUtil;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ImageReaderProxysTest {
+    private static final String CAMERA_ID = "0";
+
+    private BaseCamera mCamera;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+
+    private static ImageReaderProxy.OnImageAvailableListener createSemaphoreReleasingListener(
+            final Semaphore semaphore) {
+        return new ImageReaderProxy.OnImageAvailableListener() {
+            @Override
+            public void onImageAvailable(ImageReaderProxy 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);
+        mCamera = cameraFactory.getCamera(CAMERA_ID);
+        mHandlerThread = new HandlerThread("Background");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        mCamera.release();
+        mHandlerThread.quitSafely();
+    }
+
+    @Test
+    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, mHandler);
+            Semaphore semaphore = new Semaphore(/*permits=*/ 0);
+            reader.setOnImageAvailableListener(
+                    createSemaphoreReleasingListener(semaphore), mHandler);
+            readers.add(reader);
+            semaphores.add(semaphore);
+        }
+
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        UseCase useCase = new UseCase(configuration, readers);
+        CameraUtil.openCameraWithUseCase(CAMERA_ID, mCamera, useCase);
+
+        // Wait for a few frames to be observed.
+        for (Semaphore semaphore : semaphores) {
+            semaphore.acquire(/*permits=*/ 5);
+        }
+    }
+
+    @Test
+    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, mHandler);
+            Semaphore semaphore = new Semaphore(/*permits=*/ 0);
+            reader.setOnImageAvailableListener(
+                    createSemaphoreReleasingListener(semaphore), mHandler);
+            readers.add(reader);
+            semaphores.add(semaphore);
+        }
+
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        UseCase useCase = new UseCase(configuration, readers);
+        CameraUtil.openCameraWithUseCase(CAMERA_ID, mCamera, 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> mImageReaders;
+
+        private UseCase(FakeUseCaseConfiguration configuration, List<ImageReaderProxy> readers) {
+            super(configuration);
+            mImageReaders = 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 : mImageReaders) {
+                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..0153cb7
--- /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 mOnOpenedSemaphore = new Semaphore(0);
+        private final Semaphore mOnClosedSemaphore = new Semaphore(0);
+        private final Semaphore mOnDisconnectedSemaphore = new Semaphore(0);
+        private final Semaphore mOnErrorSemaphore = new Semaphore(0);
+
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+            mOnOpenedSemaphore.release();
+        }
+
+        @Override
+        public void onClosed(CameraDevice cameraDevice) {
+            mOnClosedSemaphore.release();
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            mOnDisconnectedSemaphore.release();
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+            mOnErrorSemaphore.release();
+        }
+
+        void waitForOnOpened(int count) throws InterruptedException {
+            mOnOpenedSemaphore.acquire(count);
+        }
+
+        void waitForOnClosed(int count) throws InterruptedException {
+            mOnClosedSemaphore.acquire(count);
+        }
+
+        void waitForOnDisconnected(int count) throws InterruptedException {
+            mOnDisconnectedSemaphore.acquire(count);
+        }
+
+        void waitForOnError(int count) throws InterruptedException {
+            mOnErrorSemaphore.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 mOnConfiguredSemaphore = new Semaphore(0);
+        private final Semaphore mOnActiveSemaphore = new Semaphore(0);
+        private final Semaphore mOnClosedSemaphore = new Semaphore(0);
+        private final Semaphore mOnReadySemaphore = new Semaphore(0);
+        private final Semaphore mOnCaptureQueueEmptySemaphore = new Semaphore(0);
+        private final Semaphore mOnSurfacePreparedSemaphore = new Semaphore(0);
+        private final Semaphore mOnConfigureFailedSemaphore = new Semaphore(0);
+
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            mOnConfiguredSemaphore.release();
+        }
+
+        @Override
+        public void onActive(CameraCaptureSession session) {
+            mOnActiveSemaphore.release();
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            mOnClosedSemaphore.release();
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            mOnReadySemaphore.release();
+        }
+
+        @Override
+        public void onCaptureQueueEmpty(CameraCaptureSession session) {
+            mOnCaptureQueueEmptySemaphore.release();
+        }
+
+        @Override
+        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+            mOnSurfacePreparedSemaphore.release();
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            mOnConfigureFailedSemaphore.release();
+        }
+
+        void waitForOnConfigured(int count) throws InterruptedException {
+            mOnConfiguredSemaphore.acquire(count);
+        }
+
+        void waitForOnActive(int count) throws InterruptedException {
+            mOnActiveSemaphore.acquire(count);
+        }
+
+        void waitForOnClosed(int count) throws InterruptedException {
+            mOnClosedSemaphore.acquire(count);
+        }
+
+        void waitForOnReady(int count) throws InterruptedException {
+            mOnReadySemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureQueueEmpty(int count) throws InterruptedException {
+            mOnCaptureQueueEmptySemaphore.acquire(count);
+        }
+
+        void waitForOnSurfacePrepared(int count) throws InterruptedException {
+            mOnSurfacePreparedSemaphore.acquire(count);
+        }
+
+        void waitForOnConfigureFailed(int count) throws InterruptedException {
+            mOnConfigureFailedSemaphore.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 mOnCaptureBufferLostSemaphore = new Semaphore(0);
+        private final Semaphore mOnCaptureCompletedSemaphore = new Semaphore(0);
+        private final Semaphore mOnCaptureFailedSemaphore = new Semaphore(0);
+        private final Semaphore mOnCaptureProgressedSemaphore = new Semaphore(0);
+        private final Semaphore mOnCaptureSequenceAbortedSemaphore = new Semaphore(0);
+        private final Semaphore mOnCaptureSequenceCompletedSemaphore = new Semaphore(0);
+        private final Semaphore mOnCaptureStartedSemaphore = new Semaphore(0);
+
+        @Override
+        public void onCaptureBufferLost(
+                CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+            mOnCaptureBufferLostSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureCompleted(
+                CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+            mOnCaptureCompletedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureFailed(
+                CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+            mOnCaptureFailedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureProgressed(
+                CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+            mOnCaptureProgressedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+            mOnCaptureSequenceAbortedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(
+                CameraCaptureSession session, int sequenceId, long frame) {
+            mOnCaptureSequenceCompletedSemaphore.release();
+        }
+
+        @Override
+        public void onCaptureStarted(
+                CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+            mOnCaptureStartedSemaphore.release();
+        }
+
+        void waitForOnCaptureBufferLost(int count) throws InterruptedException {
+            mOnCaptureBufferLostSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureCompleted(int count) throws InterruptedException {
+            mOnCaptureCompletedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureFailed(int count) throws InterruptedException {
+            mOnCaptureFailedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureProgressed(int count) throws InterruptedException {
+            mOnCaptureProgressedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureSequenceAborted(int count) throws InterruptedException {
+            mOnCaptureSequenceAbortedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureSequenceCompleted(int count) throws InterruptedException {
+            mOnCaptureSequenceCompletedSemaphore.acquire(count);
+        }
+
+        void waitForOnCaptureStarted(int count) throws InterruptedException {
+            mOnCaptureStartedSemaphore.acquire(count);
+        }
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
new file mode 100644
index 0000000..4deb41f
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.Manifest;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraDevice;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.util.Size;
+import android.view.Surface;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraRepository;
+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.ImageProxy;
+import androidx.camera.core.ImmediateSurface;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseGroup;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+/**
+ * Contains tests for {@link androidx.camera.core.CameraX} which varies use case combinations to
+ * run.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseCombinationTest {
+    private static final String TAG = "UseCaseCombinationTest";
+    private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+    private static final LensFacing DEFAULT_LENS_FACING = LensFacing.BACK;
+    private final MutableLiveData<Long> mAnalysisResult = new MutableLiveData<>();
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(
+            Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO);
+    private Semaphore mSemaphore;
+    private FakeLifecycleOwner mLifecycle;
+    private HandlerThread mHandlerThread;
+    private Handler mMainThreadHandler;
+    private CallbackAttachingImageCaptureUseCase mImageCaptureUseCase;
+    private ImageAnalysisUseCase mImageAnalysisUseCase;
+    private ViewFinderUseCase mViewFinderUseCase;
+    private ImageAnalysisUseCase.Analyzer mImageAnalyzer;
+    private CameraRepository mCameraRepository;
+    private CameraFactory mCameraFactory;
+    private UseCaseGroup mUseCaseGroup;
+
+    private Observer<Long> createCountIncrementingObserver() {
+        return new Observer<Long>() {
+            @Override
+            public void onChanged(Long value) {
+                mSemaphore.release();
+            }
+        };
+    }
+
+    @Before
+    public void setUp() {
+        Context context = ApplicationProvider.getApplicationContext();
+        CameraX.init(context, Camera2AppConfiguration.create(context));
+        mLifecycle = new FakeLifecycleOwner();
+        mHandlerThread = new HandlerThread("ErrorHandlerThread");
+        mHandlerThread.start();
+        mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+        mSemaphore = new Semaphore(0);
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        CameraX.unbindAll();
+        mHandlerThread.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 Combination: ViewFinder + ImageCaptureUseCase
+     */
+    @Test
+    public void viewFinderCombinesImageCapture() throws InterruptedException {
+        initViewFinderUseCase();
+        initImageCaptureUseCase();
+
+        mUseCaseGroup.addUseCase(mImageCaptureUseCase);
+        mUseCaseGroup.addUseCase(mViewFinderUseCase);
+
+        CameraX.bindToLifecycle(mLifecycle, mViewFinderUseCase, mImageCaptureUseCase);
+        mLifecycle.startAndResume();
+
+        mImageCaptureUseCase.doNotifyActive();
+        mCameraRepository.onGroupActive(mUseCaseGroup);
+
+        // Wait for the CameraCaptureSession.onConfigured callback.
+        mImageCaptureUseCase.mSessionStateCallback.waitForOnConfigured(1);
+
+        assertThat(mLifecycle.getObserverCount()).isEqualTo(2);
+        assertThat(CameraX.isBound(mViewFinderUseCase)).isTrue();
+        assertThat(CameraX.isBound(mImageCaptureUseCase)).isTrue();
+    }
+
+    /**
+     * Test Combination: ViewFinder + ImageAnalysisUseCase
+     */
+    @Test
+    public void viewFinderCombinesImageAnalysis() throws InterruptedException {
+        initImageAnalysisUseCase();
+        initViewFinderUseCase();
+
+        mMainThreadHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mImageAnalysisUseCase.setAnalyzer(mImageAnalyzer);
+
+                mAnalysisResult.observe(mLifecycle,
+                        createCountIncrementingObserver());
+
+                CameraX.bindToLifecycle(mLifecycle, mViewFinderUseCase, mImageAnalysisUseCase);
+                mLifecycle.startAndResume();
+            }
+        });
+
+        // Wait for 10 frames to be analyzed.
+        mSemaphore.acquire(10);
+
+        assertThat(CameraX.isBound(mViewFinderUseCase)).isTrue();
+        assertThat(CameraX.isBound(mImageAnalysisUseCase)).isTrue();
+    }
+
+    /**
+     * Test Combination: ViewFinder + ImageAnalysis + ImageCaptureUseCase
+     */
+    @Test
+    public void viewFinderCombinesImageAnalysisAndImageCapture() throws InterruptedException {
+        initViewFinderUseCase();
+        initImageAnalysisUseCase();
+        initImageCaptureUseCase();
+
+        mUseCaseGroup.addUseCase(mImageCaptureUseCase);
+        mUseCaseGroup.addUseCase(mImageAnalysisUseCase);
+        mUseCaseGroup.addUseCase(mViewFinderUseCase);
+
+        mImageCaptureUseCase.doNotifyActive();
+        mCameraRepository.onGroupActive(mUseCaseGroup);
+
+        mMainThreadHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mImageAnalysisUseCase.setAnalyzer(mImageAnalyzer);
+
+                mAnalysisResult.observe(mLifecycle,
+                        createCountIncrementingObserver());
+            }
+        });
+
+        CameraX.bindToLifecycle(mLifecycle, mViewFinderUseCase, mImageAnalysisUseCase,
+                mImageCaptureUseCase);
+        mLifecycle.startAndResume();
+
+        // Wait for 10 frames to be analyzed.
+        mSemaphore.acquire(10);
+
+        // Wait for the CameraCaptureSession.onConfigured callback.
+        try {
+            mImageCaptureUseCase.mSessionStateCallback.waitForOnConfigured(1);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        assertThat(CameraX.isBound(mViewFinderUseCase)).isTrue();
+        assertThat(CameraX.isBound(mImageAnalysisUseCase)).isTrue();
+        assertThat(CameraX.isBound(mImageCaptureUseCase)).isTrue();
+    }
+
+    private void initImageAnalysisUseCase() {
+        ImageAnalysisUseCaseConfiguration imageAnalysisUseCaseConfiguration =
+                new ImageAnalysisUseCaseConfiguration.Builder()
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .setTargetName("ImageAnalysis")
+                        .setCallbackHandler(new Handler(Looper.getMainLooper()))
+                        .build();
+        mImageAnalyzer =
+                new ImageAnalysisUseCase.Analyzer() {
+                    @Override
+                    public void analyze(ImageProxy image, int rotationDegrees) {
+                        mAnalysisResult.postValue(image.getTimestamp());
+                    }
+                };
+        mImageAnalysisUseCase = new ImageAnalysisUseCase(imageAnalysisUseCaseConfiguration);
+    }
+
+    private void initImageCaptureUseCase() {
+        mCameraRepository = new CameraRepository();
+        mCameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext());
+        mCameraRepository.init(mCameraFactory);
+        mUseCaseGroup = new UseCaseGroup();
+
+        ImageCaptureUseCaseConfiguration imageCaptureUseCaseConfiguration =
+                new ImageCaptureUseCaseConfiguration.Builder().setLensFacing(
+                        LensFacing.BACK).build();
+        String cameraId = getCameraIdForLensFacingUnchecked(
+                imageCaptureUseCaseConfiguration.getLensFacing());
+        mImageCaptureUseCase = new CallbackAttachingImageCaptureUseCase(
+                imageCaptureUseCaseConfiguration, cameraId);
+
+        mImageCaptureUseCase.addStateChangeListener(
+                mCameraRepository.getCamera(
+                        getCameraIdForLensFacingUnchecked(
+                                imageCaptureUseCaseConfiguration.getLensFacing())));
+    }
+
+    private void initViewFinderUseCase() {
+        ViewFinderUseCaseConfiguration viewFinderUseCaseConfiguration =
+                new ViewFinderUseCaseConfiguration.Builder()
+                        .setLensFacing(DEFAULT_LENS_FACING)
+                        .setTargetName("ViewFinder")
+                        .build();
+
+        mViewFinderUseCase = new ViewFinderUseCase(viewFinderUseCaseConfiguration);
+    }
+
+    private String getCameraIdForLensFacingUnchecked(LensFacing lensFacing) {
+        try {
+            return mCameraFactory.cameraIdForLensFacing(lensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + lensFacing, e);
+        }
+    }
+
+    /** A use case which attaches to a camera with various callbacks. */
+    private static class CallbackAttachingImageCaptureUseCase extends ImageCaptureUseCase {
+        private final SemaphoreReleasingCamera2Callbacks.SessionStateCallback
+                mSessionStateCallback =
+                new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
+        private final SurfaceTexture mSurfaceTexture = new SurfaceTexture(0);
+
+        CallbackAttachingImageCaptureUseCase(ImageCaptureUseCaseConfiguration configuration,
+                String cameraId) {
+            super(configuration);
+
+            SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+            builder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+            builder.addSurface(new ImmediateSurface(new Surface(mSurfaceTexture)));
+            builder.setSessionStateCallback(mSessionStateCallback);
+
+            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/UseCaseSurfaceOccupancyManagerTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerTest.java
new file mode 100644
index 0000000..7f51673
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/UseCaseSurfaceOccupancyManagerTest.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.camera2;
+
+import android.content.Context;
+
+import androidx.camera.core.AppConfiguration;
+import androidx.camera.core.BaseUseCase;
+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 androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+/** JUnit test cases for UseCaseSurfaceOccupancyManager class. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseSurfaceOccupancyManagerTest {
+
+    @Before
+    public void setUp() {
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraX.init(context, appConfig);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void failedWhenBindTooManyImageCaptureUseCase() {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder().build();
+        ImageCaptureUseCase useCase1 = new ImageCaptureUseCase(configuration);
+        ImageCaptureUseCase useCase2 = new ImageCaptureUseCase(configuration);
+
+        // Should throw IllegalArgumentException
+        UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+                Collections.<BaseUseCase>singletonList(useCase1),
+                Collections.<BaseUseCase>singletonList(useCase2));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void failedWhenBindTooManyVideoCaptureUseCase() {
+        VideoCaptureUseCaseConfiguration configuration =
+                new VideoCaptureUseCaseConfiguration.Builder().build();
+        VideoCaptureUseCase useCase1 = new VideoCaptureUseCase(configuration);
+        VideoCaptureUseCase useCase2 = new VideoCaptureUseCase(configuration);
+
+        // Should throw IllegalArgumentException
+        UseCaseSurfaceOccupancyManager.checkUseCaseLimitNotExceeded(
+                Collections.<BaseUseCase>singletonList(useCase1),
+                Collections.<BaseUseCase>singletonList(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(
+                Collections.<BaseUseCase>singletonList(imageCaptureUseCase),
+                Collections.<BaseUseCase>singletonList(videoCaptureUseCase));
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseTest.java
new file mode 100644
index 0000000..4dae785
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/VideoCaptureUseCaseTest.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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.Manifest;
+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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.GrantPermissionRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+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
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class VideoCaptureUseCaseTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+
+    private final Context mContext = InstrumentationRegistry.getTargetContext();
+    private final StateChangeListener mListener = Mockito.mock(StateChangeListener.class);
+    private final ArgumentCaptor<BaseUseCase> mBaseUseCaseCaptor =
+            ArgumentCaptor.forClass(BaseUseCase.class);
+    private final OnVideoSavedListener mMockVideoSavedListener =
+            Mockito.mock(OnVideoSavedListener.class);
+    private VideoCaptureUseCaseConfiguration mDefaultConfiguration;
+    private String mCameraId;
+
+    @Rule
+    public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(
+            Manifest.permission.RECORD_AUDIO);
+
+    @Before
+    public void setUp() {
+        mDefaultConfiguration = VideoCaptureUseCase.DEFAULT_CONFIG.getConfiguration(null);
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfiguration = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            mCameraId = 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(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(mListener);
+
+        useCase.startRecording(
+                new File(
+                        mContext.getFilesDir()
+                                + "/useCaseBecomesActive_whenStartingVideoRecording.mp4"),
+                mMockVideoSavedListener);
+
+        verify(mListener, times(1)).onUseCaseActive(mBaseUseCaseCaptor.capture());
+        assertThat(mBaseUseCaseCaptor.getValue()).isSameAs(useCase);
+    }
+
+    @Test
+    public void useCaseBecomesInactive_whenStoppingVideoRecording() {
+        VideoCaptureUseCase useCase = new VideoCaptureUseCase(mDefaultConfiguration);
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(mListener);
+
+        useCase.startRecording(
+                new File(
+                        mContext.getFilesDir()
+                                + "/useCaseBecomesInactive_whenStoppingVideoRecording.mp4"),
+                mMockVideoSavedListener);
+
+        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(mListener, times(1)).onUseCaseInactive(mBaseUseCaseCaptor.capture());
+        assertThat(mBaseUseCaseCaptor.getValue()).isSameAs(useCase);
+    }
+
+    @Test
+    public void updateSessionConfigurationWithSuggestedResolution() {
+        VideoCaptureUseCase useCase = new VideoCaptureUseCase(mDefaultConfiguration);
+        // Create video encoder with default 1920x1080 resolution
+        Map<String, Size> suggestedResolutionMap = new HashMap<>();
+        suggestedResolutionMap.put(mCameraId, DEFAULT_RESOLUTION);
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+        useCase.addStateChangeListener(mListener);
+
+        // Recreate video encoder with new 640x480 resolution
+        suggestedResolutionMap.put(mCameraId, new Size(640, 480));
+        useCase.updateSuggestedResolution(suggestedResolutionMap);
+
+        // Check it could be started to record and become active
+        useCase.startRecording(
+                new File(
+                        mContext.getFilesDir()
+                                + "/useCaseBecomesInactive_whenStoppingVideoRecording.mp4"),
+                mMockVideoSavedListener);
+
+        verify(mListener, times(1)).onUseCaseActive(mBaseUseCaseCaptor.capture());
+        assertThat(mBaseUseCaseCaptor.getValue()).isSameAs(useCase);
+    }
+}
diff --git a/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseTest.java b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseTest.java
new file mode 100644
index 0000000..1a267e4
--- /dev/null
+++ b/camera/camera2/src/androidTest/java/androidx/camera/camera2/ViewFinderUseCaseTest.java
@@ -0,0 +1,510 @@
+/*
+ * 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+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.OnFocusCompletedListener;
+import androidx.camera.core.SessionConfiguration;
+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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ViewFinderUseCaseTest {
+    private static final Size DEFAULT_RESOLUTION = new Size(1920, 1080);
+    private static final Size SECONDARY_RESOLUTION = new Size(1280, 720);
+
+    private ViewFinderUseCaseConfiguration mDefaultConfiguration;
+    @Mock
+    private OnViewFinderOutputUpdateListener mMockListener;
+    private String mCameraId;
+
+    @Before
+    public void setUp() {
+        // Instantiates OnViewFinderOutputUpdateListener before each test run.
+        mMockListener = mock(OnViewFinderOutputUpdateListener.class);
+        Context context = ApplicationProvider.getApplicationContext();
+        AppConfiguration appConfig = Camera2AppConfiguration.create(context);
+        CameraFactory cameraFactory = appConfig.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            mCameraId = 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
+        mDefaultConfiguration = ViewFinderUseCase.DEFAULT_CONFIG.getConfiguration(null);
+    }
+
+    @Test
+    @UiThreadTest
+    public void useCaseIsConstructedWithDefaultConfiguration() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        List<Surface> surfaces =
+                DeferrableSurfaces.surfaceList(
+                        useCase.getSessionConfiguration(mCameraId).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(mCameraId, DEFAULT_RESOLUTION));
+
+        List<Surface> surfaces =
+                DeferrableSurfaces.surfaceList(
+                        useCase.getSessionConfiguration(mCameraId).getSurfaces());
+
+        assertThat(surfaces.size()).isEqualTo(1);
+        assertThat(surfaces.get(0).isValid()).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void focusRegionCanBeSet() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        CameraControl cameraControl = mock(CameraControl.class);
+        useCase.attachCameraControl(mCameraId, cameraControl);
+
+        Rect rect = new Rect(/*left=*/ 200, /*top=*/ 200, /*right=*/ 800, /*bottom=*/ 800);
+        useCase.focus(rect, rect, mock(OnFocusCompletedListener.class));
+
+        ArgumentCaptor<Rect> rectArgumentCaptor1 = ArgumentCaptor.forClass(Rect.class);
+        ArgumentCaptor<Rect> rectArgumentCaptor2 = ArgumentCaptor.forClass(Rect.class);
+        verify(cameraControl).focus(rectArgumentCaptor1.capture(), rectArgumentCaptor2.capture(),
+                any(OnFocusCompletedListener.class), any(Handler.class));
+        assertThat(rectArgumentCaptor1.getValue()).isEqualTo(rect);
+        assertThat(rectArgumentCaptor2.getValue()).isEqualTo(rect);
+    }
+
+    @Test
+    @UiThreadTest
+    public void zoomRegionCanBeSet() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        CameraControl cameraControl = mock(CameraControl.class);
+        useCase.attachCameraControl(mCameraId, cameraControl);
+
+        Rect rect = new Rect(/*left=*/ 200, /*top=*/ 200, /*right=*/ 800, /*bottom=*/ 800);
+        useCase.zoom(rect);
+
+        ArgumentCaptor<Rect> rectArgumentCaptor = ArgumentCaptor.forClass(Rect.class);
+        verify(cameraControl).setCropRegion(rectArgumentCaptor.capture());
+        assertThat(rectArgumentCaptor.getValue()).isEqualTo(rect);
+    }
+
+    @Test
+    @UiThreadTest
+    public void torchModeCanBeSet() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        CameraControl cameraControl = getFakeCameraControl();
+        useCase.attachCameraControl(mCameraId, cameraControl);
+
+        useCase.enableTorch(true);
+
+        assertThat(useCase.isTorchOn()).isTrue();
+    }
+
+    @Test
+    @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(mDefaultConfiguration);
+
+        final SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        final FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput viewFinderOutput) {
+                        surfaceTextureCallable0.setSurfaceTexture(
+                                viewFinderOutput.getSurfaceTexture());
+                        future0.run();
+                    }
+                });
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        SurfaceTexture surfaceTexture0 = future0.get(1, TimeUnit.SECONDS);
+        surfaceTexture0.release();
+
+        final SurfaceTextureCallable surfaceTextureCallable1 = new SurfaceTextureCallable();
+        final FutureTask<SurfaceTexture> future1 = new FutureTask<>(surfaceTextureCallable1);
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput viewFinderOutput) {
+                        surfaceTextureCallable1.setSurfaceTexture(
+                                viewFinderOutput.getSurfaceTexture());
+                        future1.run();
+                    }
+                });
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture1 = future1.get(1, TimeUnit.SECONDS);
+
+        assertThat(surfaceTexture1.isReleased()).isFalse();
+    }
+
+    @Test
+    @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(mDefaultConfiguration);
+
+        final SurfaceTextureCallable surfaceTextureCallable = new SurfaceTextureCallable();
+        final FutureTask<SurfaceTexture> future = new FutureTask<>(surfaceTextureCallable);
+
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput viewFinderOutput) {
+                        surfaceTextureCallable.setSurfaceTexture(
+                                viewFinderOutput.getSurfaceTexture());
+                        future.run();
+                    }
+                });
+
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture = future.get(1, TimeUnit.SECONDS);
+
+        useCase.clear();
+
+        assertThat(surfaceTexture.isReleased()).isFalse();
+    }
+
+    @Test
+    @UiThreadTest
+    public void surfaceTexture_isListenedOnlyOnce()
+            throws InterruptedException, ExecutionException, TimeoutException {
+
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+
+        final SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        final FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput viewFinderOutput) {
+                        surfaceTextureCallable0.setSurfaceTexture(
+                                viewFinderOutput.getSurfaceTexture());
+                        future0.run();
+                    }
+                });
+
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture0 = future0.get();
+
+        final SurfaceTextureCallable surfaceTextureCallable1 = new SurfaceTextureCallable();
+        final FutureTask<SurfaceTexture> future1 = new FutureTask<>(surfaceTextureCallable1);
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput 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(mCameraId, size));
+
+            List<Surface> surfaces =
+                    DeferrableSurfaces.surfaceList(
+                            useCase.getSessionConfiguration(mCameraId).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(mDefaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+        OnViewFinderOutputUpdateListener viewFinderOutputListener =
+                useCase.getOnViewFinderOutputUpdateListener();
+        useCase.setOnViewFinderOutputUpdateListener(mMockListener);
+
+        OnViewFinderOutputUpdateListener retrievedViewFinderOutputListener =
+                useCase.getOnViewFinderOutputUpdateListener();
+
+        assertThat(viewFinderOutputListener).isNull();
+        assertThat(retrievedViewFinderOutputListener).isSameAs(mMockListener);
+    }
+
+    @Test
+    @UiThreadTest
+    public void clear_removeViewFinderOutputListener() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        useCase.setOnViewFinderOutputUpdateListener(mMockListener);
+        useCase.clear();
+
+        assertThat(useCase.getOnViewFinderOutputUpdateListener()).isNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void viewFinderOutput_isResetOnUpdatedResolution() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        final AtomicInteger calledCount = new AtomicInteger(0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput viewFinderOutput) {
+                        calledCount.incrementAndGet();
+                    }
+                });
+
+        int initialCount = calledCount.get();
+
+        useCase.updateSuggestedResolution(
+                Collections.singletonMap(mCameraId, SECONDARY_RESOLUTION));
+
+        int countAfterUpdate = calledCount.get();
+
+        assertThat(initialCount).isEqualTo(1);
+        assertThat(countAfterUpdate).isEqualTo(2);
+    }
+
+    @Test
+    @UiThreadTest
+    public void viewFinderOutput_updatesWithTargetRotation() {
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        useCase.setTargetRotation(Surface.ROTATION_0);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        final AtomicReference<ViewFinderOutput> latestViewFinderOutput = new AtomicReference<>();
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput 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
+    public void viewFinderOutput_isResetByReleasedSurface()
+            throws InterruptedException, ExecutionException {
+        final ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+        Handler mainHandler = new Handler(Looper.getMainLooper());
+        final Semaphore semaphore = new Semaphore(0);
+
+        mainHandler.post(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        useCase.updateSuggestedResolution(
+                                Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+                        useCase.setOnViewFinderOutputUpdateListener(
+                                new OnViewFinderOutputUpdateListener() {
+                                    @Override
+                                    public void onUpdated(ViewFinderOutput 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(mCameraId).getSurfaces().get(0).getSurface().get();
+
+        // Wait for the surface to reset
+        semaphore.acquire();
+    }
+
+    @Test
+    @UiThreadTest
+    public void outputIsPublished_whenListenerIsSetBefore()
+            throws InterruptedException, ExecutionException {
+
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+
+        final SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        final FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput viewFinderOutput) {
+                        surfaceTextureCallable0.setSurfaceTexture(
+                                viewFinderOutput.getSurfaceTexture());
+                        future0.run();
+                    }
+                });
+
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+        SurfaceTexture surfaceTexture0 = future0.get();
+
+        assertThat(surfaceTexture0).isNotNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void outputIsPublished_whenListenerIsSetAfter()
+            throws InterruptedException, ExecutionException {
+
+        ViewFinderUseCase useCase = new ViewFinderUseCase(mDefaultConfiguration);
+
+        final SurfaceTextureCallable surfaceTextureCallable0 = new SurfaceTextureCallable();
+        final FutureTask<SurfaceTexture> future0 = new FutureTask<>(surfaceTextureCallable0);
+        useCase.updateSuggestedResolution(Collections.singletonMap(mCameraId, DEFAULT_RESOLUTION));
+
+        useCase.setOnViewFinderOutputUpdateListener(
+                new OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderOutput viewFinderOutput) {
+                        surfaceTextureCallable0.setSurfaceTexture(
+                                viewFinderOutput.getSurfaceTexture());
+                        future0.run();
+                    }
+                });
+        SurfaceTexture surfaceTexture0 = future0.get();
+
+        assertThat(surfaceTexture0).isNotNull();
+    }
+
+    private CameraControl getFakeCameraControl() {
+        return new Camera2CameraControl(
+                new CameraControl.ControlUpdateListener() {
+                    @Override
+                    public void onCameraControlUpdateSessionConfiguration(
+                            SessionConfiguration sessionConfiguration) {
+                    }
+
+                    @Override
+                    public void onCameraControlSingleRequest(
+                            CaptureRequestConfiguration captureRequestConfiguration) {
+                    }
+                },
+                new Handler());
+    }
+
+    private static final class SurfaceTextureCallable implements Callable<SurfaceTexture> {
+        SurfaceTexture mSurfaceTexture;
+
+        void setSurfaceTexture(SurfaceTexture surfaceTexture) {
+            this.mSurfaceTexture = surfaceTexture;
+        }
+
+        @Override
+        public SurfaceTexture call() {
+            return mSurfaceTexture;
+        }
+    }
+}
diff --git a/camera/camera2/src/main/AndroidManifest.xml b/camera/camera2/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2aac609
--- /dev/null
+++ b/camera/camera2/src/main/AndroidManifest.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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.camera2">
+
+    <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..8764d74
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CamcorderProfileHelper.java
@@ -0,0 +1,31 @@
+/*
+ * 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.annotation.RestrictTo;
+
+/**
+ * This is helper class to use {@link android.media.CamcorderProfile} that may be mocked.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface CamcorderProfileHelper {
+
+    /** Returns true if the camcorder profile exists for the given camera and quality. */
+    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..25938e2
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera.java
@@ -0,0 +1,766 @@
+/*
+ * 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 {
+    private static final String TAG = "Camera";
+
+    private final Object mAttachedUseCaseLock = new Object();
+
+    /** Map of the use cases to the information on their state. */
+    @GuardedBy("mAttachedUseCaseLock")
+    private final UseCaseAttachState mUseCaseAttachState;
+
+    /** The identifier for the {@link CameraDevice} */
+    private final String mCameraId;
+
+    /** Handle to the camera service. */
+    private final CameraManager mCameraManager;
+
+    private final Object mCameraInfoLock = new Object();
+    /** The handler for camera callbacks and use case state management calls. */
+    private final Handler mHandler;
+    /**
+     * 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> mState = new AtomicReference<>(State.UNINITIALIZED);
+    /** The camera control shared across all use cases bound to this Camera. */
+    private final CameraControl mCameraControl;
+    private final StateCallback mStateCallback = new StateCallback();
+    /** Information about the characteristics of this camera */
+    // Nullable because this is lazily instantiated
+    @GuardedBy("mCameraInfoLock")
+    @Nullable
+    private CameraInfo mCameraInfo;
+    /** The handle to the opened camera. */
+    @Nullable
+    CameraDevice mCameraDevice;
+    /** The configured session which handles issuing capture requests. */
+    private CaptureSession mCaptureSession = new CaptureSession(null);
+    /** The session configuration of camera control. */
+    private SessionConfiguration mCameraControlSessionConfiguration =
+            SessionConfiguration.defaultEmptySessionConfiguration();
+
+    /**
+     * 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) {
+        mCameraManager = cameraManager;
+        mCameraId = cameraId;
+        mHandler = handler;
+        mUseCaseAttachState = new UseCaseAttachState(cameraId);
+        mState.set(State.INITIALIZED);
+        mCameraControl = 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() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.open();
+                }
+            });
+            return;
+        }
+
+        switch (mState.get()) {
+            case INITIALIZED:
+                openCameraDevice();
+                break;
+            case CLOSING:
+                mState.set(State.REOPENING);
+                break;
+            default:
+                Log.d(TAG, "open() ignored due to being in state: " + mState.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() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.close();
+                }
+            });
+            return;
+        }
+
+        Log.d(TAG, "Closing camera: " + mCameraId);
+        switch (mState.get()) {
+            case OPENED:
+                mState.set(State.CLOSING);
+                mCaptureSession.close();
+                mCameraDevice.close();
+                mCaptureSession.notifyCameraDeviceClose();
+                resetCaptureSession();
+                mCameraDevice = null;
+
+                break;
+            case OPENING:
+            case REOPENING:
+                mState.set(State.CLOSING);
+                break;
+            default:
+                Log.d(TAG, "close() ignored due to being in state: " + mState.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() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.release();
+                }
+            });
+            return;
+        }
+
+        switch (mState.get()) {
+            case INITIALIZED:
+                mState.set(State.RELEASED);
+                break;
+            case OPENED:
+                mState.set(State.RELEASING);
+                mCameraDevice.close();
+                mCaptureSession.notifyCameraDeviceClose();
+                break;
+            case OPENING:
+            case CLOSING:
+            case REOPENING:
+                mState.set(State.RELEASING);
+                break;
+            default:
+                Log.d(TAG, "release() ignored due to being in state: " + mState.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(final BaseUseCase useCase) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.onUseCaseActive(useCase);
+                }
+            });
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " ACTIVE for camera " + mCameraId);
+
+        synchronized (mAttachedUseCaseLock) {
+            mUseCaseAttachState.setUseCaseActive(useCase);
+        }
+        updateCaptureSessionConfiguration();
+    }
+
+    /** Removes the use case from a state of issuing capture requests. */
+    @Override
+    public void onUseCaseInactive(final BaseUseCase useCase) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.onUseCaseInactive(useCase);
+                }
+            });
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " INACTIVE for camera " + mCameraId);
+        synchronized (mAttachedUseCaseLock) {
+            mUseCaseAttachState.setUseCaseInactive(useCase);
+        }
+
+        updateCaptureSessionConfiguration();
+    }
+
+    /** Updates the capture requests based on the latest settings. */
+    @Override
+    public void onUseCaseUpdated(final BaseUseCase useCase) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.onUseCaseUpdated(useCase);
+                }
+            });
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " UPDATED for camera " + mCameraId);
+        synchronized (mAttachedUseCaseLock) {
+            mUseCaseAttachState.updateUseCase(useCase);
+        }
+
+        updateCaptureSessionConfiguration();
+    }
+
+    @Override
+    public void onUseCaseReset(final BaseUseCase useCase) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.onUseCaseReset(useCase);
+                }
+            });
+            return;
+        }
+
+        Log.d(TAG, "Use case " + useCase + " RESET for camera " + mCameraId);
+        synchronized (mAttachedUseCaseLock) {
+            mUseCaseAttachState.updateUseCase(useCase);
+        }
+
+        updateCaptureSessionConfiguration();
+        openCaptureSession();
+    }
+
+    /**
+     * 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(final Collection<BaseUseCase> useCases) {
+        if (useCases.isEmpty()) {
+            return;
+        }
+
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.addOnlineUseCase(useCases);
+                }
+            });
+            return;
+        }
+
+        Log.d(TAG, "Use cases " + useCases + " ONLINE for camera " + mCameraId);
+        synchronized (mAttachedUseCaseLock) {
+            for (BaseUseCase useCase : useCases) {
+                mUseCaseAttachState.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(final Collection<BaseUseCase> useCases) {
+        if (useCases.isEmpty()) {
+            return;
+        }
+
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.removeOnlineUseCase(useCases);
+                }
+            });
+            return;
+        }
+
+        Log.d(TAG, "Use cases " + useCases + " OFFLINE for camera " + mCameraId);
+        synchronized (mAttachedUseCaseLock) {
+            for (BaseUseCase useCase : useCases) {
+                mUseCaseAttachState.setUseCaseOffline(useCase);
+            }
+
+            if (mUseCaseAttachState.getOnlineUseCases().isEmpty()) {
+                close();
+                return;
+            }
+        }
+
+        openCaptureSession();
+        updateCaptureSessionConfiguration();
+    }
+
+    /** Returns an interface to retrieve characteristics of the camera. */
+    @Override
+    public CameraInfo getCameraInfo() throws CameraInfoUnavailableException {
+        synchronized (mCameraInfoLock) {
+            if (mCameraInfo == null) {
+                // Lazily instantiate camera info
+                mCameraInfo = new Camera2CameraInfo(mCameraManager, mCameraId);
+            }
+
+            return mCameraInfo;
+        }
+    }
+
+    /** Opens the camera device */
+    // TODO(b/124268878): Handle SecurityException and require permission in manifest.
+    @SuppressLint("MissingPermission")
+    void openCameraDevice() {
+        mState.set(State.OPENING);
+
+        Log.d(TAG, "Opening camera: " + mCameraId);
+
+        try {
+            mCameraManager.openCamera(mCameraId, createDeviceStateCallback(), mHandler);
+        } catch (CameraAccessException e) {
+            Log.e(TAG, "Unable to open camera " + mCameraId + " due to " + e.getMessage());
+            mState.set(State.INITIALIZED);
+        }
+    }
+
+    /** Updates the capture request configuration for the current capture session. */
+    private void updateCaptureSessionConfiguration() {
+        ValidatingBuilder validatingBuilder;
+        synchronized (mAttachedUseCaseLock) {
+            validatingBuilder = mUseCaseAttachState.getActiveAndOnlineBuilder();
+        }
+
+        if (validatingBuilder.isValid()) {
+            // Apply CameraControl's SessionConfiguration to let CameraControl be able to control
+            // Repeating Request and process results.
+            validatingBuilder.add(mCameraControlSessionConfiguration);
+
+            SessionConfiguration sessionConfiguration = validatingBuilder.build();
+            mCaptureSession.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 (mAttachedUseCaseLock) {
+            validatingBuilder = mUseCaseAttachState.getOnlineBuilder();
+        }
+        if (!validatingBuilder.isValid()) {
+            Log.d(TAG, "Unable to create capture session due to conflicting configurations");
+            return;
+        }
+
+        resetCaptureSession();
+
+        if (mCameraDevice == null) {
+            Log.d(TAG, "CameraDevice is null");
+            return;
+        }
+
+        try {
+            mCaptureSession.open(validatingBuilder.build(), mCameraDevice);
+        } catch (CameraAccessException e) {
+            Log.d(TAG, "Unable to configure camera " + mCameraId + " 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");
+
+        // Recreate an initialized (but not opened) capture session from the previous configuration
+        SessionConfiguration previousSessionConfiguration =
+                mCaptureSession.getSessionConfiguration();
+
+        mCaptureSession.close();
+
+        List<CaptureRequestConfiguration> unissuedCaptureRequestConfigurations =
+                mCaptureSession.getCaptureRequestConfigurations();
+        mCaptureSession = new CaptureSession(mHandler);
+        mCaptureSession.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.
+        mCaptureSession.issueSingleCaptureRequests(unissuedCaptureRequestConfigurations);
+    }
+
+    private CameraDevice.StateCallback createDeviceStateCallback() {
+        synchronized (mAttachedUseCaseLock) {
+            SessionConfiguration configuration = mUseCaseAttachState.getOnlineBuilder().build();
+            return CameraDeviceStateCallbacks.createComboCallback(
+                    mStateCallback, configuration.getDeviceStateCallback());
+        }
+    }
+
+    /**
+     * Checks if there's valid repeating surface and attaches one to
+     * {@link CaptureRequestConfiguration.Builder}.
+     *
+     * @param captureRequestConfigurationBuilder the configuration builder to attach a repeating
+     *                                           surface
+     * @return True if repeating surface has been successfully attached, otherwise false.
+     */
+    private boolean checkAndAttachRepeatingSurface(
+            CaptureRequestConfiguration.Builder captureRequestConfigurationBuilder) {
+        Collection<BaseUseCase> activeUseCases;
+        synchronized (mAttachedUseCaseLock) {
+            activeUseCases = mUseCaseAttachState.getActiveAndOnlineUseCases();
+        }
+
+        DeferrableSurface repeatingSurface = null;
+        for (BaseUseCase useCase : activeUseCases) {
+            SessionConfiguration sessionConfiguration = useCase.getSessionConfiguration(mCameraId);
+            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) {
+            Log.w(TAG,
+                    "Unable to find a repeating surface to attach to CaptureRequestConfiguration");
+            return false;
+        }
+
+        captureRequestConfigurationBuilder.addSurface(repeatingSurface);
+        return true;
+    }
+
+    /** Returns the Camera2CameraControl attached to Camera */
+    @Override
+    public CameraControl getCameraControl() {
+        return mCameraControl;
+    }
+
+    /**
+     * Submits single request
+     *
+     * @param captureRequestConfiguration capture configuration used for creating CaptureRequest
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void submitSingleRequest(final CaptureRequestConfiguration captureRequestConfiguration) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera.this.submitSingleRequest(captureRequestConfiguration);
+                }
+            });
+            return;
+        }
+
+        // Recreates the Builder to add extra config needed
+        CaptureRequestConfiguration.Builder builder =
+                CaptureRequestConfiguration.Builder.from(captureRequestConfiguration);
+
+        if (captureRequestConfiguration.getSurfaces().isEmpty()
+                && captureRequestConfiguration.isUseRepeatingSurface()) {
+            // Checks and attaches if there's valid repeating surface. If there's no, skip this
+            // single request.
+            if (!checkAndAttachRepeatingSurface(builder)) {
+                return;
+            }
+        }
+
+        Log.d(TAG, "issue single capture request for camera " + mCameraId);
+
+        mCaptureSession.issueSingleCaptureRequest(builder.build());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCameraControlUpdateSessionConfiguration(
+            SessionConfiguration sessionConfiguration) {
+        mCameraControlSessionConfiguration = sessionConfiguration;
+        updateCaptureSessionConfiguration();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCameraControlSingleRequest(
+            CaptureRequestConfiguration captureRequestConfiguration) {
+        submitSingleRequest(captureRequestConfiguration);
+    }
+
+    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 (mState.get()) {
+                case CLOSING:
+                case RELEASING:
+                    cameraDevice.close();
+                    Camera.this.mCameraDevice = null;
+                    break;
+                case OPENING:
+                case REOPENING:
+                    mState.set(State.OPENED);
+                    Camera.this.mCameraDevice = cameraDevice;
+                    openCaptureSession();
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "onOpened() should not be possible from state: " + mState.get());
+            }
+        }
+
+        @Override
+        public void onClosed(CameraDevice cameraDevice) {
+            Log.d(TAG, "CameraDevice.onClosed(): " + cameraDevice.getId());
+
+            resetCaptureSession();
+            switch (mState.get()) {
+                case CLOSING:
+                    mState.set(State.INITIALIZED);
+                    Camera.this.mCameraDevice = null;
+                    break;
+                case REOPENING:
+                    mState.set(State.OPENING);
+                    openCameraDevice();
+                    break;
+                case RELEASING:
+                    mState.set(State.RELEASED);
+                    Camera.this.mCameraDevice = null;
+                    break;
+                default:
+                    CameraX.postError(
+                            CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT,
+                            "Camera closed while in state: " + mState.get());
+            }
+
+
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            Log.d(TAG, "CameraDevice.onDisconnected(): " + cameraDevice.getId());
+            resetCaptureSession();
+            switch (mState.get()) {
+                case CLOSING:
+                    mState.set(State.INITIALIZED);
+                    Camera.this.mCameraDevice = null;
+                    break;
+                case REOPENING:
+                case OPENED:
+                case OPENING:
+                    mState.set(State.CLOSING);
+                    cameraDevice.close();
+                    Camera.this.mCameraDevice = null;
+                    break;
+                case RELEASING:
+                    mState.set(State.RELEASED);
+                    cameraDevice.close();
+                    Camera.this.mCameraDevice = null;
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "onDisconnected() should not be possible from state: " + mState.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 (mState.get()) {
+                case CLOSING:
+                    mState.set(State.INITIALIZED);
+                    Camera.this.mCameraDevice = null;
+                    break;
+                case REOPENING:
+                case OPENED:
+                case OPENING:
+                    mState.set(State.CLOSING);
+                    cameraDevice.close();
+                    Camera.this.mCameraDevice = null;
+                    break;
+                case RELEASING:
+                    mState.set(State.RELEASED);
+                    cameraDevice.close();
+                    Camera.this.mCameraDevice = null;
+                    break;
+                default:
+                    throw new IllegalStateException(
+                            "onError() should not be possible from state: " + mState.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..64d1515
--- /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, context));
+        configFactory.installDefaultProvider(
+                ImageCaptureUseCaseConfiguration.class,
+                new DefaultImageCaptureConfigurationProvider(cameraFactory, context));
+        configFactory.installDefaultProvider(
+                VideoCaptureUseCaseConfiguration.class,
+                new DefaultVideoCaptureConfigurationProvider(cameraFactory, context));
+        configFactory.installDefaultProvider(
+                ViewFinderUseCaseConfiguration.class,
+                new DefaultViewFinderConfigurationProvider(cameraFactory, context));
+
+        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..1cd5df7
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraCaptureResult.java
@@ -0,0 +1,203 @@
+/*
+ * 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.annotation.Nullable;
+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 = "C2CameraCaptureResult";
+
+    private final Object mTag;
+
+    /** The actual camera2 {@link CaptureResult}. */
+    private final CaptureResult mCaptureResult;
+
+    Camera2CameraCaptureResult(@Nullable Object tag, CaptureResult captureResult) {
+        mTag = tag;
+        mCaptureResult = captureResult;
+    }
+
+    /**
+     * Converts the camera2 {@link CaptureResult#CONTROL_AF_MODE} to {@link AfMode}.
+     *
+     * @return the {@link AfMode}.
+     */
+    @NonNull
+    @Override
+    public AfMode getAfMode() {
+        Integer mode = mCaptureResult.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 = mCaptureResult.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 = mCaptureResult.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 = mCaptureResult.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 = mCaptureResult.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;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public long getTimestamp() {
+        Long timestamp = mCaptureResult.get(CaptureResult.SENSOR_TIMESTAMP);
+        if (timestamp == null) {
+            return -1L;
+        }
+
+        return timestamp;
+    }
+
+    @Override
+    public Object getTag() {
+        return mTag;
+    }
+}
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..21a12b2
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraControl.java
@@ -0,0 +1,582 @@
+/*
+ * 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
+ *
+ * @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 ControlUpdateListener mControlUpdateListener;
+    private final Handler mHandler;
+    final CameraControlSessionCallback mSessionCallback =
+            new CameraControlSessionCallback();
+    private final SessionConfiguration.Builder mSessionConfigurationBuilder =
+            new SessionConfiguration.Builder();
+    // use volatile modifier to make these variables in sync in all threads.
+    private volatile boolean mIsTorchOn = false;
+    private volatile boolean mIsFocusLocked = false;
+    private volatile FlashMode mFlashMode = FlashMode.OFF;
+    private volatile Rect mCropRect = null;
+    volatile MeteringRectangle mAfRect;
+    private volatile MeteringRectangle mAeRect;
+    private volatile MeteringRectangle mAwbRect;
+    volatile Integer mCurrentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
+    volatile OnFocusCompletedListener mFocusListener = null;
+    private volatile Handler mFocusListenerHandler = null;
+    volatile CaptureResultListener mSessionListenerForFocus = null;
+    private final Runnable mHandleFocusTimeoutRunnable =
+            new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.cancelFocus();
+
+                    mSessionCallback.removeListener(mSessionListenerForFocus);
+
+                    if (mFocusListener != null
+                            && mCurrentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
+                        Camera2CameraControl.this.runInFocusListenerHandler(
+                                new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        mFocusListener.onFocusTimedOut(mAfRect.getRect());
+                                    }
+                                });
+                    }
+                }
+            };
+
+    public Camera2CameraControl(ControlUpdateListener controlUpdateListener, Handler handler) {
+        mControlUpdateListener = controlUpdateListener;
+        mHandler = handler;
+
+        mSessionConfigurationBuilder.setTemplateType(getDefaultTemplate());
+        mSessionConfigurationBuilder.setCameraCaptureCallback(
+                CaptureCallbackContainer.create(mSessionCallback));
+        updateSessionConfiguration();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setCropRegion(final Rect crop) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.setCropRegion(crop);
+                }
+            });
+            return;
+        }
+
+        mCropRect = crop;
+        updateSessionConfiguration();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void focus(
+            final Rect focus,
+            final Rect metering,
+            @Nullable final OnFocusCompletedListener listener,
+            @Nullable final Handler listenerHandler) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.focus(focus, metering, listener, listenerHandler);
+                }
+            });
+            return;
+        }
+
+        mSessionCallback.removeListener(mSessionListenerForFocus);
+
+        mHandler.removeCallbacks(mHandleFocusTimeoutRunnable);
+
+        mAfRect = new MeteringRectangle(focus, MeteringRectangle.METERING_WEIGHT_MAX);
+        mAeRect = new MeteringRectangle(metering, MeteringRectangle.METERING_WEIGHT_MAX);
+        mAwbRect = new MeteringRectangle(metering, MeteringRectangle.METERING_WEIGHT_MAX);
+        Log.d(TAG, "Setting new AF rectangle: " + mAfRect);
+        Log.d(TAG, "Setting new AE rectangle: " + mAeRect);
+        Log.d(TAG, "Setting new AWB rectangle: " + mAwbRect);
+
+        mFocusListener = listener;
+        mFocusListenerHandler =
+                (listenerHandler != null ? listenerHandler : new Handler(Looper.getMainLooper()));
+        mCurrentAfState = CaptureResult.CONTROL_AF_STATE_INACTIVE;
+        mIsFocusLocked = true;
+
+        if (listener != null) {
+
+            mSessionListenerForFocus =
+                    new CaptureResultListener() {
+                        @Override
+                        public boolean onCaptureResult(TotalCaptureResult result) {
+                            Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
+                            if (afState == null) {
+                                return false;
+                            }
+
+                            if (mCurrentAfState == CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN) {
+                                if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED) {
+                                    Camera2CameraControl.this.runInFocusListenerHandler(
+                                            new Runnable() {
+                                                @Override
+                                                public void run() {
+                                                    mFocusListener.onFocusLocked(mAfRect.getRect());
+                                                }
+                                            });
+                                    return true; // finished
+                                } else if (afState
+                                        == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
+                                    Camera2CameraControl.this.runInFocusListenerHandler(
+                                            new Runnable() {
+                                                @Override
+                                                public void run() {
+                                                    mFocusListener.onFocusUnableToLock(
+                                                            mAfRect.getRect());
+                                                }
+                                            });
+                                    return true; // finished
+                                }
+                            }
+                            if (!mCurrentAfState.equals(afState)) {
+                                mCurrentAfState = afState;
+                            }
+                            return false; // continue checking
+                        }
+                    };
+
+            mSessionCallback.addListener(mSessionListenerForFocus);
+        }
+        updateSessionConfiguration();
+
+        triggerAf();
+        if (FOCUS_TIMEOUT != 0) {
+            mHandler.postDelayed(mHandleFocusTimeoutRunnable, FOCUS_TIMEOUT);
+        }
+    }
+
+    @Override
+    public void focus(Rect focus, Rect metering) {
+        focus(focus, metering, null, null);
+    }
+
+    void runInFocusListenerHandler(Runnable runnable) {
+        if (mFocusListenerHandler != null) {
+            mFocusListenerHandler.post(runnable);
+        }
+    }
+
+    /** Cancels the focus operation. */
+    @VisibleForTesting
+    void cancelFocus() {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.cancelFocus();
+                }
+            });
+            return;
+        }
+
+        mHandler.removeCallbacks(mHandleFocusTimeoutRunnable);
+
+        MeteringRectangle zeroRegion =
+                new MeteringRectangle(new Rect(), MeteringRectangle.METERING_WEIGHT_DONT_CARE);
+        mAfRect = zeroRegion;
+        mAeRect = zeroRegion;
+        mAwbRect = zeroRegion;
+
+        // Send a single request to cancel af process
+        CaptureRequestConfiguration.Builder singleRequestBuilder =
+                createCaptureRequestBuilderWithSharedOptions();
+        singleRequestBuilder.setTemplateType(getDefaultTemplate());
+        singleRequestBuilder.setUseRepeatingSurface(true);
+        Camera2Configuration.Builder configBuilder = new Camera2Configuration.Builder();
+        configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        singleRequestBuilder.addImplementationOptions(configBuilder.build());
+        notifySingleRequest(singleRequestBuilder.build());
+
+        mIsFocusLocked = false;
+        updateSessionConfiguration();
+    }
+
+    @Override
+    public FlashMode getFlashMode() {
+        return mFlashMode;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setFlashMode(FlashMode flashMode) {
+        // update mFlashMode immediately so that following getFlashMode() returns correct value.
+        mFlashMode = flashMode;
+
+        updateSessionConfiguration();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void enableTorch(boolean torch) {
+        // update isTorchOn immediately so that following isTorchOn() returns correct value.
+        mIsTorchOn = torch;
+        enableTorchInternal(torch);
+    }
+
+    void enableTorchInternal(final boolean torch) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.enableTorchInternal(torch);
+                }
+            });
+            return;
+        }
+
+        if (!torch) {
+            CaptureRequestConfiguration.Builder singleRequestBuilder =
+                    createCaptureRequestBuilderWithSharedOptions();
+            singleRequestBuilder.setTemplateType(getDefaultTemplate());
+            singleRequestBuilder.setUseRepeatingSurface(true);
+            Camera2Configuration.Builder configBuilder = new Camera2Configuration.Builder();
+            configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
+                    CaptureRequest.CONTROL_AE_MODE_ON);
+            singleRequestBuilder.addImplementationOptions(configBuilder.build());
+            notifySingleRequest(singleRequestBuilder.build());
+        }
+        updateSessionConfiguration();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isTorchOn() {
+        return mIsTorchOn;
+    }
+
+    @Override
+    public boolean isFocusLocked() {
+        return mIsFocusLocked;
+    }
+
+    /**
+     * Issues a {@link CaptureRequest#CONTROL_AF_TRIGGER_START} request to start auto focus scan.
+     */
+    @Override
+    public void triggerAf() {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.triggerAf();
+                }
+            });
+            return;
+        }
+
+        CaptureRequestConfiguration.Builder builder =
+                createCaptureRequestBuilderWithSharedOptions();
+        builder.setTemplateType(getDefaultTemplate());
+        builder.setUseRepeatingSurface(true);
+        Camera2Configuration.Builder configBuilder = new Camera2Configuration.Builder();
+        configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START);
+        builder.addImplementationOptions(configBuilder.build());
+        notifySingleRequest(builder.build());
+    }
+
+    /**
+     * Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
+     * exposure scan.
+     */
+    @Override
+    public void triggerAePrecapture() {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.triggerAePrecapture();
+                }
+            });
+            return;
+        }
+
+        CaptureRequestConfiguration.Builder builder =
+                createCaptureRequestBuilderWithSharedOptions();
+        builder.setTemplateType(getDefaultTemplate());
+        builder.setUseRepeatingSurface(true);
+        Camera2Configuration.Builder configBuilder = new Camera2Configuration.Builder();
+        configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
+        builder.addImplementationOptions(configBuilder.build());
+        notifySingleRequest(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(final boolean cancelAfTrigger,
+            final boolean cancelAePrecaptureTrigger) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.cancelAfAeTrigger(cancelAfTrigger,
+                            cancelAePrecaptureTrigger);
+                }
+            });
+            return;
+        }
+        CaptureRequestConfiguration.Builder builder =
+                createCaptureRequestBuilderWithSharedOptions();
+        builder.setUseRepeatingSurface(true);
+        builder.setTemplateType(getDefaultTemplate());
+
+        Camera2Configuration.Builder configBuilder = new Camera2Configuration.Builder();
+        if (cancelAfTrigger) {
+            configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AF_TRIGGER,
+                    CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
+        }
+        if (cancelAePrecaptureTrigger) {
+            configBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL);
+        }
+        builder.addImplementationOptions(configBuilder.build());
+        notifySingleRequest(builder.build());
+    }
+
+    private int getDefaultTemplate() {
+        return CameraDevice.TEMPLATE_PREVIEW;
+    }
+
+    void notifySingleRequest(
+            final CaptureRequestConfiguration captureRequestConfiguration) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.notifySingleRequest(captureRequestConfiguration);
+                }
+            });
+            return;
+        }
+        mControlUpdateListener.onCameraControlSingleRequest(captureRequestConfiguration);
+    }
+
+    void updateSessionConfiguration() {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.updateSessionConfiguration();
+                }
+            });
+            return;
+        }
+        mSessionConfigurationBuilder.setImplementationOptions(getSharedOptions());
+        mControlUpdateListener.onCameraControlUpdateSessionConfiguration(
+                mSessionConfigurationBuilder.build());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void submitSingleRequest(final CaptureRequestConfiguration captureRequestConfiguration) {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Camera2CameraControl.this.submitSingleRequest(captureRequestConfiguration);
+                }
+            });
+            return;
+        }
+
+        CaptureRequestConfiguration.Builder builder = CaptureRequestConfiguration.Builder.from(
+                captureRequestConfiguration);
+        // Always override options by shared options for the single request from outside.
+        builder.addImplementationOptions(getSharedOptions());
+        notifySingleRequest(builder.build());
+    }
+
+    /**
+     * Creates a CaptureRequestConfiguration.Builder contains shared options.
+     *
+     * @return a {@link CaptureRequestConfiguration.Builder} contains shared options.
+     */
+    private CaptureRequestConfiguration.Builder createCaptureRequestBuilderWithSharedOptions() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+        builder.addImplementationOptions(getSharedOptions());
+        return builder;
+    }
+
+    /**
+     * Gets shared options by current status.
+     *
+     * <p>The shared options are based on the current torch status, flash mode, focus area, crop
+     * area, etc... They should be appended to the repeat request and each single capture request.
+     */
+    Configuration getSharedOptions() {
+        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);
+
+        int aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
+        if (mIsTorchOn) {
+            aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
+            builder.setCaptureRequestOption(CaptureRequest.FLASH_MODE,
+                    CaptureRequest.FLASH_MODE_TORCH);
+        } else {
+            switch (mFlashMode) {
+                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;
+            }
+        }
+        builder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, aeMode);
+
+        builder.setCaptureRequestOption(
+                CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO);
+
+        if (mAfRect != null) {
+            builder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{mAfRect});
+        }
+        if (mAeRect != null) {
+            builder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[]{mAeRect});
+        }
+        if (mAwbRect != null) {
+            builder.setCaptureRequestOption(
+                    CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{mAwbRect});
+        }
+
+        if (mCropRect != null) {
+            builder.setCaptureRequestOption(CaptureRequest.SCALER_CROP_REGION, mCropRect);
+        }
+
+        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> mResultListeners = new HashSet<>();
+
+        public void addListener(CaptureResultListener listener) {
+            synchronized (mResultListeners) {
+                mResultListeners.add(listener);
+            }
+        }
+
+        public void removeListener(CaptureResultListener listener) {
+            if (listener == null) {
+                return;
+            }
+            synchronized (mResultListeners) {
+                mResultListeners.remove(listener);
+            }
+        }
+
+        @Override
+        public void onCaptureCompleted(
+                @NonNull CameraCaptureSession session,
+                @NonNull CaptureRequest request,
+                @NonNull TotalCaptureResult result) {
+            Set<CaptureResultListener> listeners;
+            synchronized (mResultListeners) {
+                if (mResultListeners.isEmpty()) {
+                    return;
+                }
+                listeners = new HashSet<>(mResultListeners);
+            }
+
+            Set<CaptureResultListener> removeSet = new HashSet<>();
+            for (CaptureResultListener listener : listeners) {
+                boolean isFinished = listener.onCaptureResult(result);
+                if (isFinished) {
+                    removeSet.add(listener);
+                }
+            }
+
+            if (!removeSet.isEmpty()) {
+                synchronized (mResultListeners) {
+                    mResultListeners.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..4379c69
--- /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 sHandlerThread = new HandlerThread(CameraXThreads.TAG);
+    private static final Handler sHandler;
+
+    static {
+        sHandlerThread.start();
+        sHandler = new Handler(sHandlerThread.getLooper());
+    }
+
+    private final CameraManager mCameraManager;
+
+    Camera2CameraFactory(Context context) {
+        mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+    }
+
+    @Override
+    public BaseCamera getCamera(String cameraId) {
+        return new Camera(mCameraManager, cameraId, sHandler);
+    }
+
+    @Override
+    public Set<String> getAvailableCameraIds() throws CameraInfoUnavailableException {
+        List<String> camerasList = null;
+        try {
+            camerasList = Arrays.asList(mCameraManager.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 = mCameraManager.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..c72afa4
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CameraInfo.java
@@ -0,0 +1,144 @@
+/*
+ * 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.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.util.Log;
+import android.view.Surface;
+
+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;
+import androidx.core.util.Preconditions;
+
+/** Implementation of the {@link CameraInfo} interface that exposes parameters through camera2. */
+final class Camera2CameraInfo implements CameraInfo {
+
+    private final CameraCharacteristics mCameraCharacteristics;
+    private static final String TAG = "Camera2CameraInfo";
+
+    Camera2CameraInfo(CameraManager cameraManager, String cameraId)
+            throws CameraInfoUnavailableException {
+        try {
+            mCameraCharacteristics = 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");
+        checkCharacteristicAvailable(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, "Supported hardware level");
+        logDeviceInfo();
+    }
+
+    @SuppressLint("RestrictedApi") // TODO(b/124323692): Remove after aosp/900913 is merged
+    @Nullable
+    @Override
+    public LensFacing getLensFacing() {
+        Integer lensFacing = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
+        Preconditions.checkNotNull(lensFacing);
+        switch (lensFacing) {
+            case CameraCharacteristics.LENS_FACING_FRONT:
+                return LensFacing.FRONT;
+            case CameraCharacteristics.LENS_FACING_BACK:
+                return LensFacing.BACK;
+            default:
+                return null;
+        }
+    }
+
+    @SuppressLint("RestrictedApi") // TODO(b/124323692): Remove after aosp/900913 is merged
+    @Override
+    public int getSensorRotationDegrees(@RotationValue int relativeRotation) {
+        Integer sensorOrientation =
+                mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+        Preconditions.checkNotNull(sensorOrientation);
+        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,
+                sensorOrientation,
+                isOppositeFacingScreen);
+    }
+
+    @SuppressLint("RestrictedApi") // TODO(b/124323692): Remove after aosp/900913 is merged
+    private int getSupportedHardwareLevel() {
+        Integer deviceLevel =
+                mCameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+        Preconditions.checkNotNull(deviceLevel);
+        return deviceLevel;
+    }
+
+    private void checkCharacteristicAvailable(CameraCharacteristics.Key<?> key, String readableName)
+            throws CameraInfoUnavailableException {
+        if (mCameraCharacteristics.get(key) == null) {
+            throw new CameraInfoUnavailableException(
+                    "Camera characteristics map is missing value for characteristic: "
+                            + readableName);
+        }
+    }
+
+    @Override
+    public int getSensorRotationDegrees() {
+        return getSensorRotationDegrees(Surface.ROTATION_0);
+    }
+
+    private void logDeviceInfo() {
+        // Extend by adding logging here as needed.
+        logDeviceLevel();
+    }
+
+    private void logDeviceLevel() {
+        String levelString;
+
+        int deviceLevel = getSupportedHardwareLevel();
+        switch (deviceLevel) {
+            case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:
+                levelString = "INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY";
+                break;
+            case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL:
+                levelString = "INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL";
+                break;
+            case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:
+                levelString = "INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED";
+                break;
+            case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL:
+                levelString = "INFO_SUPPORTED_HARDWARE_LEVEL_FULL";
+                break;
+            case CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3:
+                levelString = "INFO_SUPPORTED_HARDWARE_LEVEL_3";
+                break;
+            default:
+                levelString = "Unknown value: " + deviceLevel;
+                break;
+        }
+        Log.i(TAG, "Device Level: " + levelString);
+    }
+}
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..f480734
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2CaptureSessionCaptureCallbacks.java
@@ -0,0 +1,173 @@
+/*
+ * 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.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.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> mCallbacks = 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)) {
+                    mCallbacks.add(callback);
+                }
+            }
+        }
+
+        @RequiresApi(api = Build.VERSION_CODES.N)
+        @Override
+        public void onCaptureBufferLost(
+                CameraCaptureSession session, CaptureRequest request, Surface surface, long frame) {
+            for (CameraCaptureSession.CaptureCallback callback : mCallbacks) {
+                callback.onCaptureBufferLost(session, request, surface, frame);
+            }
+        }
+
+        @Override
+        public void onCaptureCompleted(
+                CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
+            for (CameraCaptureSession.CaptureCallback callback : mCallbacks) {
+                callback.onCaptureCompleted(session, request, result);
+            }
+        }
+
+        @Override
+        public void onCaptureFailed(
+                CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
+            for (CameraCaptureSession.CaptureCallback callback : mCallbacks) {
+                callback.onCaptureFailed(session, request, failure);
+            }
+        }
+
+        @Override
+        public void onCaptureProgressed(
+                CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
+            for (CameraCaptureSession.CaptureCallback callback : mCallbacks) {
+                callback.onCaptureProgressed(session, request, partialResult);
+            }
+        }
+
+        @Override
+        public void onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) {
+            for (CameraCaptureSession.CaptureCallback callback : mCallbacks) {
+                callback.onCaptureSequenceAborted(session, sequenceId);
+            }
+        }
+
+        @Override
+        public void onCaptureSequenceCompleted(
+                CameraCaptureSession session, int sequenceId, long frame) {
+            for (CameraCaptureSession.CaptureCallback callback : mCallbacks) {
+                callback.onCaptureSequenceCompleted(session, sequenceId, frame);
+            }
+        }
+
+        @Override
+        public void onCaptureStarted(
+                CameraCaptureSession session, CaptureRequest request, long timestamp, long frame) {
+            for (CameraCaptureSession.CaptureCallback callback : mCallbacks) {
+                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..5d7168c
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2Configuration.java
@@ -0,0 +1,398 @@
+/*
+ * 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 mConfig;
+
+    /**
+     * Creates a Camera2Configuration for reading Camera2 options from the given config.
+     *
+     * @param config The config that potentially contains Camera2 options.
+     */
+    public Camera2Configuration(Configuration config) {
+        mConfig = 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. */
+    Set<Option<?>> getCaptureRequestOptions() {
+        final Set<Option<?>> optionSet = new HashSet<>();
+        findOptions(
+                Camera2Configuration.CAPTURE_REQUEST_ID_STEM,
+                new OptionMatcher() {
+                    @Override
+                    public boolean onOptionMatched(Option<?> 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 mConfig;
+    }
+
+    /** Extends a {@link Configuration.Builder} to add Camera2 options. */
+    public static final class Extender {
+
+        Configuration.Builder<?, ?> mBaseBuilder;
+
+        /**
+         * 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) {
+            mBaseBuilder = 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);
+            mBaseBuilder.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) {
+            mBaseBuilder.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) {
+            mBaseBuilder.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) {
+            mBaseBuilder.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) {
+            mBaseBuilder.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
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Builder implements
+            Configuration.Builder<Camera2Configuration, Builder> {
+
+        private final MutableOptionsBundle mMutableOptionsBundle = MutableOptionsBundle.create();
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mMutableOptionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        /**
+         * Inserts new capture request option with specific {@link CaptureRequest.Key} setting.
+         */
+        public <ValueT> Builder setCaptureRequestOption(
+                CaptureRequest.Key<ValueT> key, ValueT value) {
+            Option<Object> opt = Camera2Configuration.createCaptureRequestOption(key);
+            insertOption(opt, value);
+            return this;
+        }
+
+        /** Inserts options from other {@link Configuration} object. */
+        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(mMutableOptionsBundle));
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+}
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..73833710
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2DeviceSurfaceManager.java
@@ -0,0 +1,265 @@
+/*
+ * 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.
+ */
+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> mCameraSupportedSurfaceCombinationMap =
+            new HashMap<>();
+    private boolean mIsInitialized = false;
+
+    Camera2DeviceSurfaceManager(Context context) {
+        init(context, new CamcorderProfileHelper() {
+            @Override
+            public boolean hasProfile(int cameraId, int quality) {
+                return CamcorderProfile.hasProfile(cameraId, quality);
+            }
+        });
+    }
+
+    @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 (!mIsInitialized) {
+            throw new IllegalStateException("Camera2DeviceSurfaceManager is not initialized.");
+        }
+
+        if (surfaceConfigurationList == null || surfaceConfigurationList.isEmpty()) {
+            return true;
+        }
+
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                mCameraSupportedSurfaceCombinationMap.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 (!mIsInitialized) {
+            throw new IllegalStateException("Camera2DeviceSurfaceManager is not initialized.");
+        }
+
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                mCameraSupportedSurfaceCombinationMap.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 =
+                mCameraSupportedSurfaceCombinationMap.get(cameraId);
+
+        if (supportedSurfaceCombination == null
+                || !supportedSurfaceCombination.checkSupported(surfaceConfigurations)) {
+            throw new IllegalArgumentException(
+                    "No supported surface combination is found for camera device - Id : "
+                            + cameraId + ".  May be attempting to bind too many use cases.");
+        }
+
+        return supportedSurfaceCombination.getSuggestedResolutions(originalUseCases, newUseCases);
+    }
+
+    private void init(Context context, CamcorderProfileHelper camcorderProfileHelper) {
+        if (!mIsInitialized) {
+            CameraManager cameraManager =
+                    (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+
+            try {
+                for (String cameraId : cameraManager.getCameraIdList()) {
+                    mCameraSupportedSurfaceCombinationMap.put(
+                            cameraId,
+                            new SupportedSurfaceCombination(
+                                    context, cameraId, camcorderProfileHelper));
+                }
+            } catch (CameraAccessException e) {
+                throw new IllegalArgumentException("Fail to get camera id list", e);
+            }
+
+            mIsInitialized = 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 (!mIsInitialized) {
+            throw new IllegalStateException("CameraDeviceSurfaceManager is not initialized.");
+        }
+
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                mCameraSupportedSurfaceCombinationMap.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 (!mIsInitialized) {
+            throw new IllegalStateException("CameraDeviceSurfaceManager is not initialized.");
+        }
+
+        // 1920x1080 is maximum preview size
+        Size previewSize = MAXIMUM_PREVIEW_SIZE;
+
+        if (!mCameraSupportedSurfaceCombinationMap.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) mCameraSupportedSurfaceCombinationMap.keySet().toArray()[0];
+            previewSize =
+                    mCameraSupportedSurfaceCombinationMap
+                            .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..33e77b7
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/Camera2OptionUnpacker.java
@@ -0,0 +1,120 @@
+/*
+ * 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, final 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
+        final 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,
+                new Configuration.OptionMatcher() {
+                    @Override
+                    public boolean onOptionMatched(Option<?> 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/CameraCaptureCallbackAdapter.java b/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.java
new file mode 100644
index 0000000..5891989
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CameraCaptureCallbackAdapter.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.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 mCameraCaptureCallback;
+
+    CameraCaptureCallbackAdapter(CameraCaptureCallback cameraCaptureCallback) {
+        if (cameraCaptureCallback == null) {
+            throw new NullPointerException("cameraCaptureCallback is null");
+        }
+        mCameraCaptureCallback = cameraCaptureCallback;
+    }
+
+    @Override
+    public void onCaptureCompleted(
+            @NonNull CameraCaptureSession session,
+            @NonNull CaptureRequest request,
+            @NonNull TotalCaptureResult result) {
+        super.onCaptureCompleted(session, request, result);
+
+        mCameraCaptureCallback.onCaptureCompleted(
+                new Camera2CameraCaptureResult(request.getTag(), 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);
+
+        mCameraCaptureCallback.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..5ee1c0c
--- /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 mCaptureCallback;
+
+    private CaptureCallbackContainer(CaptureCallback captureCallback) {
+        if (captureCallback == null) {
+            throw new NullPointerException("captureCallback is null");
+        }
+        mCaptureCallback = captureCallback;
+    }
+
+    static CaptureCallbackContainer create(CaptureCallback captureCallback) {
+        return new CaptureCallbackContainer(captureCallback);
+    }
+
+    @NonNull
+    CaptureCallback getCaptureCallback() {
+        return mCaptureCallback;
+    }
+}
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..1522c23
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/CaptureSession.java
@@ -0,0 +1,563 @@
+/*
+ * 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.DeferrableSurface;
+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 mHandler;
+    /** The configuration for the currently issued single capture requests. */
+    private final List<CaptureRequestConfiguration> mCaptureRequestConfigurations =
+            new ArrayList<>();
+    /** Lock on whether the camera is open or closed. */
+    final Object mStateLock = new Object();
+    /** Callback for handling image captures. */
+    private final CameraCaptureSession.CaptureCallback mCaptureCallback =
+            new CaptureCallback() {
+                @Override
+                public void onCaptureCompleted(
+                        CameraCaptureSession session,
+                        CaptureRequest request,
+                        TotalCaptureResult result) {
+                }
+            };
+    private final StateCallback mCaptureSessionStateCallback = new StateCallback();
+    /** The framework camera capture session held by this session. */
+    @Nullable
+    CameraCaptureSession mCameraCaptureSession;
+    /** The configuration for the currently issued capture requests. */
+    @Nullable
+    private volatile SessionConfiguration mSessionConfiguration;
+    /** The list of surfaces used to configure the current capture session. */
+    private List<Surface> mConfiguredSurfaces = Collections.emptyList();
+    /** The list of DeferrableSurface used to notify surface detach events */
+    @GuardedBy("mConfiguredDeferrableSurfaces")
+    private List<DeferrableSurface> mConfiguredDeferrableSurfaces = Collections.emptyList();
+    /** Tracks the current state of the session. */
+    @GuardedBy("mStateLock")
+    State mState = 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.mHandler = handler;
+        mState = State.INITIALIZED;
+    }
+
+    /**
+     * Returns the configurations of the capture session, or null if it has not yet been set
+     * or if the capture session has been closed.
+     */
+    @Nullable
+    SessionConfiguration getSessionConfiguration() {
+        synchronized (mStateLock) {
+            return mSessionConfiguration;
+        }
+    }
+
+    /**
+     * 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 (mStateLock) {
+            switch (mState) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "setSessionConfiguration() should not be possible in state: " + mState);
+                case INITIALIZED:
+                case OPENING:
+                    this.mSessionConfiguration = sessionConfiguration;
+                    break;
+                case OPENED:
+                    this.mSessionConfiguration = sessionConfiguration;
+
+                    if (!mConfiguredSurfaces.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 (mStateLock) {
+            switch (mState) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "open() should not be possible in state: " + mState);
+                case INITIALIZED:
+                    mConfiguredDeferrableSurfaces = new ArrayList<>(
+                            sessionConfiguration.getSurfaces());
+                    mConfiguredSurfaces =
+                            new ArrayList<>(
+                                    DeferrableSurfaces.surfaceSet(
+                                            mConfiguredDeferrableSurfaces));
+                    if (mConfiguredSurfaces.isEmpty()) {
+                        Log.e(TAG, "Unable to open capture session with no surfaces. ");
+                        return;
+                    }
+
+                    notifySurfaceAttached();
+                    mState = State.OPENING;
+                    Log.d(TAG, "Opening capture session.");
+                    CameraCaptureSession.StateCallback comboCallback =
+                            CameraCaptureSessionStateCallbacks.createComboCallback(
+                                    mCaptureSessionStateCallback,
+                                    sessionConfiguration.getSessionStateCallback());
+                    cameraDevice.createCaptureSession(mConfiguredSurfaces, comboCallback, mHandler);
+                    break;
+                default:
+                    Log.e(TAG, "Open not allowed in state: " + mState);
+            }
+        }
+    }
+
+    /**
+     * 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 (mStateLock) {
+            switch (mState) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "close() should not be possible in state: " + mState);
+                case INITIALIZED:
+                    mState = State.RELEASED;
+                    break;
+                case OPENING:
+                case OPENED:
+                    mState = State.CLOSED;
+                    mSessionConfiguration = null;
+                    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 (mStateLock) {
+            switch (mState) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "release() should not be possible in state: " + mState);
+                case INITIALIZED:
+                    mState = State.RELEASED;
+                    break;
+                case OPENING:
+                    mState = State.RELEASING;
+                    break;
+                case OPENED:
+                case CLOSED:
+                    mCameraCaptureSession.close();
+                    mState = State.RELEASING;
+                    break;
+                case RELEASING:
+                case RELEASED:
+            }
+        }
+    }
+
+    // Notify the surface is attached to a new capture session.
+    void notifySurfaceAttached() {
+        synchronized (mConfiguredDeferrableSurfaces) {
+            for (DeferrableSurface deferrableSurface : mConfiguredDeferrableSurfaces) {
+                deferrableSurface.notifySurfaceAttached();
+            }
+        }
+    }
+
+    // Notify the surface is detached from current capture session.
+    void notifySurfaceDetached() {
+        synchronized (mConfiguredDeferrableSurfaces) {
+            for (DeferrableSurface deferredSurface : mConfiguredDeferrableSurfaces) {
+                deferredSurface.notifySurfaceDetached();
+            }
+            // Clears the mConfiguredDeferrableSurfaces to prevent from duplicate
+            // notifySurfaceDetached calls.
+            mConfiguredDeferrableSurfaces.clear();
+        }
+    }
+
+    /**
+     * 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 (mStateLock) {
+            switch (mState) {
+                case UNINITIALIZED:
+                    throw new IllegalStateException(
+                            "issueSingleCaptureRequests() should not be possible in state: "
+                                    + mState);
+                case INITIALIZED:
+                case OPENING:
+                    Log.d(TAG, "issueSingleCaptureRequests() before capture session opened.");
+                    this.mCaptureRequestConfigurations.addAll(captureRequestConfigurations);
+                    break;
+                case OPENED:
+                    this.mCaptureRequestConfigurations.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 (mStateLock) {
+            return Collections.unmodifiableList(mCaptureRequestConfigurations);
+        }
+    }
+
+    /** Returns the current state of the session. */
+    State getState() {
+        synchronized (mStateLock) {
+            return mState;
+        }
+    }
+
+    /**
+     * 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() {
+        if (mSessionConfiguration == null) {
+            Log.d(TAG, "Skipping issueRepeatingCaptureRequests for no configuration case.");
+            return;
+        }
+
+        CaptureRequestConfiguration captureRequestConfiguration =
+                mSessionConfiguration.getCaptureRequestConfiguration();
+
+        try {
+            Log.d(TAG, "Issuing request for session.");
+            CaptureRequest.Builder builder =
+                    captureRequestConfiguration.buildCaptureRequest(
+                            mCameraCaptureSession.getDevice());
+            if (builder == null) {
+                Log.d(TAG, "Skipping issuing empty request for session.");
+                return;
+            }
+
+            applyImplementationOptionTCaptureBuilder(
+                    builder, captureRequestConfiguration.getImplementationOptions());
+
+            CameraCaptureSession.CaptureCallback comboCaptureCallback =
+                    Camera2CaptureSessionCaptureCallbacks.createComboCallback(
+                            mCaptureCallback,
+                            CaptureCallbackConverter.toCaptureCallback(
+                                    captureRequestConfiguration.getCameraCaptureCallback()));
+            mCameraCaptureSession.setRepeatingRequest(
+                    builder.build(), comboCaptureCallback, mHandler);
+        } 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 mCaptureRequestConfigurations to {@link CameraCaptureSession}. */
+    void issueCaptureRequests() {
+        if (mCaptureRequestConfigurations.isEmpty()) {
+            return;
+        }
+
+        for (CaptureRequestConfiguration captureRequestConfiguration :
+                mCaptureRequestConfigurations) {
+            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(
+                                mCameraCaptureSession.getDevice());
+
+                applyImplementationOptionTCaptureBuilder(
+                        builder, captureRequestConfiguration.getImplementationOptions());
+
+                mCameraCaptureSession.capture(
+                        builder.build(),
+                        CaptureCallbackConverter.toCaptureCallback(
+                                captureRequestConfiguration.getCameraCaptureCallback()),
+                        mHandler);
+            } catch (CameraAccessException e) {
+                Log.e(TAG, "Unable to access camera: " + e.getMessage());
+                Thread.dumpStack();
+            }
+        }
+        mCaptureRequestConfigurations.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 (mStateLock) {
+                switch (mState) {
+                    case UNINITIALIZED:
+                    case INITIALIZED:
+                    case OPENED:
+                    case RELEASED:
+                        throw new IllegalStateException(
+                                "onConfigured() should not be possible in state: " + mState);
+                    case OPENING:
+                        mState = State.OPENED;
+                        mCameraCaptureSession = session;
+                        Log.d(TAG, "Attempting to send capture request onConfigured");
+                        issueRepeatingCaptureRequests();
+                        issueCaptureRequests();
+                        break;
+                    case CLOSED:
+                        mCameraCaptureSession = session;
+                        break;
+                    case RELEASING:
+                        session.close();
+                        break;
+                }
+                Log.d(TAG, "CameraCaptureSession.onConfigured()");
+            }
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            synchronized (mStateLock) {
+                switch (mState) {
+                    case UNINITIALIZED:
+                        throw new IllegalStateException(
+                                "onReady() should not be possible in state: " + mState);
+                    default:
+                }
+                Log.d(TAG, "CameraCaptureSession.onReady()");
+            }
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            synchronized (mStateLock) {
+                switch (mState) {
+                    case UNINITIALIZED:
+                        throw new IllegalStateException(
+                                "onClosed() should not be possible in state: " + mState);
+                    default:
+                        mState = State.RELEASED;
+                        mCameraCaptureSession = null;
+                }
+                Log.d(TAG, "CameraCaptureSession.onClosed()");
+
+                notifySurfaceDetached();
+
+            }
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            synchronized (mStateLock) {
+                switch (mState) {
+                    case UNINITIALIZED:
+                    case INITIALIZED:
+                    case OPENED:
+                    case RELEASED:
+                        throw new IllegalStateException(
+                                "onConfiguredFailed() should not be possible in state: " + mState);
+                    case OPENING:
+                    case CLOSED:
+                        mState = State.CLOSED;
+                        mCameraCaptureSession = session;
+                        break;
+                    case RELEASING:
+                        mState = State.RELEASING;
+                        session.close();
+                }
+                Log.e(TAG, "CameraCaptureSession.onConfiguredFailed()");
+            }
+        }
+    }
+
+    /** Also notify the surface detach event if receives camera device close event */
+    public void notifyCameraDeviceClose() {
+        notifySurfaceDetached();
+    }
+}
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..450ac9f
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageAnalysisConfigurationProvider.java
@@ -0,0 +1,101 @@
+/*
+ * 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.CameraDevice;
+import android.util.Log;
+import android.util.Rational;
+import android.view.WindowManager;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+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;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Provides defaults for {@link ImageAnalysisUseCaseConfiguration} in the Camera2 implementation.
+ */
+final class DefaultImageAnalysisConfigurationProvider
+        implements ConfigurationProvider<ImageAnalysisUseCaseConfiguration> {
+    private static final String TAG = "DefImgAnalysisProvider";
+    private static final Rational DEFAULT_ASPECT_RATIO_4_3 = new Rational(4, 3);
+    private static final Rational DEFAULT_ASPECT_RATIO_3_4 = new Rational(3, 4);
+
+    private final CameraFactory mCameraFactory;
+    private final WindowManager mWindowManager;
+
+    DefaultImageAnalysisConfigurationProvider(CameraFactory cameraFactory, Context context) {
+        mCameraFactory = cameraFactory;
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    }
+
+    @Override
+    public ImageAnalysisUseCaseConfiguration getConfiguration(LensFacing lensFacing) {
+        ImageAnalysisUseCaseConfiguration.Builder builder =
+                ImageAnalysisUseCaseConfiguration.Builder.fromConfig(
+                        ImageAnalysisUseCase.DEFAULT_CONFIG.getConfiguration(lensFacing));
+
+        // 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);
+
+        List<LensFacing> lensFacingList;
+
+        // Add default lensFacing if we can
+        if (lensFacing == LensFacing.FRONT) {
+            lensFacingList = Arrays.asList(LensFacing.FRONT, LensFacing.BACK);
+        } else {
+            lensFacingList = Arrays.asList(LensFacing.BACK, LensFacing.FRONT);
+        }
+
+        try {
+            String defaultId = null;
+
+            for (LensFacing lensFacingCandidate : lensFacingList) {
+                defaultId = mCameraFactory.cameraIdForLensFacing(lensFacingCandidate);
+                if (defaultId != null) {
+                    builder.setLensFacing(lensFacingCandidate);
+                    break;
+                }
+            }
+
+            int targetRotation = mWindowManager.getDefaultDisplay().getRotation();
+            int rotationDegrees = CameraX.getCameraInfo(defaultId).getSensorRotationDegrees(
+                    targetRotation);
+            boolean isRotateNeeded = (rotationDegrees == 90 || rotationDegrees == 270);
+            builder.setTargetRotation(targetRotation);
+            builder.setTargetAspectRatio(
+                    isRotateNeeded ? DEFAULT_ASPECT_RATIO_3_4 : DEFAULT_ASPECT_RATIO_4_3);
+        } 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..19e4e86
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultImageCaptureConfigurationProvider.java
@@ -0,0 +1,99 @@
+/*
+ * 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.CameraDevice;
+import android.util.Log;
+import android.util.Rational;
+import android.view.WindowManager;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+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;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Provides defaults for {@link ImageCaptureUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultImageCaptureConfigurationProvider
+        implements ConfigurationProvider<ImageCaptureUseCaseConfiguration> {
+    private static final String TAG = "DefImgCapProvider";
+    private static final Rational DEFAULT_ASPECT_RATIO_4_3 = new Rational(4, 3);
+    private static final Rational DEFAULT_ASPECT_RATIO_3_4 = new Rational(3, 4);
+
+    private final CameraFactory mCameraFactory;
+    private final WindowManager mWindowManager;
+
+    DefaultImageCaptureConfigurationProvider(CameraFactory cameraFactory, Context context) {
+        mCameraFactory = cameraFactory;
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    }
+
+    @Override
+    public ImageCaptureUseCaseConfiguration getConfiguration(LensFacing lensFacing) {
+        ImageCaptureUseCaseConfiguration.Builder builder =
+                ImageCaptureUseCaseConfiguration.Builder.fromConfig(
+                        ImageCaptureUseCase.DEFAULT_CONFIG.getConfiguration(lensFacing));
+
+        // 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);
+
+        List<LensFacing> lensFacingList;
+
+        // Add default lensFacing if we can
+        if (lensFacing == LensFacing.FRONT) {
+            lensFacingList = Arrays.asList(LensFacing.FRONT, LensFacing.BACK);
+        } else {
+            lensFacingList = Arrays.asList(LensFacing.BACK, LensFacing.FRONT);
+        }
+
+        try {
+            String defaultId = null;
+
+            for (LensFacing lensFacingCandidate : lensFacingList) {
+                defaultId = mCameraFactory.cameraIdForLensFacing(lensFacingCandidate);
+                if (defaultId != null) {
+                    builder.setLensFacing(lensFacingCandidate);
+                    break;
+                }
+            }
+
+            int targetRotation = mWindowManager.getDefaultDisplay().getRotation();
+            int rotationDegrees = CameraX.getCameraInfo(defaultId).getSensorRotationDegrees(
+                    targetRotation);
+            boolean isRotateNeeded = (rotationDegrees == 90 || rotationDegrees == 270);
+            builder.setTargetRotation(targetRotation);
+            builder.setTargetAspectRatio(
+                    isRotateNeeded ? DEFAULT_ASPECT_RATIO_3_4 : DEFAULT_ASPECT_RATIO_4_3);
+        } 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..7603637
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultVideoCaptureConfigurationProvider.java
@@ -0,0 +1,99 @@
+/*
+ * 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.CameraDevice;
+import android.util.Log;
+import android.util.Rational;
+import android.view.WindowManager;
+
+import androidx.camera.core.CameraFactory;
+import androidx.camera.core.CameraX;
+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;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Provides defaults for {@link VideoCaptureUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultVideoCaptureConfigurationProvider
+        implements ConfigurationProvider<VideoCaptureUseCaseConfiguration> {
+    private static final String TAG = "DefVideoCapProvider";
+    private static final Rational DEFAULT_ASPECT_RATIO_16_9 = new Rational(16, 9);
+    private static final Rational DEFAULT_ASPECT_RATIO_9_16 = new Rational(9, 16);
+
+    private final CameraFactory mCameraFactory;
+    private final WindowManager mWindowManager;
+
+    DefaultVideoCaptureConfigurationProvider(CameraFactory cameraFactory, Context context) {
+        mCameraFactory = cameraFactory;
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    }
+
+    @Override
+    public VideoCaptureUseCaseConfiguration getConfiguration(LensFacing lensFacing) {
+        VideoCaptureUseCaseConfiguration.Builder builder =
+                VideoCaptureUseCaseConfiguration.Builder.fromConfig(
+                        VideoCaptureUseCase.DEFAULT_CONFIG.getConfiguration(lensFacing));
+
+        // 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);
+
+        List<LensFacing> lensFacingList;
+
+        // Add default lensFacing if we can
+        if (lensFacing == LensFacing.FRONT) {
+            lensFacingList = Arrays.asList(LensFacing.FRONT, LensFacing.BACK);
+        } else {
+            lensFacingList = Arrays.asList(LensFacing.BACK, LensFacing.FRONT);
+        }
+
+        try {
+            String defaultId = null;
+
+            for (LensFacing lensFacingCandidate : lensFacingList) {
+                defaultId = mCameraFactory.cameraIdForLensFacing(lensFacingCandidate);
+                if (defaultId != null) {
+                    builder.setLensFacing(lensFacingCandidate);
+                    break;
+                }
+            }
+
+            int targetRotation = mWindowManager.getDefaultDisplay().getRotation();
+            int rotationDegrees = CameraX.getCameraInfo(defaultId).getSensorRotationDegrees(
+                    targetRotation);
+            boolean isRotateNeeded = (rotationDegrees == 90 || rotationDegrees == 270);
+            builder.setTargetRotation(targetRotation);
+            builder.setTargetAspectRatio(
+                    isRotateNeeded ? DEFAULT_ASPECT_RATIO_9_16 : DEFAULT_ASPECT_RATIO_16_9);
+        } 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..6c8281c
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/DefaultViewFinderConfigurationProvider.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.camera2;
+
+import android.content.Context;
+import android.hardware.camera2.CameraDevice;
+import android.util.Log;
+import android.view.WindowManager;
+
+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;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Provides defaults for {@link ViewFinderUseCaseConfiguration} in the Camera2 implementation. */
+final class DefaultViewFinderConfigurationProvider
+        implements ConfigurationProvider<ViewFinderUseCaseConfiguration> {
+    private static final String TAG = "DefViewFinderProvider";
+
+    private final CameraFactory mCameraFactory;
+    private final WindowManager mWindowManager;
+
+    DefaultViewFinderConfigurationProvider(CameraFactory cameraFactory, Context context) {
+        mCameraFactory = cameraFactory;
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    }
+
+    @Override
+    public ViewFinderUseCaseConfiguration getConfiguration(LensFacing lensFacing) {
+        ViewFinderUseCaseConfiguration.Builder builder =
+                ViewFinderUseCaseConfiguration.Builder.fromConfig(
+                        ViewFinderUseCase.DEFAULT_CONFIG.getConfiguration(lensFacing));
+
+        // 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);
+
+        List<LensFacing> lensFacingList;
+
+        // Add default lensFacing if we can
+        if (lensFacing == LensFacing.FRONT) {
+            lensFacingList = Arrays.asList(LensFacing.FRONT, LensFacing.BACK);
+        } else {
+            lensFacingList = Arrays.asList(LensFacing.BACK, LensFacing.FRONT);
+        }
+
+        try {
+            String defaultId = null;
+
+            for (LensFacing lensFacingCandidate : lensFacingList) {
+                defaultId = mCameraFactory.cameraIdForLensFacing(lensFacingCandidate);
+                if (defaultId != null) {
+                    builder.setLensFacing(lensFacingCandidate);
+                    break;
+                }
+            }
+
+            int targetRotation = mWindowManager.getDefaultDisplay().getRotation();
+            builder.setTargetRotation(targetRotation);
+        } 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..85286b7
--- /dev/null
+++ b/camera/camera2/src/main/java/androidx/camera/camera2/SupportedSurfaceCombination.java
@@ -0,0 +1,1005 @@
+/*
+ * 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.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.Surface;
+import android.view.WindowManager;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraDeviceConfiguration;
+import androidx.camera.core.CameraInfoUnavailableException;
+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> mSurfaceCombinations = new ArrayList<>();
+    private String mCameraId;
+    private CameraCharacteristics mCharacteristics;
+    private int mHardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+    private boolean mIsRawSupported = false;
+    private boolean mIsBurstCaptureSupported = false;
+    private SurfaceSizeDefinition mSurfaceSizeDefinition;
+    private CamcorderProfileHelper mCamcorderProfileHelper;
+
+    SupportedSurfaceCombination(
+            Context context, String cameraId, CamcorderProfileHelper camcorderProfileHelper) {
+        mCameraId = cameraId;
+        mCamcorderProfileHelper = camcorderProfileHelper;
+        init(context);
+    }
+
+    private SupportedSurfaceCombination() {
+    }
+
+    String getCameraId() {
+        return mCameraId;
+    }
+
+    boolean isRawSupported() {
+        return mIsRawSupported;
+    }
+
+    boolean isBurstCaptureSupported() {
+        return mIsBurstCaptureSupported;
+    }
+
+    /**
+     * 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 : mSurfaceCombinations) {
+            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 = mSurfaceSizeDefinition.getMaximumSizeMap().get(imageFormat);
+
+        // Compare with surface size definition to determine the surface configuration size
+        if (size.getWidth() * size.getHeight()
+                <= mSurfaceSizeDefinition.getAnalysisSize().getWidth()
+                * mSurfaceSizeDefinition.getAnalysisSize().getHeight()) {
+            configurationSize = ConfigurationSize.ANALYSIS;
+        } else if (size.getWidth() * size.getHeight()
+                <= mSurfaceSizeDefinition.getPreviewSize().getWidth()
+                * mSurfaceSizeDefinition.getPreviewSize().getHeight()) {
+            configurationSize = ConfigurationSize.PREVIEW;
+        } else if (size.getWidth() * size.getHeight()
+                <= mSurfaceSizeDefinition.getRecordSize().getWidth()
+                * mSurfaceSizeDefinition.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 mSurfaceSizeDefinition;
+    }
+
+    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.
+        Arrays.sort(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<>();
+
+        // Get target rotation value to calibrate the target resolution and aspect ratio
+        int sensorRotationDegrees = 0;
+
+        try {
+            int targetRotation = configuration.getTargetRotation(Surface.ROTATION_0);
+            sensorRotationDegrees = CameraX.getCameraInfo(
+                    mCameraId).getSensorRotationDegrees(targetRotation);
+        } catch (CameraInfoUnavailableException e) {
+            throw new IllegalArgumentException("Unable to retrieve camera sensor orientation.", e);
+        }
+
+        Rational aspectRatio = configuration.getTargetAspectRatio(null);
+
+        // Calibrates the target aspect ratio with the display and sensor rotation degrees values
+        // . Otherwise, retrieves default aspect ratio for the target use case.
+        if (aspectRatio != null && (sensorRotationDegrees == 90 || sensorRotationDegrees == 270)) {
+            aspectRatio = new Rational(aspectRatio.getDenominator(),
+                    aspectRatio.getNumerator());
+        }
+
+        for (Size outputSize : outputSizeCandidates) {
+            // If target aspect ratio is set, moves the matched results to the front of the list.
+            if (aspectRatio != null && aspectRatio.equals(
+                    new Rational(outputSize.getWidth(), outputSize.getHeight()))) {
+                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;
+    }
+
+    private Size[] getAllOutputSizesByFormat(int imageFormat) {
+        if (mCharacteristics == null) {
+            throw new IllegalStateException("CameraCharacteristics is null.");
+        }
+
+        StreamConfigurationMap map =
+                mCharacteristics.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.
+        Arrays.sort(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:"
+                            + mCameraId,
+                    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 {
+        mCharacteristics = cameraManager.getCameraCharacteristics(mCameraId);
+
+        Integer keyValue = mCharacteristics.get(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+
+        if (keyValue != null) {
+            mHardwareLevel = keyValue;
+        }
+
+        mSurfaceCombinations.addAll(getLegacySupportedCombinationList());
+
+        if (mHardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+                || mHardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+                || mHardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+            mSurfaceCombinations.addAll(getLimitedSupportedCombinationList());
+        }
+
+        if (mHardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+                || mHardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+            mSurfaceCombinations.addAll(getFullSupportedCombinationList());
+        }
+
+        int[] availableCapabilities =
+                mCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+
+        if (availableCapabilities != null) {
+            for (int capability : availableCapabilities) {
+                if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) {
+                    mIsRawSupported = true;
+                } else if (capability
+                        == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE) {
+                    mIsBurstCaptureSupported = true;
+                }
+            }
+        }
+
+        if (mIsRawSupported) {
+            mSurfaceCombinations.addAll(getRAWSupportedCombinationList());
+        }
+
+        if (mIsBurstCaptureSupported
+                && mHardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED) {
+            mSurfaceCombinations.addAll(getBurstSupportedCombinationList());
+        }
+
+        if (mHardwareLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3) {
+            mSurfaceCombinations.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));
+
+        mSurfaceSizeDefinition =
+                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);
+
+        Size displayViewSize;
+        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 (mCamcorderProfileHelper.hasProfile(
+                Integer.parseInt(mCameraId), CamcorderProfile.QUALITY_2160P)) {
+            recordSize = QUALITY_2160P_SIZE;
+        } else if (mCamcorderProfileHelper.hasProfile(
+                Integer.parseInt(mCameraId), CamcorderProfile.QUALITY_1080P)) {
+            recordSize = QUALITY_1080P_SIZE;
+        } else if (mCamcorderProfileHelper.hasProfile(
+                Integer.parseInt(mCameraId), CamcorderProfile.QUALITY_720P)) {
+            recordSize = QUALITY_720P_SIZE;
+        } else if (mCamcorderProfileHelper.hasProfile(
+                Integer.parseInt(mCameraId), 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 mReverse = false;
+
+        CompareSizesByArea() {
+        }
+
+        CompareSizesByArea(boolean reverse) {
+            mReverse = 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 (mReverse) {
+                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/Camera2CameraInfoTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoTest.java
new file mode 100644
index 0000000..b45cc12
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2CameraInfoTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Build;
+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 androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class Camera2CameraInfoTest {
+
+    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 static final int FAKE_SUPPORTED_HARDWARE_LEVEL =
+            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3;
+
+    private CameraManager mCameraManager;
+
+    @Before
+    public void setUp() {
+        initCameras();
+        mCameraManager =
+                (CameraManager) ApplicationProvider.getApplicationContext().getSystemService(
+                        Context.CAMERA_SERVICE);
+    }
+
+    @Test
+    public void canCreateCameraInfo() throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(mCameraManager, CAMERA0_ID);
+        assertThat(cameraInfo).isNotNull();
+    }
+
+    @Test
+    public void cameraInfo_canReturnSensorOrientation() throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(mCameraManager, CAMERA0_ID);
+        assertThat(cameraInfo.getSensorRotationDegrees()).isEqualTo(CAMERA0_SENSOR_ORIENTATION);
+    }
+
+    @Test
+    public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera()
+            throws CameraInfoUnavailableException {
+        CameraInfo cameraInfo = new Camera2CameraInfo(mCameraManager, 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(mCameraManager, 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(mCameraManager, 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);
+
+        shadowCharacteristics0.set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL,
+                FAKE_SUPPORTED_HARDWARE_LEVEL);
+
+        // 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(Context.CAMERA_SERVICE)))
+                .addCamera(CAMERA0_ID, characteristics0);
+
+        // **** Camera 1 characteristics ****//
+        CameraCharacteristics characteristics1 =
+                ShadowCameraCharacteristics.newCameraCharacteristics();
+
+        ShadowCameraCharacteristics shadowCharacteristics1 = Shadow.extract(characteristics1);
+
+        shadowCharacteristics1.set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL,
+                FAKE_SUPPORTED_HARDWARE_LEVEL);
+
+        // 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(Context.CAMERA_SERVICE)))
+                .addCamera(CAMERA1_ID, characteristics1);
+    }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerTest.java
new file mode 100644
index 0000000..e76d6a2
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/Camera2DeviceSurfaceManagerTest.java
@@ -0,0 +1,579 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
+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.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 androidx.camera.testing.StreamConfigurationMapUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+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 */
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public final class Camera2DeviceSurfaceManagerTest {
+    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 static final int DEFAULT_SENSOR_ORIENTATION = 90;
+    private final Size mDisplaySize = new Size(1280, 720);
+    private final Size mAnalysisSize = new Size(640, 480);
+    private final Size mPreviewSize = mDisplaySize;
+    private final Size mRecordSize = new Size(3840, 2160);
+    private final Size mMaximumSize = new Size(4032, 3024);
+    private final Size mMaximumVideoSize = new Size(1920, 1080);
+    private final CamcorderProfileHelper mMockCamcorderProfileHelper =
+            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[] mSupportedFormats =
+            new int[]{
+                    ImageFormat.YUV_420_888,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+            };
+
+    private final Size[] mSupportedSizes =
+            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 mContext = ApplicationProvider.getApplicationContext();
+    private CameraDeviceSurfaceManager mSurfaceManager;
+
+    @Before
+    public void setUp() {
+        WindowManager windowManager =
+                (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealWidth(mDisplaySize.getWidth());
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealHeight(mDisplaySize.getHeight());
+
+        when(mMockCamcorderProfileHelper.hasProfile(anyInt(), anyInt())).thenReturn(true);
+
+        setupCamera();
+    }
+
+    @Test
+    public void checkLegacySurfaceCombinationSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            LEGACY_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            LIMITED_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, FULL_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            FULL_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertTrue(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, FULL_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.checkSupported(
+                            FULL_CAMERA_ID, combination.getSurfaceConfigurationList());
+            assertFalse(isSupported);
+        }
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationSupportedInLevel3Device() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEVEL3_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        for (SurfaceCombination combination : combinationList) {
+            boolean isSupported =
+                    mSurfaceManager.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
+            mSurfaceManager.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 =
+                mSurfaceManager.getSuggestedResolutions(LIMITED_CAMERA_ID, null, useCases);
+
+        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
+        assertThat(suggestedResolutionMap).containsEntry(imageCaptureUseCase, mRecordSize);
+        assertThat(suggestedResolutionMap).containsEntry(videoCaptureUseCase, mMaximumVideoSize);
+        assertThat(suggestedResolutionMap).containsEntry(viewFinderUseCase, mPreviewSize);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVAnalysisSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, mAnalysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVPreviewSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, mPreviewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVRecordSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, mRecordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVMaximumSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.YUV_420_888, mMaximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVNotSupportSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID,
+                        ImageFormat.YUV_420_888,
+                        new Size(mMaximumSize.getWidth() + 1, mMaximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGAnalysisSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, mAnalysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGPreviewSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, mPreviewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGRecordSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, mRecordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGMaximumSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID, ImageFormat.JPEG, mMaximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGNotSupportSize() {
+        SurfaceConfiguration surfaceConfiguration =
+                mSurfaceManager.transformSurfaceConfiguration(
+                        LEGACY_CAMERA_ID,
+                        ImageFormat.JPEG,
+                        new Size(mMaximumSize.getWidth() + 1, mMaximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void getMaximumSizeForImageFormat() {
+        Size maximumYUVSize =
+                mSurfaceManager.getMaxOutputSize(LEGACY_CAMERA_ID, ImageFormat.YUV_420_888);
+        assertEquals(mMaximumSize, maximumYUVSize);
+        Size maximumJPEGSize = mSurfaceManager.getMaxOutputSize(LEGACY_CAMERA_ID, ImageFormat.JPEG);
+        assertEquals(mMaximumSize, 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);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.SENSOR_ORIENTATION, DEFAULT_SENSOR_ORIENTATION);
+
+        if (capabilities != null) {
+            shadowCharacteristics.set(
+                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities);
+        }
+
+        ((ShadowCameraManager) Shadow.extract(
+                ApplicationProvider.getApplicationContext().getSystemService(
+                        Context.CAMERA_SERVICE)))
+                .addCamera(cameraId, characteristics);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(
+                        mSupportedFormats, mSupportedSizes));
+    }
+
+    private void initCameraX() {
+        AppConfiguration appConfig = createFakeAppConfiguration();
+        CameraX.init(mContext, appConfig);
+        mSurfaceManager = CameraX.getSurfaceManager();
+    }
+
+    private AppConfiguration createFakeAppConfiguration() {
+
+        // Create the camera factory for creating Camera2 camera objects
+        CameraFactory cameraFactory = new Camera2CameraFactory(mContext);
+
+        // Create the DeviceSurfaceManager for Camera2
+        CameraDeviceSurfaceManager surfaceManager =
+                new Camera2DeviceSurfaceManager(mContext, mMockCamcorderProfileHelper);
+
+        // Create default configuration factory
+        ExtendableUseCaseConfigFactory configFactory = new ExtendableUseCaseConfigFactory();
+        configFactory.installDefaultProvider(
+                ImageAnalysisUseCaseConfiguration.class,
+                new DefaultImageAnalysisConfigurationProvider(cameraFactory, mContext));
+        configFactory.installDefaultProvider(
+                ImageCaptureUseCaseConfiguration.class,
+                new DefaultImageCaptureConfigurationProvider(cameraFactory, mContext));
+        configFactory.installDefaultProvider(
+                VideoCaptureUseCaseConfiguration.class,
+                new DefaultVideoCaptureConfigurationProvider(cameraFactory, mContext));
+        configFactory.installDefaultProvider(
+                ViewFinderUseCaseConfiguration.class,
+                new DefaultViewFinderConfigurationProvider(cameraFactory, mContext));
+
+        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/CameraCaptureCallbackAdapterTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/CameraCaptureCallbackAdapterTest.java
new file mode 100644
index 0000000..3a67f31
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/CameraCaptureCallbackAdapterTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+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.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CaptureFailure;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Build;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureFailure;
+import androidx.camera.core.CameraCaptureResult;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public final class CameraCaptureCallbackAdapterTest {
+
+    private CameraCaptureCallback mCameraCaptureCallback;
+    private CameraCaptureSession mCameraCaptureSession;
+    private CaptureRequest mCaptureRequest;
+    private TotalCaptureResult mCaptureResult;
+    private CameraCaptureCallbackAdapter mCameraCaptureCallbackAdapter;
+
+    @Before
+    public void setUp() {
+        mCameraCaptureCallback = mock(CameraCaptureCallback.class);
+        mCameraCaptureSession = mock(CameraCaptureSession.class);
+        mCaptureRequest = mock(CaptureRequest.class);
+        mCaptureResult = mock(TotalCaptureResult.class);
+        mCameraCaptureCallbackAdapter = new CameraCaptureCallbackAdapter(mCameraCaptureCallback);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void createCameraCaptureCallbackAdapterWithNullArgument() {
+        new CameraCaptureCallbackAdapter(null);
+    }
+
+    @Test
+    public void onCaptureCompleted() {
+        mCameraCaptureCallbackAdapter.onCaptureCompleted(
+                mCameraCaptureSession, mCaptureRequest, mCaptureResult);
+        verify(mCameraCaptureCallback, times(1)).onCaptureCompleted(any(CameraCaptureResult.class));
+    }
+
+    @Test
+    public void onCaptureFailed() {
+        mCameraCaptureCallbackAdapter.onCaptureFailed(mCameraCaptureSession, mCaptureRequest,
+                mock(CaptureFailure.class));
+        verify(mCameraCaptureCallback, times(1)).onCaptureFailed(any(CameraCaptureFailure.class));
+    }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/CaptureCallbackConverterTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/CaptureCallbackConverterTest.java
new file mode 100644
index 0000000..04d82d0
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/CaptureCallbackConverterTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2;
+
+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.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureCallback;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.TotalCaptureResult;
+import android.os.Build;
+
+import androidx.camera.core.CameraCaptureCallback;
+import androidx.camera.core.CameraCaptureCallbacks;
+import androidx.camera.core.CameraCaptureResult;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public final class CaptureCallbackConverterTest {
+
+    private CameraCaptureSession mCameraCaptureSession;
+    private CaptureRequest mCaptureRequest;
+    private TotalCaptureResult mCaptureResult;
+
+    @Before
+    public void setUp() {
+        mCameraCaptureSession = mock(CameraCaptureSession.class);
+        mCaptureRequest = mock(CaptureRequest.class);
+        mCaptureResult = mock(TotalCaptureResult.class);
+    }
+
+    @Test
+    public void toCaptureCallback() {
+        CameraCaptureCallback cameraCallback = Mockito.mock(CameraCaptureCallback.class);
+        CaptureCallback callback = CaptureCallbackConverter.toCaptureCallback(cameraCallback);
+        callback.onCaptureCompleted(mCameraCaptureSession, mCaptureRequest, mCaptureResult);
+        verify(cameraCallback, times(1)).onCaptureCompleted(any(CameraCaptureResult.class));
+    }
+
+    @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(mCameraCaptureSession, mCaptureRequest, mCaptureResult);
+        verify(actualCallback, times(1)).onCaptureCompleted(
+                any(CameraCaptureSession.class),
+                any(CaptureRequest.class),
+                any(TotalCaptureResult.class));
+    }
+
+    @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(mCameraCaptureSession, mCaptureRequest, mCaptureResult);
+        verify(cameraCallback1, times(1)).onCaptureCompleted(any(CameraCaptureResult.class));
+        verify(cameraCallback2, times(1)).onCaptureCompleted(any(CameraCaptureResult.class));
+        verify(cameraCallback3, times(1)).onCaptureCompleted(any(CameraCaptureSession.class), any(
+                CaptureRequest.class), any(TotalCaptureResult.class));
+    }
+}
diff --git a/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationTest.java b/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationTest.java
new file mode 100644
index 0000000..5c58ea0
--- /dev/null
+++ b/camera/camera2/src/test/java/androidx/camera/camera2/SupportedSurfaceCombinationTest.java
@@ -0,0 +1,659 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
+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.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 androidx.camera.testing.StreamConfigurationMapUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+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 */
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public final class SupportedSurfaceCombinationTest {
+    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 static final int DEFAULT_SENSOR_ORIENTATION = 90;
+    private final Size mDisplaySize = new Size(1280, 720);
+    private final Size mAnalysisSize = new Size(640, 480);
+    private final Size mPreviewSize = mDisplaySize;
+    private final Size mRecordSize = new Size(3840, 2160);
+    private final Size mMaximumSize = new Size(4032, 3024);
+    private final Size mMaximumVideoSize = new Size(1920, 1080);
+    private final CamcorderProfileHelper mMockCamcorderProfileHelper =
+            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[] mSupportedFormats =
+            new int[]{
+                    ImageFormat.YUV_420_888,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_JPEG,
+                    ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+            };
+
+    private final Size[] mSupportedSizes =
+            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 mContext = RuntimeEnvironment.application.getApplicationContext();
+
+    @Before
+    public void setUp() {
+        WindowManager windowManager =
+                (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealWidth(mDisplaySize.getWidth());
+        Shadows.shadowOf(windowManager.getDefaultDisplay()).setRealHeight(mDisplaySize.getHeight());
+
+        when(mMockCamcorderProfileHelper.hasProfile(anyInt(), anyInt())).thenReturn(true);
+
+        setupCamera();
+    }
+
+    @Test
+    public void checkLegacySurfaceCombinationSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLegacySupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void checkLimitedSurfaceCombinationNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLimitedSupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void checkFullSurfaceCombinationNotSupportedInLimitedDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, FULL_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, FULL_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getFullSupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void checkLevel3SurfaceCombinationNotSupportedInFullDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, FULL_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LEVEL3_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LEVEL3_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        List<SurfaceCombination> combinationList =
+                supportedSurfaceCombination.getLevel3SupportedCombinationList();
+
+        boolean isSupported =
+                isAllSubConfigurationListSupported(supportedSurfaceCombination, combinationList);
+        assertTrue(isSupported);
+    }
+
+    @Test
+    public void suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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(
+                        mContext, LIMITED_CAMERA_ID, mMockCamcorderProfileHelper);
+
+        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, mRecordSize);
+        assertThat(suggestedResolutionMap).containsEntry(videoCaptureUseCase, mMaximumVideoSize);
+        assertThat(suggestedResolutionMap).containsEntry(viewFinderUseCase, mPreviewSize);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVAnalysisSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, mAnalysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVPreviewSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, mPreviewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVRecordSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, mRecordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVMaximumSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888, mMaximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithYUVNotSupportSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.YUV_420_888,
+                        new Size(mMaximumSize.getWidth() + 1, mMaximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.YUV, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGAnalysisSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, mAnalysisSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.ANALYSIS);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGPreviewSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, mPreviewSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.PREVIEW);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGRecordSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, mRecordSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.RECORD);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGMaximumSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG, mMaximumSize);
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.MAXIMUM);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void transformSurfaceConfigurationWithJPEGNotSupportSize() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        SurfaceConfiguration surfaceConfiguration =
+                supportedSurfaceCombination.transformSurfaceConfiguration(
+                        ImageFormat.JPEG,
+                        new Size(mMaximumSize.getWidth() + 1, mMaximumSize.getHeight() + 1));
+        SurfaceConfiguration expectedSurfaceConfiguration =
+                SurfaceConfiguration.create(ConfigurationType.JPEG, ConfigurationSize.NOT_SUPPORT);
+        assertEquals(expectedSurfaceConfiguration, surfaceConfiguration);
+    }
+
+    @Test
+    public void getMaximumSizeForImageFormat() {
+        SupportedSurfaceCombination supportedSurfaceCombination =
+                new SupportedSurfaceCombination(
+                        mContext, LEGACY_CAMERA_ID, mMockCamcorderProfileHelper);
+        Size maximumYUVSize =
+                supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.YUV_420_888);
+        assertEquals(mMaximumSize, maximumYUVSize);
+        Size maximumJPEGSize =
+                supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG);
+        assertEquals(mMaximumSize, 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);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.SENSOR_ORIENTATION, DEFAULT_SENSOR_ORIENTATION);
+
+        if (capabilities != null) {
+            shadowCharacteristics.set(
+                    CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, capabilities);
+        }
+
+        ((ShadowCameraManager) Shadow.extract(
+                ApplicationProvider.getApplicationContext().getSystemService(
+                        Context.CAMERA_SERVICE)))
+                .addCamera(cameraId, characteristics);
+
+        shadowCharacteristics.set(
+                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP,
+                StreamConfigurationMapUtil.generateFakeStreamConfigurationMap(
+                        mSupportedFormats, mSupportedSizes));
+    }
+
+    private void initCameraX() {
+        AppConfiguration appConfig = Camera2AppConfiguration.create(mContext);
+        CameraX.init(mContext, 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/build.gradle b/camera/core/build.gradle
new file mode 100644
index 0000000..da36183
--- /dev/null
+++ b/camera/core/build.gradle
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+// TODO(b/124783972): Switch to androidx.build.LibraryVersions and androidx.build.LibraryGroups when ready
+import androidx.build.UnpublishedLibraryVersions
+import androidx.build.UnpublishedLibraryGroups
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    api("androidx.lifecycle:lifecycle-common:2.0.0", libs.exclude_annotations_transitive)
+    implementation("androidx.annotation:annotation:1.0.0")
+    implementation("androidx.core:core:1.0.0")
+    implementation("androidx.exifinterface:exifinterface:1.0.0")
+
+    implementation(AUTO_VALUE_ANNOTATIONS)
+    implementation(GUAVA_LISTENABLE_FUTURE)
+    implementation(GUAVA_ANDROID) // TODO(b/120832996): Remove once we've isolated needed Futures methods
+    implementation(TRUTH) // TODO(b/120832996): Needed to resolve a version conflict in tests with GUAVA_ANDROID. Remove once we've removed GUAVA_ANDROID.
+
+    annotationProcessor(AUTO_VALUE)
+
+    testImplementation(TEST_CORE)
+    testImplementation(JUNIT)
+    testImplementation(TEST_RUNNER)
+    testImplementation(TRUTH)
+    testImplementation(ROBOLECTRIC)
+    testImplementation(MOCKITO_CORE)
+    testImplementation project(":camera:camera-testing"), {
+        exclude group: "androidx.camera", module: "camera-core"
+    }
+
+    androidTestImplementation(TEST_EXT_JUNIT)
+    androidTestImplementation(TEST_CORE)
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(TEST_RULES)
+    androidTestImplementation(TRUTH)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it's own MockMaker
+    androidTestImplementation project(":camera:camera-testing"), {
+        exclude group: "androidx.camera", module: "camera-core"
+    }
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+
+    // Use Robolectric 4.+
+    testOptions.unitTests.includeAndroidResources = true
+
+    packagingOptions {
+        pickFirst 'META-INF/support.camera_camera-core.version'
+    }
+}
+
+supportLibrary {
+    name = "Jetpack Camera Core Library"
+    publish = true
+    mavenVersion = UnpublishedLibraryVersions.CAMERA
+    mavenGroup = UnpublishedLibraryGroups.CAMERA
+    inceptionYear = "2019"
+    description = "Core components for the Jetpack Camera Library, a library providing a " +
+            "consistent and reliable camera foundation that enables great camera driven " +
+            "experiences across all of Android."
+}
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..73cd4f6
--- /dev/null
+++ b/camera/core/src/androidTest/AndroidManifest.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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.core.test">
+
+    <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
+
+</manifest>
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java
new file mode 100644
index 0000000..2616a3c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.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 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class AndroidImageProxyTest {
+    private static final long INITIAL_TIMESTAMP = 138990020L;
+
+    private final Image mImage = mock(Image.class);
+    private final Image.Plane mYPlane = mock(Image.Plane.class);
+    private final Image.Plane mUPlane = mock(Image.Plane.class);
+    private final Image.Plane mVPlane = mock(Image.Plane.class);
+    private ImageProxy mImageProxy;
+
+    @Before
+    public void setUp() {
+        when(mImage.getPlanes()).thenReturn(new Image.Plane[]{mYPlane, mUPlane, mVPlane});
+        when(mYPlane.getRowStride()).thenReturn(640);
+        when(mYPlane.getPixelStride()).thenReturn(1);
+        when(mYPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(640 * 480));
+        when(mUPlane.getRowStride()).thenReturn(320);
+        when(mUPlane.getPixelStride()).thenReturn(1);
+        when(mUPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(320 * 240));
+        when(mVPlane.getRowStride()).thenReturn(320);
+        when(mVPlane.getPixelStride()).thenReturn(1);
+        when(mVPlane.getBuffer()).thenReturn(ByteBuffer.allocateDirect(320 * 240));
+
+        when(mImage.getTimestamp()).thenReturn(INITIAL_TIMESTAMP);
+        mImageProxy = new AndroidImageProxy(mImage);
+    }
+
+    @Test
+    public void close_closesWrappedImage() {
+        mImageProxy.close();
+
+        verify(mImage).close();
+    }
+
+    @Test
+    public void getCropRect_returnsCropRectForWrappedImage() {
+        when(mImage.getCropRect()).thenReturn(new Rect(0, 0, 20, 20));
+
+        assertThat(mImageProxy.getCropRect()).isEqualTo(new Rect(0, 0, 20, 20));
+    }
+
+    @Test
+    public void setCropRect_setsCropRectForWrappedImage() {
+        mImageProxy.setCropRect(new Rect(0, 0, 40, 40));
+
+        verify(mImage).setCropRect(new Rect(0, 0, 40, 40));
+    }
+
+    @Test
+    public void getFormat_returnsFormatForWrappedImage() {
+        when(mImage.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+        assertThat(mImageProxy.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
+    }
+
+    @Test
+    public void getHeight_returnsHeightForWrappedImage() {
+        when(mImage.getHeight()).thenReturn(480);
+
+        assertThat(mImageProxy.getHeight()).isEqualTo(480);
+    }
+
+    @Test
+    public void getWidth_returnsWidthForWrappedImage() {
+        when(mImage.getWidth()).thenReturn(640);
+
+        assertThat(mImageProxy.getWidth()).isEqualTo(640);
+    }
+
+    @Test
+    public void getTimestamp_returnsTimestampForWrappedImage() {
+        assertThat(mImageProxy.getTimestamp()).isEqualTo(INITIAL_TIMESTAMP);
+    }
+
+    public void setTimestamp_setsTimestampForWrappedImage() {
+        mImageProxy.setTimestamp(INITIAL_TIMESTAMP + 10);
+
+        assertThat(mImageProxy.getTimestamp()).isEqualTo(INITIAL_TIMESTAMP + 10);
+    }
+
+    @Test
+    public void getPlanes_returnsPlanesForWrappedImage() {
+        ImageProxy.PlaneProxy[] wrappedPlanes = mImageProxy.getPlanes();
+
+        Image.Plane[] originalPlanes = new Image.Plane[]{mYPlane, mUPlane, mVPlane};
+        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());
+        }
+    }
+
+    @Test
+    public void getImage_returnsWrappedImage() {
+        assertThat(mImageProxy.getImage()).isEqualTo(mImage);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyTest.java b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyTest.java
new file mode 100644
index 0000000..b56017a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/AndroidImageReaderProxyTest.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.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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class AndroidImageReaderProxyTest {
+    private final ImageReader mImageReader = mock(ImageReader.class);
+    private ImageReaderProxy mImageReaderProxy;
+
+    @Before
+    public void setUp() {
+        mImageReaderProxy = new AndroidImageReaderProxy(mImageReader);
+        when(mImageReader.acquireLatestImage()).thenReturn(mock(Image.class));
+        when(mImageReader.acquireNextImage()).thenReturn(mock(Image.class));
+    }
+
+    @Test
+    public void acquireLatestImage_invokesMethodOnWrappedReader() {
+        mImageReaderProxy.acquireLatestImage();
+
+        verify(mImageReader, times(1)).acquireLatestImage();
+    }
+
+    @Test
+    public void acquireNextImage_invokesMethodOnWrappedReader() {
+        mImageReaderProxy.acquireNextImage();
+
+        verify(mImageReader, times(1)).acquireNextImage();
+    }
+
+    @Test
+    public void close_invokesMethodOnWrappedReader() {
+        mImageReaderProxy.close();
+
+        verify(mImageReader, times(1)).close();
+    }
+
+    @Test
+    public void getWidth_returnsWidthOfWrappedReader() {
+        when(mImageReader.getWidth()).thenReturn(640);
+
+        assertThat(mImageReaderProxy.getWidth()).isEqualTo(640);
+    }
+
+    @Test
+    public void getHeight_returnsHeightOfWrappedReader() {
+        when(mImageReader.getHeight()).thenReturn(480);
+
+        assertThat(mImageReaderProxy.getHeight()).isEqualTo(480);
+    }
+
+    @Test
+    public void getImageFormat_returnsImageFormatOfWrappedReader() {
+        when(mImageReader.getImageFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+        assertThat(mImageReaderProxy.getImageFormat()).isEqualTo(ImageFormat.YUV_420_888);
+    }
+
+    @Test
+    public void getMaxImages_returnsMaxImagesOfWrappedReader() {
+        when(mImageReader.getMaxImages()).thenReturn(8);
+
+        assertThat(mImageReaderProxy.getMaxImages()).isEqualTo(8);
+    }
+
+    @Test
+    public void getSurface_returnsSurfaceOfWrappedReader() {
+        Surface surface = mock(Surface.class);
+        when(mImageReader.getSurface()).thenReturn(surface);
+
+        assertThat(mImageReaderProxy.getSurface()).isSameAs(surface);
+    }
+
+    @Test
+    public void setOnImageAvailableListener_setsListenerOfWrappedReader() {
+        ImageReaderProxy.OnImageAvailableListener listener =
+                mock(ImageReaderProxy.OnImageAvailableListener.class);
+
+        mImageReaderProxy.setOnImageAvailableListener(listener, /*handler=*/ null);
+
+        ArgumentCaptor<ImageReader.OnImageAvailableListener> transformedListenerCaptor =
+                ArgumentCaptor.forClass(ImageReader.OnImageAvailableListener.class);
+        ArgumentCaptor<Handler> handlerCaptor = ArgumentCaptor.forClass(Handler.class);
+        verify(mImageReader, times(1))
+                .setOnImageAvailableListener(
+                        transformedListenerCaptor.capture(), handlerCaptor.capture());
+
+        transformedListenerCaptor.getValue().onImageAvailable(mImageReader);
+        verify(listener, times(1)).onImageAvailable(mImageReaderProxy);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseTest.java b/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseTest.java
new file mode 100644
index 0000000..d75cc29
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/BaseUseCaseTest.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 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.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BaseUseCaseTest {
+    private BaseUseCase.StateChangeListener mMockUseCaseListener;
+
+    @Before
+    public void setup() {
+        mMockUseCaseListener = 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(mMockUseCaseListener);
+        testUseCase.removeStateChangeListener(mMockUseCaseListener);
+
+        testUseCase.activate();
+
+        verify(mMockUseCaseListener, never()).onUseCaseActive(any(BaseUseCase.class));
+    }
+
+    @Test
+    public void clearListeners() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mMockUseCaseListener);
+        testUseCase.clear();
+
+        testUseCase.activate();
+        verify(mMockUseCaseListener, never()).onUseCaseActive(any(BaseUseCase.class));
+    }
+
+    @Test
+    public void notifyActiveState() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mMockUseCaseListener);
+
+        testUseCase.activate();
+        verify(mMockUseCaseListener, times(1)).onUseCaseActive(testUseCase);
+    }
+
+    @Test
+    public void notifyInactiveState() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mMockUseCaseListener);
+
+        testUseCase.deactivate();
+        verify(mMockUseCaseListener, times(1)).onUseCaseInactive(testUseCase);
+    }
+
+    @Test
+    public void notifyUpdatedSettings() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mMockUseCaseListener);
+
+        testUseCase.update();
+        verify(mMockUseCaseListener, times(1)).onUseCaseUpdated(testUseCase);
+    }
+
+    @Test
+    public void notifyResetUseCase() {
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder().setTargetName("UseCase").build();
+        TestUseCase testUseCase = new TestUseCase(configuration);
+        testUseCase.addStateChangeListener(mMockUseCaseListener);
+
+        testUseCase.notifyReset();
+        verify(mMockUseCaseListener, times(1)).onUseCaseReset(testUseCase);
+    }
+
+    @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/CameraCaptureCallbacksTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksTest.java
new file mode 100644
index 0000000..ad61b42
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureCallbacksTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CameraCaptureCallbacksTest {
+
+    @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/CameraCaptureFailureTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureTest.java
new file mode 100644
index 0000000..2ad8475
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureFailureTest.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 static com.google.common.truth.Truth.assertThat;
+
+import androidx.camera.core.CameraCaptureFailure.Reason;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CameraCaptureFailureTest {
+
+    @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/CameraCaptureSessionStateCallbacksTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksTest.java
new file mode 100644
index 0000000..c9cedf6
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraCaptureSessionStateCallbacksTest.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 org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraCaptureSession;
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CameraCaptureSessionStateCallbacksTest {
+
+    @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/CameraDeviceStateCallbacksTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksTest.java
new file mode 100644
index 0000000..ebae54e
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraDeviceStateCallbacksTest.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 org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.camera2.CameraDevice;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CameraDeviceStateCallbacksTest {
+
+    @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/CameraRepositoryTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryTest.java
new file mode 100644
index 0000000..3748fc8
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraRepositoryTest.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 androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CameraRepositoryTest {
+
+    private CameraRepository mCameraRepository;
+
+    @Before
+    public void setUp() {
+        mCameraRepository = new CameraRepository();
+        mCameraRepository.init(new FakeCameraFactory());
+    }
+
+    @Test
+    public void cameraIdsCanBeAcquired() {
+        Set<String> cameraIds = mCameraRepository.getCameraIds();
+
+        assertThat(cameraIds).isNotEmpty();
+    }
+
+    @Test
+    public void cameraCanBeObtainedWithValidId() {
+        for (String cameraId : mCameraRepository.getCameraIds()) {
+            BaseCamera camera = mCameraRepository.getCamera(cameraId);
+
+            assertThat(camera).isNotNull();
+        }
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void cameraCannotBeObtainedWithInvalidId() {
+        // Should throw IllegalArgumentException
+        mCameraRepository.getCamera("no_such_id");
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/CameraXTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CameraXTest.java
new file mode 100644
index 0000000..853a5e3
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CameraXTest.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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+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.FakeCamera;
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
+import androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.camera.testing.fakes.FakeCameraInfo;
+import androidx.camera.testing.fakes.FakeLifecycleOwner;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CameraXTest {
+    // TODO(b/126431497): This shouldn't need to be static, but the initialization behavior does
+    //  not allow us to reinitialize before each test.
+    private static FakeCameraFactory sCameraFactory = new FakeCameraFactory();
+
+    static {
+        String cameraId = sCameraFactory.cameraIdForLensFacing(LensFacing.BACK);
+        sCameraFactory.insertCamera(cameraId,
+                new FakeCamera(new FakeCameraInfo(), mock(CameraControl.class)));
+    }
+
+    private String mCameraId;
+    private BaseCamera mCamera;
+    private FakeLifecycleOwner mLifecycle;
+    private CountingErrorListener mErrorlistener;
+    private CountDownLatch mLatch;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+
+    private static 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();
+        ExtendableUseCaseConfigFactory defaultConfigFactory = new ExtendableUseCaseConfigFactory();
+        defaultConfigFactory.installDefaultProvider(FakeUseCaseConfiguration.class,
+                new ConfigurationProvider<FakeUseCaseConfiguration>() {
+                    @Override
+                    public FakeUseCaseConfiguration getConfiguration(
+                            CameraX.LensFacing lensFacing) {
+                        return new FakeUseCaseConfiguration.Builder().build();
+                    }
+                });
+        AppConfiguration.Builder appConfigBuilder =
+                new AppConfiguration.Builder()
+                        .setCameraFactory(sCameraFactory)
+                        .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());
+        mLifecycle = new FakeLifecycleOwner();
+
+
+        mLatch = new CountDownLatch(1);
+        mErrorlistener = new CountingErrorListener(mLatch);
+        mHandlerThread = new HandlerThread("ErrorHandlerThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mCameraId = getCameraIdUnchecked(LensFacing.BACK);
+        mCamera = sCameraFactory.getCamera(mCameraId);
+
+    }
+
+    @After
+    public void tearDown() throws InterruptedException {
+        CameraX.unbindAll();
+        mHandlerThread.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(mLifecycle, new FakeUseCase());
+
+        // One observer is the use case group. The other observer removes the use case upon the
+        // lifecycle's destruction.
+        assertThat(mLifecycle.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(mLifecycle, fakeUseCase, fakeOtherUseCase);
+
+        assertThat(CameraX.isBound(fakeUseCase)).isTrue();
+        assertThat(CameraX.isBound(fakeOtherUseCase)).isTrue();
+    }
+
+    @Test
+    public void isNotBound_afterUnbind() {
+        FakeUseCase fakeUseCase = new FakeUseCase();
+        CameraX.bindToLifecycle(mLifecycle, 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(mLifecycle, 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(mLifecycle.getObserverCount()).isEqualTo(2);
+        assertThat(anotherLifecycle.getObserverCount()).isEqualTo(2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void exception_withDestroyedLifecycle() {
+        FakeUseCase useCase = new FakeUseCase();
+
+        mLifecycle.destroy();
+
+        CameraX.bindToLifecycle(mLifecycle, useCase);
+    }
+
+    @Test
+    public void errorListenerGetsCalled_whenErrorPosted() throws InterruptedException {
+        CameraX.setErrorListener(mErrorlistener, mHandler);
+        CameraX.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+        mLatch.await(1, TimeUnit.SECONDS);
+
+        assertThat(mErrorlistener.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, LensFacing.BACK);
+        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(mLifecycle, fakeUseCase);
+
+        assertThat(fakeUseCase.getCameraControl(mCameraId)).isEqualTo(mCamera.getCameraControl());
+    }
+
+    @Test
+    public void onCameraControlReadyIsCalled_afterBindToLifecycle() {
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+        AttachCameraFakeCase fakeUseCase = spy(new AttachCameraFakeCase(configuration0));
+
+        CameraX.bindToLifecycle(mLifecycle, fakeUseCase);
+
+        Mockito.verify(fakeUseCase).onCameraControlReady(mCameraId);
+    }
+
+    @Test
+    public void detachCameraControl_afterUnbind() {
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder().setTargetName("config0").build();
+        AttachCameraFakeCase fakeUseCase = new AttachCameraFakeCase(configuration0);
+        CameraX.bindToLifecycle(mLifecycle, fakeUseCase);
+
+        CameraX.unbind(fakeUseCase);
+
+        // after unbind, Camera's CameraControl should be detached from Usecase
+        assertThat(fakeUseCase.getCameraControl(mCameraId)).isNotEqualTo(
+                mCamera.getCameraControl());
+        // UseCase still gets a non-null default CameraControl that does nothing.
+        assertThat(fakeUseCase.getCameraControl(mCameraId)).isEqualTo(
+                CameraControl.DEFAULT_EMPTY_INSTANCE);
+    }
+
+    @Test
+    public void canRetrieveCameraInfo() throws CameraInfoUnavailableException {
+        String cameraId = CameraX.getCameraWithLensFacing(LensFacing.BACK);
+        CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
+        assertThat(cameraInfo).isNotNull();
+    }
+
+    @Test
+    public void canGetCameraXContext() {
+        Context context = CameraX.getContext();
+        assertThat(context).isNotNull();
+    }
+
+    private static class CountingErrorListener implements ErrorListener {
+        CountDownLatch mLatch;
+        AtomicInteger mCount = new AtomicInteger(0);
+
+        CountingErrorListener(CountDownLatch latch) {
+            mLatch = latch;
+        }
+
+        @Override
+        public void onError(ErrorCode errorCode, String message) {
+            mCount.getAndIncrement();
+            mLatch.countDown();
+        }
+
+        public int getCount() {
+            return mCount.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/CaptureRequestConfigurationTest.java b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationTest.java
new file mode 100644
index 0000000..0cf6078
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/CaptureRequestConfigurationTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CaptureRequestConfigurationTest {
+    private DeferrableSurface mMockSurface0;
+
+    @Before
+    public void setup() {
+        mMockSurface0 = 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(mMockSurface0);
+        CaptureRequestConfiguration captureRequestConfiguration = builder.build();
+
+        List<DeferrableSurface> surfaces = captureRequestConfiguration.getSurfaces();
+
+        assertThat(surfaces).hasSize(1);
+        assertThat(surfaces).contains(mMockSurface0);
+    }
+
+    @Test
+    public void builderRemoveSurface() {
+        CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+
+        builder.addSurface(mMockSurface0);
+        builder.removeSurface(mMockSurface0);
+        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(mMockSurface0);
+        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/ErrorHandlerTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerTest.java
new file mode 100644
index 0000000..0b77991
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ErrorHandlerTest.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.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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ErrorHandlerTest {
+    private ErrorHandler mErrorHandler;
+    private CountingErrorListener mErrorListener0;
+    private CountingErrorListener mErrorListener1;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private CountDownLatch mLatch;
+
+    @Before
+    public void setup() {
+        mErrorHandler = new ErrorHandler();
+        mLatch = new CountDownLatch(1);
+        mErrorListener0 = new CountingErrorListener(mLatch);
+        mErrorListener1 = new CountingErrorListener(mLatch);
+
+        mHandlerThread = new HandlerThread("ErrorHandlerThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+    }
+
+    @Test
+    public void errorListenerCalled_whenSet() throws InterruptedException {
+        mErrorHandler.setErrorListener(mErrorListener0, mHandler);
+
+        mErrorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+        mLatch.await(1, TimeUnit.SECONDS);
+
+        assertThat(mErrorListener0.getCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void errorListenerRemoved_whenNullSet() throws InterruptedException {
+        mErrorHandler.setErrorListener(mErrorListener0, mHandler);
+        mErrorHandler.setErrorListener(null, mHandler);
+
+        mErrorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+        assertThat(mLatch.await(1, TimeUnit.SECONDS)).isFalse();
+    }
+
+    @Test
+    public void errorListenerReplaced() throws InterruptedException {
+        mErrorHandler.setErrorListener(mErrorListener0, mHandler);
+        mErrorHandler.setErrorListener(mErrorListener1, mHandler);
+
+        mErrorHandler.postError(CameraX.ErrorCode.CAMERA_STATE_INCONSISTENT, "");
+
+        mLatch.await(1, TimeUnit.SECONDS);
+
+        assertThat(mErrorListener0.getCount()).isEqualTo(0);
+        assertThat(mErrorListener1.getCount()).isEqualTo(1);
+    }
+
+    private static class CountingErrorListener implements ErrorListener {
+        CountDownLatch mLatch;
+        AtomicInteger mCount = new AtomicInteger(0);
+
+        CountingErrorListener(CountDownLatch latch) {
+            mLatch = latch;
+        }
+
+        @Override
+        public void onError(ErrorCode errorCode, String message) {
+            mCount.getAndIncrement();
+            mLatch.countDown();
+        }
+
+        public int getCount() {
+            return mCount.get();
+        }
+    }
+}
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..ce282ba
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCase.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.util.Size;
+
+import androidx.camera.testing.fakes.FakeUseCase;
+
+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 mIsCleared = 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();
+        mIsCleared = true;
+    }
+
+    @Override
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder(
+            CameraX.LensFacing lensFacing) {
+        return new FakeOtherUseCaseConfiguration.Builder().setLensFacing(
+                lensFacing == null ? CameraX.LensFacing.BACK : lensFacing);
+    }
+
+    @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 mIsCleared;
+    }
+}
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..6d395f9
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfiguration.java
@@ -0,0 +1,286 @@
+/*
+ * 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 java.util.Set;
+import java.util.UUID;
+
+/** A fake configuration for {@link FakeOtherUseCase}. */
+public class FakeOtherUseCaseConfiguration
+        implements UseCaseConfiguration<FakeOtherUseCase>, CameraDeviceConfiguration {
+
+    private final Configuration mConfig;
+
+    private FakeOtherUseCaseConfiguration(Configuration config) {
+        mConfig = config;
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return mConfig;
+    }
+
+    /** Builder for an empty Configuration */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<
+            FakeOtherUseCase, FakeOtherUseCaseConfiguration, Builder>,
+            CameraDeviceConfiguration.Builder<FakeOtherUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle mOptionsBundle;
+
+        public Builder() {
+            mOptionsBundle = MutableOptionsBundle.create();
+            setTargetClass(FakeOtherUseCase.class);
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mOptionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public FakeOtherUseCaseConfiguration build() {
+            return new FakeOtherUseCaseConfiguration(OptionsBundle.from(mOptionsBundle));
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // Implementations of TargetConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetClass(Class<FakeOtherUseCase> 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();
+        }
+
+        @Override
+        public Builder setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+
+        // Implementations of CameraDeviceConfiguration.Builder default methods
+
+        @Override
+        public Builder setLensFacing(CameraX.LensFacing lensFacing) {
+            getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+            return builder();
+        }
+
+        // Implementations of UseCaseConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+            getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+            getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // Implementations of TargetConfiguration default methods
+
+    @Override
+    @Nullable
+    public Class<FakeOtherUseCase> getTargetClass(
+            @Nullable Class<FakeOtherUseCase> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<FakeOtherUseCase> storedClass = (Class<FakeOtherUseCase>) retrieveOption(
+                OPTION_TARGET_CLASS,
+                valueIfMissing);
+        return storedClass;
+    }
+
+    @Override
+    public Class<FakeOtherUseCase> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<FakeOtherUseCase> storedClass = (Class<FakeOtherUseCase>) retrieveOption(
+                OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    @Override
+    @Nullable
+    public String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    @Override
+    public String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    // Implementations of CameraDeviceConfiguration default methods
+
+    @Override
+    @Nullable
+    public CameraX.LensFacing getLensFacing(@Nullable CameraX.LensFacing valueIfMissing) {
+        return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+    }
+
+    @Override
+    public CameraX.LensFacing getLensFacing() {
+        return retrieveOption(OPTION_LENS_FACING);
+    }
+
+    // Implementations of UseCaseConfiguration default methods
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration valueIfMissing) {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration getDefaultSessionConfiguration() {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+        return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+        return retrieveOption(OPTION_CONFIG_UNPACKER);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority(int valueIfMissing) {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority() {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
new file mode 100644
index 0000000..82546d7
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicReference;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ForwardingImageProxyTest {
+
+    private final ImageProxy mBaseImageProxy = mock(ImageProxy.class);
+    private final ImageProxy.PlaneProxy mYPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy mUPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy mVPlane = mock(ImageProxy.PlaneProxy.class);
+    private ForwardingImageProxy mImageProxy;
+
+    @Before
+    public void setUp() {
+        mImageProxy = new ConcreteImageProxy(mBaseImageProxy);
+    }
+
+    @Test
+    public void close_closesWrappedImage() {
+        mImageProxy.close();
+
+        verify(mBaseImageProxy).close();
+    }
+
+    @Test
+    public void close_notifiesOnImageCloseListener_afterSetOnImageCloseListener()
+            throws InterruptedException {
+        final Semaphore closedImageSemaphore = new Semaphore(/*permits=*/ 0);
+        final AtomicReference<ImageProxy> closedImage = new AtomicReference<>();
+        mImageProxy.addOnImageCloseListener(
+                new ForwardingImageProxy.OnImageCloseListener() {
+                    @Override
+                    public void onImageClose(ImageProxy image) {
+                        closedImage.set(image);
+                        closedImageSemaphore.release();
+                    }
+                });
+
+        mImageProxy.close();
+
+        closedImageSemaphore.acquire();
+        assertThat(closedImage.get()).isSameAs(mImageProxy);
+    }
+
+    @Test
+    public void getCropRect_returnsCropRectForWrappedImage() {
+        when(mBaseImageProxy.getCropRect()).thenReturn(new Rect(0, 0, 20, 20));
+
+        assertThat(mImageProxy.getCropRect()).isEqualTo(new Rect(0, 0, 20, 20));
+    }
+
+    @Test
+    public void setCropRect_setsCropRectForWrappedImage() {
+        mImageProxy.setCropRect(new Rect(0, 0, 40, 40));
+
+        verify(mBaseImageProxy).setCropRect(new Rect(0, 0, 40, 40));
+    }
+
+    @Test
+    public void getFormat_returnsFormatForWrappedImage() {
+        when(mBaseImageProxy.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+
+        assertThat(mImageProxy.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
+    }
+
+    @Test
+    public void getHeight_returnsHeightForWrappedImage() {
+        when(mBaseImageProxy.getHeight()).thenReturn(480);
+
+        assertThat(mImageProxy.getHeight()).isEqualTo(480);
+    }
+
+    @Test
+    public void getWidth_returnsWidthForWrappedImage() {
+        when(mBaseImageProxy.getWidth()).thenReturn(640);
+
+        assertThat(mImageProxy.getWidth()).isEqualTo(640);
+    }
+
+    @Test
+    public void getTimestamp_returnsTimestampForWrappedImage() {
+        when(mBaseImageProxy.getTimestamp()).thenReturn(138990020L);
+
+        assertThat(mImageProxy.getTimestamp()).isEqualTo(138990020L);
+    }
+
+    @Test
+    public void setTimestamp_setsTimestampForWrappedImage() {
+        mImageProxy.setTimestamp(138990020L);
+
+        verify(mBaseImageProxy).setTimestamp(138990020L);
+    }
+
+    @Test
+    public void getPlanes_returnsPlanesForWrappedImage() {
+        when(mBaseImageProxy.getPlanes())
+                .thenReturn(new ImageProxy.PlaneProxy[]{mYPlane, mUPlane, mVPlane});
+
+        ImageProxy.PlaneProxy[] planes = mImageProxy.getPlanes();
+        assertThat(planes.length).isEqualTo(3);
+        assertThat(planes[0]).isEqualTo(mYPlane);
+        assertThat(planes[1]).isEqualTo(mUPlane);
+        assertThat(planes[2]).isEqualTo(mVPlane);
+    }
+
+    @Test
+    public void getImage_returnsImageForWrappedImage() {
+        assertThat(mImageProxy.getImage()).isEqualTo(mBaseImageProxy.getImage());
+    }
+
+    private static final class ConcreteImageProxy extends ForwardingImageProxy {
+        private ConcreteImageProxy(ImageProxy baseImageProxy) {
+            super(baseImageProxy);
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerTest.java
new file mode 100644
index 0000000..6e9020e
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ForwardingImageReaderListenerTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ForwardingImageReaderListenerTest {
+    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 mImageReader = mock(ImageReader.class);
+    private final Surface mSurface = mock(Surface.class);
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private List<QueuedImageReaderProxy> mImageReaderProxies;
+    private ForwardingImageReaderListener mForwardingListener;
+
+    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(final Semaphore semaphore) {
+        return new ImageReaderProxy.OnImageAvailableListener() {
+            @Override
+            public void onImageAvailable(ImageReaderProxy imageReaderProxy) {
+                imageReaderProxy.acquireNextImage().close();
+                semaphore.release();
+            }
+        };
+    }
+
+    @Before
+    public void setUp() {
+        mHandlerThread = new HandlerThread("listener");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mImageReaderProxies = new ArrayList<>(3);
+        for (int i = 0; i < 3; ++i) {
+            mImageReaderProxies.add(
+                    new QueuedImageReaderProxy(
+                            IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_FORMAT, MAX_IMAGES, mSurface));
+        }
+        mForwardingListener = new ForwardingImageReaderListener(mImageReaderProxies);
+    }
+
+    @After
+    public void tearDown() {
+        mHandlerThread.quitSafely();
+    }
+
+    @Test
+    public void newImageIsForwardedToAllListeners() {
+        Image baseImage = createMockImage();
+        when(mImageReader.acquireNextImage()).thenReturn(baseImage);
+        List<ImageReaderProxy.OnImageAvailableListener> listeners = new ArrayList<>();
+        for (ImageReaderProxy imageReaderProxy : mImageReaderProxies) {
+            ImageReaderProxy.OnImageAvailableListener listener = createMockListener();
+            imageReaderProxy.setOnImageAvailableListener(listener, mHandler);
+            listeners.add(listener);
+        }
+
+        final int availableImages = 5;
+        for (int i = 0; i < availableImages; ++i) {
+            mForwardingListener.onImageAvailable(mImageReader);
+        }
+
+        for (int i = 0; i < mImageReaderProxies.size(); ++i) {
+            // Listener should be notified about every available image.
+            verify(listeners.get(i), timeout(2000).times(availableImages))
+                    .onImageAvailable(mImageReaderProxies.get(i));
+        }
+    }
+
+    @Test
+    public void baseImageIsClosed_allQueuesAreCleared_whenAllForwardedCopiesAreClosed()
+            throws InterruptedException {
+        Semaphore  Semaphore(/*permits=*/ 0);
+        Image baseImage = createMockImage();
+        when(mImageReader.acquireNextImage()).thenReturn(baseImage);
+        for (ImageReaderProxy imageReaderProxy : mImageReaderProxies) {
+            // Close the image for every listener.
+            imageReaderProxy.setOnImageAvailableListener(
+                    createSemaphoreReleasingClosingListener(onCloseSemaphore), mHandler);
+        }
+
+        final int availableImages = 5;
+        for (int i = 0; i < availableImages; ++i) {
+            mForwardingListener.onImageAvailable(mImageReader);
+        }
+        onCloseSemaphore.acquire(availableImages * mImageReaderProxies.size());
+
+        // Base image should be closed every time.
+        verify(baseImage, times(availableImages)).close();
+        // All queues should be cleared.
+        for (QueuedImageReaderProxy imageReaderProxy : mImageReaderProxies) {
+            assertThat(imageReaderProxy.getCurrentImages()).isEqualTo(0);
+        }
+    }
+
+    @Test
+    public void baseImageIsNotClosed_someQueuesAreCleared_whenNotAllForwardedCopiesAreClosed()
+            throws InterruptedException {
+        Semaphore  Semaphore(/*permits=*/ 0);
+        Image baseImage = createMockImage();
+        when(mImageReader.acquireNextImage()).thenReturn(baseImage);
+        // Don't close the image for the first listener.
+        mImageReaderProxies.get(0).setOnImageAvailableListener(createMockListener(), mHandler);
+        // Close the image for the other listeners.
+        mImageReaderProxies
+                .get(1)
+                .setOnImageAvailableListener(
+                        createSemaphoreReleasingClosingListener(onCloseSemaphore), mHandler);
+        mImageReaderProxies
+                .get(2)
+                .setOnImageAvailableListener(
+                        createSemaphoreReleasingClosingListener(onCloseSemaphore), mHandler);
+
+        final int availableImages = 5;
+        for (int i = 0; i < availableImages; ++i) {
+            mForwardingListener.onImageAvailable(mImageReader);
+        }
+        onCloseSemaphore.acquire(availableImages * (mImageReaderProxies.size() - 1));
+
+        // Base image should not be closed every time.
+        verify(baseImage, never()).close();
+        // First reader's queue should not be cleared.
+        assertThat(mImageReaderProxies.get(0).getCurrentImages()).isEqualTo(availableImages);
+        // Other readers' queues should be cleared.
+        assertThat(mImageReaderProxies.get(1).getCurrentImages()).isEqualTo(0);
+        assertThat(mImageReaderProxies.get(2).getCurrentImages()).isEqualTo(0);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerTest.java
new file mode 100644
index 0000000..7dcf79a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageProxyDownsamplerTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ImageProxyDownsamplerTest {
+    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(
+            final int width, final int height, final int pixelStride, final int initialValue) {
+        return new ImageProxy.PlaneProxy() {
+            final ByteBuffer mBuffer =
+                    createBufferWithRampPattern(width, height, pixelStride, initialValue);
+
+            @Override
+            public int getRowStride() {
+                return width * pixelStride;
+            }
+
+            @Override
+            public int getPixelStride() {
+                return pixelStride;
+            }
+
+            @Override
+            public ByteBuffer getBuffer() {
+                return mBuffer;
+            }
+        };
+    }
+
+    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/ImageSaverTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java
new file mode 100644
index 0000000..3cce59c
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImageSaverTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.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.graphics.Rect;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Base64;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageSaver.OnImageSavedListener;
+import androidx.camera.core.ImageSaver.SaveError;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ImageSaverTest {
+
+    private static final int WIDTH = 160;
+    private static final int HEIGHT = 120;
+    private static final int CROP_WIDTH = 100;
+    private static final int CROP_HEIGHT = 100;
+    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;
+    private static final String JPEG_IMAGE_DATA_BASE_64 =
+            "/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=";
+    // The image used here has a YUV_420_888 format.
+    @Mock
+    private final ImageProxy mMockYuvImage = mock(ImageProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy mYPlane = mock(ImageProxy.PlaneProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy mUPlane = mock(ImageProxy.PlaneProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy mVPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ByteBuffer mYBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
+    private final ByteBuffer mUBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+    private final ByteBuffer mVBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+    @Mock
+    private final ImageProxy mMockJpegImage = mock(ImageProxy.class);
+    @Mock
+    private final ImageProxy.PlaneProxy mJpegDataPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ByteBuffer mJpegDataBuffer =
+            ByteBuffer.wrap(Base64.decode(JPEG_IMAGE_DATA_BASE_64, Base64.DEFAULT));
+
+    private final Semaphore mSemaphore = new Semaphore(0);
+    private final ImageSaver.OnImageSavedListener mMockListener =
+            mock(ImageSaver.OnImageSavedListener.class);
+    private final ImageSaver.OnImageSavedListener mSyncListener =
+            new OnImageSavedListener() {
+                @Override
+                public void onImageSaved(File file) {
+                    mMockListener.onImageSaved(file);
+                    mSemaphore.release();
+                }
+
+                @Override
+                public void onError(
+                        SaveError saveError, String message, @Nullable Throwable cause) {
+                    mMockListener.onError(saveError, message, cause);
+                    mSemaphore.release();
+                }
+            };
+
+    private HandlerThread mBackgroundThread;
+    private Handler mBackgroundHandler;
+
+    @Before
+    public void setup() {
+        // The YUV image's behavior.
+        when(mMockYuvImage.getFormat()).thenReturn(ImageFormat.YUV_420_888);
+        when(mMockYuvImage.getWidth()).thenReturn(WIDTH);
+        when(mMockYuvImage.getHeight()).thenReturn(HEIGHT);
+
+        when(mYPlane.getBuffer()).thenReturn(mYBuffer);
+        when(mYPlane.getPixelStride()).thenReturn(Y_PIXEL_STRIDE);
+        when(mYPlane.getRowStride()).thenReturn(Y_ROW_STRIDE);
+
+        when(mUPlane.getBuffer()).thenReturn(mUBuffer);
+        when(mUPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
+        when(mUPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
+
+        when(mVPlane.getBuffer()).thenReturn(mVBuffer);
+        when(mVPlane.getPixelStride()).thenReturn(UV_PIXEL_STRIDE);
+        when(mVPlane.getRowStride()).thenReturn(UV_ROW_STRIDE);
+        when(mMockYuvImage.getPlanes())
+                .thenReturn(new ImageProxy.PlaneProxy[]{mYPlane, mUPlane, mVPlane});
+        when(mMockYuvImage.getCropRect()).thenReturn(new Rect(0, 0, CROP_WIDTH, CROP_HEIGHT));
+
+        // The JPEG image's behavior
+        when(mMockJpegImage.getFormat()).thenReturn(ImageFormat.JPEG);
+        when(mMockJpegImage.getWidth()).thenReturn(WIDTH);
+        when(mMockJpegImage.getHeight()).thenReturn(HEIGHT);
+        when(mMockJpegImage.getCropRect()).thenReturn(new Rect(0, 0, CROP_WIDTH, CROP_HEIGHT));
+
+        when(mJpegDataPlane.getBuffer()).thenReturn(mJpegDataBuffer);
+        when(mMockJpegImage.getPlanes()).thenReturn(new ImageProxy.PlaneProxy[]{mJpegDataPlane});
+
+        // Set up a background thread/handler for callbacks
+        mBackgroundThread = new HandlerThread("CallbackThread");
+        mBackgroundThread.start();
+        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+    }
+
+    @After
+    public void tearDown() {
+        mBackgroundThread.quitSafely();
+    }
+
+    private ImageSaver getDefaultImageSaver(ImageProxy image, File file) {
+        return new ImageSaver(
+                image,
+                file,
+                /*orientation=*/ 0,
+                /*reversedHorizontal=*/ false,
+                /*reversedVertical=*/ false,
+                /*location=*/ null,
+                mSyncListener,
+                mBackgroundHandler);
+    }
+
+    @Test
+    public void canSaveYuvImage() throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        ImageSaver imageSaver = getDefaultImageSaver(mMockYuvImage, saveLocation);
+
+        imageSaver.run();
+
+        mSemaphore.acquire();
+
+        verify(mMockListener).onImageSaved(any(File.class));
+    }
+
+    @Test
+    public void canSaveJpegImage() throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        ImageSaver imageSaver = getDefaultImageSaver(mMockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        mSemaphore.acquire();
+
+        verify(mMockListener).onImageSaved(any(File.class));
+    }
+
+    @Test
+    public void errorCallbackWillBeCalledOnInvalidPath() throws InterruptedException {
+        // Invalid filename should cause error
+        File saveLocation = new File("/not/a/real/path.jpg");
+
+        ImageSaver imageSaver = getDefaultImageSaver(mMockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        mSemaphore.acquire();
+
+        verify(mMockListener).onError(eq(SaveError.FILE_IO_FAILED), anyString(),
+                any(Throwable.class));
+    }
+
+    @Test
+    public void imageIsClosedOnSuccess() throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        ImageSaver imageSaver = getDefaultImageSaver(mMockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        mSemaphore.acquire();
+
+        verify(mMockJpegImage).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(mMockJpegImage, saveLocation);
+
+        imageSaver.run();
+
+        mSemaphore.acquire();
+
+        verify(mMockJpegImage).close();
+    }
+
+    private void imageCanBeCropped(ImageProxy image) throws InterruptedException, IOException {
+        File saveLocation = File.createTempFile("test", ".jpg");
+        saveLocation.deleteOnExit();
+
+        ImageSaver imageSaver =
+                new ImageSaver(
+                        image,
+                        saveLocation,
+                        /*orientation=*/ 0,
+                        /*reversedHorizontal=*/ false,
+                        /*reversedVertical=*/ false,
+                        /*location=*/ null,
+                        mSyncListener,
+                        mBackgroundHandler);
+        imageSaver.run();
+
+        mSemaphore.acquire();
+
+        Bitmap bitmap = BitmapFactory.decodeFile(saveLocation.getPath());
+        assertThat(bitmap.getWidth()).isEqualTo(bitmap.getHeight());
+    }
+
+    @Test
+    public void jpegImageCanBeCropped() throws InterruptedException, IOException {
+        imageCanBeCropped(mMockJpegImage);
+    }
+
+    @Test
+    public void yuvImageCanBeCropped() throws InterruptedException, IOException {
+        imageCanBeCropped(mMockYuvImage);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceTest.java
new file mode 100644
index 0000000..cbc2029
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ImmediateSurfaceTest.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 com.google.common.truth.Truth.assertThat;
+
+import android.view.Surface;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ImmediateSurfaceTest {
+    private Surface mMockSurface = Mockito.mock(Surface.class);
+
+    @Test
+    public void getSurface_returnsInstance() throws ExecutionException, InterruptedException {
+        ImmediateSurface immediateSurface = new ImmediateSurface(mMockSurface);
+
+        ListenableFuture<Surface> surfaceListenableFuture = immediateSurface.getSurface();
+
+        assertThat(surfaceListenableFuture.get()).isSameAs(mMockSurface);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorTest.java b/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorTest.java
new file mode 100644
index 0000000..a9092e4
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/IoExecutorTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class IoExecutorTest {
+
+    private Executor mIoExecutor;
+    private Lock mLock = new ReentrantLock();
+    private Condition mCondition = mLock.newCondition();
+    @GuardedBy("lock")
+    private RunnableState mState = RunnableState.CLEAR;
+    private final Runnable mRunnable1 =
+            new Runnable() {
+                @Override
+                public void run() {
+                    mLock.lock();
+                    try {
+                        mState = RunnableState.RUNNABLE1_WAITING;
+                        mCondition.signalAll();
+                        while (mState != RunnableState.CLEAR) {
+                            mCondition.await();
+                        }
+
+                        mState = RunnableState.RUNNABLE1_FINISHED;
+                        mCondition.signalAll();
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException("Thread interrupted unexpectedly", e);
+                    } finally {
+                        mLock.unlock();
+                    }
+                }
+            };
+    private final Runnable mRunnable2 =
+            new Runnable() {
+                @Override
+                public void run() {
+                    mLock.lock();
+                    try {
+                        while (mState != RunnableState.RUNNABLE1_WAITING) {
+                            mCondition.await();
+                        }
+
+                        mState = RunnableState.RUNNABLE2_FINISHED;
+                        mCondition.signalAll();
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException("Thread interrupted unexpectedly", e);
+                    } finally {
+                        mLock.unlock();
+                    }
+                }
+            };
+    private final Runnable mSimpleRunnable1 =
+            new Runnable() {
+                @Override
+                public void run() {
+                    mLock.lock();
+                    try {
+                        mState = RunnableState.RUNNABLE1_FINISHED;
+                        mCondition.signalAll();
+                    } finally {
+                        mLock.unlock();
+                    }
+                }
+            };
+
+    @Before
+    public void setup() {
+        mLock.lock();
+        try {
+            mState = RunnableState.CLEAR;
+        } finally {
+            mLock.unlock();
+        }
+        mIoExecutor = IoExecutor.getInstance();
+    }
+
+    @Test
+    public void canRunRunnable() throws InterruptedException {
+        mIoExecutor.execute(mSimpleRunnable1);
+        mLock.lock();
+        try {
+            while (mState != RunnableState.RUNNABLE1_FINISHED) {
+                mCondition.await();
+            }
+        } finally {
+            mLock.unlock();
+        }
+
+        // No need to check anything here. Completing this method should signal success.
+    }
+
+    @Test
+    public void canRunMultipleRunnableInParallel() throws InterruptedException {
+        mIoExecutor.execute(mRunnable1);
+        mIoExecutor.execute(mRunnable2);
+
+        mLock.lock();
+        try {
+            // mRunnable2 cannot finish until mRunnable1 has started
+            while (mState != RunnableState.RUNNABLE2_FINISHED) {
+                mCondition.await();
+            }
+
+            // Allow mRunnable1 to finish
+            mState = RunnableState.CLEAR;
+            mCondition.signalAll();
+
+            while (mState != RunnableState.RUNNABLE1_FINISHED) {
+                mCondition.await();
+            }
+        } finally {
+            mLock.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/MetadataImageReaderTest.java b/camera/core/src/androidTest/java/androidx/camera/core/MetadataImageReaderTest.java
new file mode 100644
index 0000000..4ce2caf
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/MetadataImageReaderTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.TestCase.fail;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.camera.testing.fakes.FakeCameraCaptureResult;
+import androidx.camera.testing.fakes.FakeImageProxy;
+import androidx.camera.testing.fakes.FakeImageReaderProxy;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public final class MetadataImageReaderTest {
+    private static final long TIMESTAMP_0 = 0L;
+    private static final long TIMESTAMP_1 = 1000L;
+    private static final long TIMESTAMP_NONEXISTANT = 5000L;
+    private final FakeImageReaderProxy mImageReader = new FakeImageReaderProxy();
+    private final FakeCameraCaptureResult mCameraCaptureResult0 = new FakeCameraCaptureResult();
+    private final FakeCameraCaptureResult mCameraCaptureResult1 = new FakeCameraCaptureResult();
+    private final Semaphore mSemaphore = new Semaphore(0);
+    private HandlerThread mBackgroundThread;
+    private Handler mBackgroundHandler;
+    private MetadataImageReader mMetadataImageReader;
+
+    @Before
+    public void setUp() {
+        mBackgroundThread = new HandlerThread("CallbackThread");
+        mBackgroundThread.start();
+        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+
+        createMetadataImageReaderWithCapacity(8);
+        mCameraCaptureResult0.setTimestamp(TIMESTAMP_0);
+        mCameraCaptureResult1.setTimestamp(TIMESTAMP_1);
+    }
+
+    @Test(timeout = 500)
+    public void canBindImageToImageInfoWithSameTimestamp() throws InterruptedException {
+        // Triggers CaptureCompleted with two different CaptureResult.
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult0);
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult1);
+
+        ImageReaderProxy.OnImageAvailableListener outputListener =
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // Checks if the output Image has ImageInfo with same timestamp.
+                        ImageProxy resultImage = imageReader.acquireNextImage();
+                        assertThat(resultImage.getTimestamp()).isEqualTo(TIMESTAMP_0);
+                        assertThat(resultImage.getImageInfo().getTimestamp()).isEqualTo(
+                                TIMESTAMP_0);
+                        mSemaphore.release();
+                    }
+                };
+        mMetadataImageReader.setOnImageAvailableListener(outputListener, mBackgroundHandler);
+        // Triggers ImageAvailable with one Image.
+        triggerImageAvailable(TIMESTAMP_0);
+
+        mSemaphore.acquire();
+
+        outputListener =
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // Checks if the MetadataImageReader can output the other matched
+                        // ImageProxy.
+                        ImageProxy resultImage = imageReader.acquireNextImage();
+                        assertThat(resultImage.getTimestamp()).isEqualTo(TIMESTAMP_1);
+                        assertThat(resultImage.getImageInfo().getTimestamp()).isEqualTo(
+                                TIMESTAMP_1);
+                        mSemaphore.release();
+                    }
+                };
+        mMetadataImageReader.setOnImageAvailableListener(outputListener, mBackgroundHandler);
+        // Triggers ImageAvailable with another Image with different timestamp.
+        triggerImageAvailable(TIMESTAMP_1);
+        mSemaphore.acquire();
+    }
+
+    @Test(timeout = 500)
+    public void canBindImageInfoToImageWithSameTimestamp() throws InterruptedException {
+        // Triggers ImageAvailable with two different Image.
+        triggerImageAvailable(TIMESTAMP_0);
+        triggerImageAvailable(TIMESTAMP_1);
+
+        ImageReaderProxy.OnImageAvailableListener outputListener =
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // Checks if the output contains the first matched ImageProxy.
+                        ImageProxy resultImage = imageReader.acquireNextImage();
+                        assertThat(resultImage.getTimestamp()).isEqualTo(TIMESTAMP_0);
+                        assertThat(resultImage.getImageInfo().getTimestamp()).isEqualTo(
+                                TIMESTAMP_0);
+                        mSemaphore.release();
+                    }
+                };
+        mMetadataImageReader.setOnImageAvailableListener(outputListener, mBackgroundHandler);
+        // Triggers CaptureCompleted with one CaptureResult.
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult0);
+        mSemaphore.acquire();
+
+        outputListener =
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // Checks if the MetadataImageReader can output the other ImageProxy.
+                        ImageProxy resultImage = imageReader.acquireNextImage();
+                        assertThat(resultImage.getTimestamp()).isEqualTo(TIMESTAMP_1);
+                        assertThat(resultImage.getImageInfo().getTimestamp()).isEqualTo(
+                                TIMESTAMP_1);
+                        mSemaphore.release();
+                    }
+                };
+        mMetadataImageReader.setOnImageAvailableListener(outputListener, mBackgroundHandler);
+        // Triggers CaptureCompleted with another CaptureResult.
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult1);
+        mSemaphore.acquire();
+    }
+
+    @Test(timeout = 500)
+    public void canNotFindAMatch() throws InterruptedException {
+        // Triggers CaptureCompleted with two different CaptureResult.
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult0);
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult1);
+
+        ImageReaderProxy.OnImageAvailableListener outputListener =
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // If the Metadata still get a match, fail the test case.
+                        fail("Match should not be found.");
+                        mSemaphore.release();
+                    }
+                };
+        mMetadataImageReader.setOnImageAvailableListener(outputListener, mBackgroundHandler);
+        // Triggers ImageAvailable with an Image which contains a timestamp doesn't match any of
+        // the CameraResult.
+        triggerImageAvailable(TIMESTAMP_NONEXISTANT);
+        // Waits for a period of time.
+        mSemaphore.tryAcquire(300, TimeUnit.MILLISECONDS);
+    }
+
+    @Test(timeout = 500)
+    public void maxImageHasBeenAcquired() throws InterruptedException {
+        // Creates a MetadataImageReader with only one capacity.
+        createMetadataImageReaderWithCapacity(1);
+
+        // Feeds two CaptureResult into it.
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult0);
+        mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(mCameraCaptureResult1);
+
+        ImageReaderProxy.OnImageAvailableListener outputListener =
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // The First ImageProxy is output without closing.
+                        ImageProxy resultImage = imageReader.acquireNextImage();
+                        assertThat(resultImage.getTimestamp()).isEqualTo(TIMESTAMP_0);
+                        assertThat(resultImage.getImageInfo().getTimestamp()).isEqualTo(
+                                TIMESTAMP_0);
+                        mSemaphore.release();
+                    }
+                };
+        mMetadataImageReader.setOnImageAvailableListener(outputListener, mBackgroundHandler);
+        // Feeds the first Image.
+        triggerImageAvailable(TIMESTAMP_0);
+        mSemaphore.acquire();
+
+        outputListener =
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // The second ImageProxy should be dropped, otherwise fail the test case.
+                        fail("Should not exceed maximum Image number.");
+                        mSemaphore.release();
+                    }
+                };
+        mMetadataImageReader.setOnImageAvailableListener(outputListener, mBackgroundHandler);
+        // Feeds the second Image.
+        triggerImageAvailable(TIMESTAMP_1);
+        // Waits for a time period.
+        mSemaphore.tryAcquire(300, TimeUnit.MILLISECONDS);
+    }
+
+    private void createMetadataImageReaderWithCapacity(int maxImages) {
+        mImageReader.setMaxImages(maxImages);
+        mMetadataImageReader = new MetadataImageReader(mImageReader, null);
+    }
+
+    private void triggerImageAvailable(long timestamp) {
+        FakeImageProxy image = new FakeImageProxy();
+        image.setTimestamp(timestamp);
+        mImageReader.setImageProxy(image);
+        mImageReader.triggerImageAvailable();
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderTest.java
new file mode 100644
index 0000000..c64fa03
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.lang.Thread.sleep;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.Surface;
+
+import androidx.camera.testing.fakes.FakeCaptureStage;
+import androidx.camera.testing.fakes.FakeImageInfo;
+import androidx.camera.testing.fakes.FakeImageProxy;
+import androidx.camera.testing.fakes.FakeImageReaderProxy;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public final class ProcessingImageReaderTest {
+    private static final int CAPTURE_ID_0 = 0;
+    private static final int CAPTURE_ID_1 = 1;
+    private static final long TIMESTAMP_0 = 0L;
+    private static final long TIMESTAMP_1 = 1000L;
+    private final CaptureStage mCaptureStage0 = new FakeCaptureStage(CAPTURE_ID_0, null);
+    private final CaptureStage mCaptureStage1 = new FakeCaptureStage(CAPTURE_ID_1, null);
+    private final FakeImageReaderProxy mImageReaderProxy = new FakeImageReaderProxy();
+    private final CaptureBundle mCaptureBundle = new CaptureBundle();
+    private final CaptureProcessor mCaptureProcessor = new CaptureProcessor() {
+        @Override
+        public void onOutputSurface(Surface surface, int imageFormat) {
+
+        }
+
+        @Override
+        public void process(ImageProxyBundle bundle) {
+
+        }
+    };
+    private final Semaphore mSemaphore = new Semaphore(0);
+    private ProcessingImageReader mProcessingImageReader;
+    private HandlerThread mBackgroundThread;
+    private Handler mBackgroundHandler;
+
+    @Before
+    public void setUp() {
+        mBackgroundThread = new HandlerThread("CallbackThread");
+        mBackgroundThread.start();
+        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
+    }
+
+    @Test
+    public void canSetFuturesInSettableImageProxyBundle() throws InterruptedException {
+        mCaptureBundle.addCaptureStage(mCaptureStage0);
+        mCaptureBundle.addCaptureStage(mCaptureStage1);
+        // Sets the callback from ProcessingImageReader to start processing
+        CaptureProcessor captureProcessor = new CaptureProcessor() {
+            @Override
+            public void onOutputSurface(Surface surface, int imageFormat) {
+
+            }
+
+            @Override
+            public void process(ImageProxyBundle bundle) {
+                try {
+                    // CaptureProcessor.process should be called once all ImageProxies on the
+                    // initial lists are ready. Then checks if the output has matched timestamp.
+                    assertThat(bundle.getImageProxy(CAPTURE_ID_0).get(0,
+                            TimeUnit.SECONDS).getTimestamp()).isEqualTo(TIMESTAMP_0);
+                    assertThat(bundle.getImageProxy(CAPTURE_ID_1).get(0,
+                            TimeUnit.SECONDS).getTimestamp()).isEqualTo(TIMESTAMP_1);
+                } catch (Exception e) {
+
+                }
+                mSemaphore.release();
+            }
+        };
+        mProcessingImageReader = new ProcessingImageReader(
+                mImageReaderProxy, mBackgroundHandler, mCaptureBundle, captureProcessor);
+
+        // Feeds ImageProxy with all capture id on the initial list.
+        triggerImageAvailable(CAPTURE_ID_0, TIMESTAMP_0);
+        sleep(500);
+        triggerImageAvailable(CAPTURE_ID_1, TIMESTAMP_1);
+
+        mSemaphore.acquire();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void imageReaderSizeIsSmallerThanCaptureBundle() {
+        ImageReaderProxy imageReaderProxy = new FakeImageReaderProxy();
+
+        // Creates a ProcessingImageReader with maximum Image number smaller than CaptureBundle
+        // size.
+        ((FakeImageReaderProxy) imageReaderProxy).setMaxImages(1);
+        mCaptureBundle.addCaptureStage(mCaptureStage0);
+        mCaptureBundle.addCaptureStage(mCaptureStage1);
+
+        // Expects to throw exception when creating ProcessingImageReader.
+        mProcessingImageReader = new ProcessingImageReader(imageReaderProxy, mBackgroundHandler,
+                mCaptureBundle, mCaptureProcessor);
+    }
+
+    private void triggerImageAvailable(int captureId, long timestamp) {
+        FakeImageProxy image = new FakeImageProxy();
+        FakeImageInfo imageInfo = new FakeImageInfo();
+        imageInfo.setTag(captureId);
+        imageInfo.setTimestamp(timestamp);
+        image.setImageInfo(imageInfo);
+        image.setTimestamp(timestamp);
+        mImageReaderProxy.setImageProxy(image);
+        mImageReaderProxy.triggerImageAvailable();
+    }
+
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyTest.java b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyTest.java
new file mode 100644
index 0000000..21403c2
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/QueuedImageReaderProxyTest.java
@@ -0,0 +1,322 @@
+/*
+ * 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.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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class QueuedImageReaderProxyTest {
+    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 mSurface = mock(Surface.class);
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private QueuedImageReaderProxy mImageReaderProxy;
+
+    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(
+            final Semaphore semaphore) {
+        ConcreteImageProxy image = createForwardingImageProxy();
+        image.addOnImageCloseListener(
+                new ForwardingImageProxy.OnImageCloseListener() {
+                    @Override
+                    public void onImageClose(ImageProxy closedImage) {
+                        semaphore.release();
+                    }
+                });
+        return image;
+    }
+
+    private static ConcreteImageProxy createForwardingImageProxy() {
+        return new ConcreteImageProxy(createMockImageProxy());
+    }
+
+    @Before
+    public void setUp() {
+        mHandlerThread = new HandlerThread("background");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mImageReaderProxy =
+                new QueuedImageReaderProxy(
+                        IMAGE_WIDTH, IMAGE_HEIGHT, IMAGE_FORMAT, MAX_IMAGES, mSurface);
+    }
+
+    @After
+    public void tearDown() {
+        mHandlerThread.quitSafely();
+    }
+
+    @Test
+    public void enqueueImage_incrementsQueueSize() {
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+
+        assertThat(mImageReaderProxy.getCurrentImages()).isEqualTo(2);
+    }
+
+    @Test
+    public void enqueueImage_doesNotIncreaseSizeBeyondMaxImages() {
+        // Exceed the queue's capacity by 2.
+        for (int i = 0; i < MAX_IMAGES + 2; ++i) {
+            mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+        }
+
+        assertThat(mImageReaderProxy.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());
+            mImageReaderProxy.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
+    public void closedImages_reduceQueueSize() throws InterruptedException {
+        // Fill up to the queue's capacity.
+        Semaphore  Semaphore(/*permits=*/ 0);
+        for (int i = 0; i < MAX_IMAGES; ++i) {
+            ForwardingImageProxy image =
+                    createSemaphoreReleasingOnCloseImageProxy(onCloseSemaphore);
+            mImageReaderProxy.enqueueImage(image);
+        }
+
+        mImageReaderProxy.acquireNextImage().close();
+        mImageReaderProxy.acquireNextImage().close();
+        onCloseSemaphore.acquire(/*permits=*/ 2);
+
+        assertThat(mImageReaderProxy.getCurrentImages()).isEqualTo(MAX_IMAGES - 2);
+    }
+
+    @Test
+    public void closedImage_allowsNewImageToBeEnqueued() throws InterruptedException {
+        // Fill up to the queue's capacity.
+        Semaphore  Semaphore(/*permits=*/ 0);
+        for (int i = 0; i < MAX_IMAGES; ++i) {
+            ForwardingImageProxy image =
+                    createSemaphoreReleasingOnCloseImageProxy(onCloseSemaphore);
+            mImageReaderProxy.enqueueImage(image);
+        }
+
+        mImageReaderProxy.acquireNextImage().close();
+        onCloseSemaphore.acquire();
+
+        ConcreteImageProxy lastImageProxy = createForwardingImageProxy();
+        mImageReaderProxy.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);
+        mImageReaderProxy.setOnImageAvailableListener(listener, mHandler);
+
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+
+        verify(listener, timeout(2000).times(2)).onImageAvailable(mImageReaderProxy);
+    }
+
+    @Test
+    public void acquireLatestImage_returnsNull_whenQueueIsEmpty() {
+        assertThat(mImageReaderProxy.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());
+            mImageReaderProxy.enqueueImage(images.get(i));
+        }
+
+        ImageProxy lastImage = images.get(availableImages - 1);
+        assertThat(mImageReaderProxy.acquireLatestImage()).isEqualTo(lastImage);
+        assertThat(mImageReaderProxy.getCurrentImages()).isEqualTo(1);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void acquireLatestImage_throwsException_whenAllImagesWerePreviouslyAcquired() {
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+        mImageReaderProxy.acquireNextImage();
+
+        // Should throw IllegalStateException
+        mImageReaderProxy.acquireLatestImage();
+    }
+
+    @Test
+    public void acquireNextImage_returnsNull_whenQueueIsEmpty() {
+        assertThat(mImageReaderProxy.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());
+            mImageReaderProxy.enqueueImage(images.get(i));
+        }
+
+        for (int i = 0; i < availableImages; ++i) {
+            assertThat(mImageReaderProxy.acquireNextImage()).isEqualTo(images.get(i));
+        }
+        assertThat(mImageReaderProxy.getCurrentImages()).isEqualTo(availableImages);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void acquireNextImage_throwsException_whenAllImagesWerePreviouslyAcquired() {
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+        mImageReaderProxy.acquireNextImage();
+
+        // Should throw IllegalStateException
+        mImageReaderProxy.acquireNextImage();
+    }
+
+    @Test
+    public void close_closesAnyImagesStillInQueue() {
+        ConcreteImageProxy image0 = createForwardingImageProxy();
+        ConcreteImageProxy image1 = createForwardingImageProxy();
+        mImageReaderProxy.enqueueImage(image0);
+        mImageReaderProxy.enqueueImage(image1);
+
+        mImageReaderProxy.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);
+        mImageReaderProxy.addOnReaderCloseListener(listenerA);
+        mImageReaderProxy.addOnReaderCloseListener(listenerB);
+
+        mImageReaderProxy.close();
+
+        verify(listenerA, times(1)).onReaderClose(mImageReaderProxy);
+        verify(listenerB, times(1)).onReaderClose(mImageReaderProxy);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void acquireLatestImage_throwsException_afterReaderIsClosed() {
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+        mImageReaderProxy.close();
+
+        // Should throw IllegalStateException
+        mImageReaderProxy.acquireLatestImage();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void acquireNextImage_throwsException_afterReaderIsClosed() {
+        mImageReaderProxy.enqueueImage(createForwardingImageProxy());
+        mImageReaderProxy.close();
+
+        // Should throw IllegalStateException
+        mImageReaderProxy.acquireNextImage();
+    }
+
+    @Test
+    public void getHeight_returnsFixedHeight() {
+        assertThat(mImageReaderProxy.getHeight()).isEqualTo(IMAGE_HEIGHT);
+    }
+
+    @Test
+    public void getWidth_returnsFixedWidth() {
+        assertThat(mImageReaderProxy.getWidth()).isEqualTo(IMAGE_WIDTH);
+    }
+
+    @Test
+    public void getImageFormat_returnsFixedFormat() {
+        assertThat(mImageReaderProxy.getImageFormat()).isEqualTo(IMAGE_FORMAT);
+    }
+
+    @Test
+    public void getMaxImages_returnsFixedCapacity() {
+        assertThat(mImageReaderProxy.getMaxImages()).isEqualTo(MAX_IMAGES);
+    }
+
+    private static final class ConcreteImageProxy extends ForwardingImageProxy {
+        private boolean mIsClosed = false;
+
+        ConcreteImageProxy(ImageProxy image) {
+            super(image);
+        }
+
+        @Override
+        public synchronized void close() {
+            super.close();
+            mIsClosed = true;
+        }
+
+        public synchronized boolean isClosed() {
+            return mIsClosed;
+        }
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyTest.java b/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyTest.java
new file mode 100644
index 0000000..de9ba46
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/ReferenceCountedImageProxyTest.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 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ReferenceCountedImageProxyTest {
+    private static final int WIDTH = 640;
+    private static final int HEIGHT = 480;
+
+    // Assume the image has YUV_420_888 format.
+    private final ImageProxy mImage = mock(ImageProxy.class);
+    private final ImageProxy.PlaneProxy mYPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy mUPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ImageProxy.PlaneProxy mVPlane = mock(ImageProxy.PlaneProxy.class);
+    private final ByteBuffer mYBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT);
+    private final ByteBuffer mUBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+    private final ByteBuffer mVBuffer = ByteBuffer.allocateDirect(WIDTH * HEIGHT / 4);
+    private ReferenceCountedImageProxy mImageProxy;
+
+    @Before
+    public void setUp() {
+        when(mImage.getWidth()).thenReturn(WIDTH);
+        when(mImage.getHeight()).thenReturn(HEIGHT);
+        when(mYPlane.getBuffer()).thenReturn(mYBuffer);
+        when(mUPlane.getBuffer()).thenReturn(mUBuffer);
+        when(mVPlane.getBuffer()).thenReturn(mVBuffer);
+        when(mImage.getPlanes()).thenReturn(new ImageProxy.PlaneProxy[]{mYPlane, mUPlane, mVPlane});
+        mImageProxy = new ReferenceCountedImageProxy(mImage);
+    }
+
+    @Test
+    public void getReferenceCount_returnsOne_afterConstruction() {
+        assertThat(mImageProxy.getReferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void fork_incrementsReferenceCount() {
+        mImageProxy.fork();
+        mImageProxy.fork();
+
+        assertThat(mImageProxy.getReferenceCount()).isEqualTo(3);
+    }
+
+    @Test
+    public void close_decrementsReferenceCount() {
+        ImageProxy forkedImage0 = mImageProxy.fork();
+        ImageProxy forkedImage1 = mImageProxy.fork();
+
+        forkedImage0.close();
+        forkedImage1.close();
+
+        assertThat(mImageProxy.getReferenceCount()).isEqualTo(1);
+        verify(mImage, never()).close();
+    }
+
+    @Test
+    public void close_closesBaseImage_whenReferenceCountHitsZero() {
+        ImageProxy forkedImage0 = mImageProxy.fork();
+        ImageProxy forkedImage1 = mImageProxy.fork();
+
+        forkedImage0.close();
+        forkedImage1.close();
+        mImageProxy.close();
+
+        assertThat(mImageProxy.getReferenceCount()).isEqualTo(0);
+        verify(mImage, times(1)).close();
+    }
+
+    @Test
+    public void close_decrementsReferenceCountOnlyOnce() {
+        ImageProxy forkedImage = mImageProxy.fork();
+
+        forkedImage.close();
+        forkedImage.close();
+
+        assertThat(mImageProxy.getReferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void fork_returnsNull_whenBaseImageIsClosed() {
+        mImageProxy.close();
+
+        ImageProxy forkedImage = mImageProxy.fork();
+
+        assertThat(forkedImage).isNull();
+    }
+
+    @Test
+    public void concurrentAccessForTwoForkedImagesOnTwoThreads() throws InterruptedException {
+        final ImageProxy forkedImage0 = mImageProxy.fork();
+        final ImageProxy forkedImage1 = mImageProxy.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/SessionConfigurationTest.java b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationTest.java
new file mode 100644
index 0000000..73258ea
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SessionConfigurationTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SessionConfigurationTest {
+    private DeferrableSurface mMockSurface0;
+    private DeferrableSurface mMockSurface1;
+
+    @Before
+    public void setup() {
+        mMockSurface0 = new ImmediateSurface(Mockito.mock(Surface.class));
+        mMockSurface1 = 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(mMockSurface0);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+
+        assertThat(surfaces).hasSize(1);
+        assertThat(surfaces).contains(mMockSurface0);
+    }
+
+    @Test
+    public void builderAddNonRepeatingSurface() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addNonRepeatingSurface(mMockSurface0);
+        SessionConfiguration sessionConfiguration = builder.build();
+
+        List<DeferrableSurface> surfaces = sessionConfiguration.getSurfaces();
+        List<DeferrableSurface> repeatingSurfaces =
+                sessionConfiguration.getCaptureRequestConfiguration().getSurfaces();
+
+        assertThat(surfaces).hasSize(1);
+        assertThat(surfaces).contains(mMockSurface0);
+        assertThat(repeatingSurfaces).isEmpty();
+        assertThat(repeatingSurfaces).doesNotContain(mMockSurface0);
+    }
+
+    @Test
+    public void builderAddSurfaceContainsRepeatingSurface() {
+        SessionConfiguration.Builder builder = new SessionConfiguration.Builder();
+
+        builder.addSurface(mMockSurface0);
+        builder.addNonRepeatingSurface(mMockSurface1);
+        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(mMockSurface0);
+        builder.removeSurface(mMockSurface0);
+        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(mMockSurface0);
+        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(mMockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mMockSurface1);
+        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(mMockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mMockSurface1);
+        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(mMockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mMockSurface1);
+        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(mMockSurface0, mMockSurface1);
+    }
+
+    @Test
+    public void combineTwoSessionsCharacteristics() {
+        SessionConfiguration.Builder builder0 = new SessionConfiguration.Builder();
+        builder0.addSurface(mMockSurface0);
+        builder0.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        builder0.addCharacteristic(
+                CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
+
+        SessionConfiguration.Builder builder1 = new SessionConfiguration.Builder();
+        builder1.addSurface(mMockSurface1);
+        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/SingleCloseImageProxyTest.java b/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyTest.java
new file mode 100644
index 0000000..04fed7a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/SingleCloseImageProxyTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class SingleCloseImageProxyTest {
+
+    private final ImageProxy mImageProxy = mock(ImageProxy.class);
+    private SingleCloseImageProxy mSingleCloseImageProxy;
+
+    @Before
+    public void setUp() {
+        mSingleCloseImageProxy = new SingleCloseImageProxy(mImageProxy);
+    }
+
+    @Test
+    public void wrappedImageIsClosedOnce_whenWrappingImageIsClosedOnce() {
+        mSingleCloseImageProxy.close();
+
+        verify(mImageProxy, times(1)).close();
+    }
+
+    @Test
+    public void wrappedImageIsClosedOnce_whenWrappingImageIsClosedTwice() {
+        mSingleCloseImageProxy.close();
+        mSingleCloseImageProxy.close();
+
+        verify(mImageProxy, times(1)).close();
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateTest.java
new file mode 100644
index 0000000..da5223a
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseAttachStateTest.java
@@ -0,0 +1,348 @@
+/*
+ * 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 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.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UseCaseAttachStateTest {
+    private final LensFacing mCameraLensFacing0 = LensFacing.BACK;
+    private final LensFacing mCameraLensFacing1 = LensFacing.FRONT;
+    private final CameraDevice mMockCameraDevice = Mockito.mock(CameraDevice.class);
+    private final CameraCaptureSession mMockCameraCaptureSession =
+            Mockito.mock(CameraCaptureSession.class);
+
+    private String mCameraId;
+
+    @Before
+    public void setUp() {
+        AppConfiguration appConfiguration = FakeAppConfiguration.create();
+        CameraFactory cameraFactory = appConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+        try {
+            mCameraId = 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(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+                .containsExactly(fakeUseCase.mSurface);
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mMockCameraDevice);
+        verify(fakeUseCase.mDeviceStateCallback, times(1)).onOpened(mMockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mMockCameraCaptureSession);
+        verify(fakeUseCase.mSessionStateCallback, times(1)).onConfigured(mMockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.mCameraCaptureCallback, times(1)).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setTwoUseCasesOnline() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration0 =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase0 = new TestUseCase(configuration0, mCameraId);
+        FakeUseCaseConfiguration configuration1 =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase1 = new TestUseCase(configuration1, mCameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase0);
+        useCaseAttachState.setUseCaseOnline(fakeUseCase1);
+
+        SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+                .containsExactly(fakeUseCase0.mSurface, fakeUseCase1.mSurface);
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mMockCameraDevice);
+        verify(fakeUseCase0.mDeviceStateCallback, times(1)).onOpened(mMockCameraDevice);
+        verify(fakeUseCase1.mDeviceStateCallback, times(1)).onOpened(mMockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mMockCameraCaptureSession);
+        verify(fakeUseCase0.mSessionStateCallback, times(1)).onConfigured(
+                mMockCameraCaptureSession);
+        verify(fakeUseCase1.mSessionStateCallback, times(1)).onConfigured(
+                mMockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase0.mCameraCaptureCallback, times(1)).onCaptureCompleted(null);
+        verify(fakeUseCase1.mCameraCaptureCallback, times(1)).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseActiveOnly() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+        useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder =
+                useCaseAttachState.getActiveAndOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mMockCameraDevice);
+        verify(fakeUseCase.mDeviceStateCallback, never()).onOpened(mMockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mMockCameraCaptureSession);
+        verify(fakeUseCase.mSessionStateCallback, never()).onConfigured(mMockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.mCameraCaptureCallback, never()).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseActiveAndOnline() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+        useCaseAttachState.setUseCaseActive(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder =
+                useCaseAttachState.getActiveAndOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(DeferrableSurfaces.surfaceList(sessionConfiguration.getSurfaces()))
+                .containsExactly(fakeUseCase.mSurface);
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mMockCameraDevice);
+        verify(fakeUseCase.mDeviceStateCallback, times(1)).onOpened(mMockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mMockCameraCaptureSession);
+        verify(fakeUseCase.mSessionStateCallback, times(1)).onConfigured(mMockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.mCameraCaptureCallback, times(1)).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseOffline() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+        useCaseAttachState.setUseCaseOffline(fakeUseCase);
+
+        SessionConfiguration.ValidatingBuilder builder = useCaseAttachState.getOnlineBuilder();
+        SessionConfiguration sessionConfiguration = builder.build();
+        assertThat(sessionConfiguration.getSurfaces()).isEmpty();
+
+        sessionConfiguration.getDeviceStateCallback().onOpened(mMockCameraDevice);
+        verify(fakeUseCase.mDeviceStateCallback, never()).onOpened(mMockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mMockCameraCaptureSession);
+        verify(fakeUseCase.mSessionStateCallback, never()).onConfigured(mMockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.mCameraCaptureCallback, never()).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void setUseCaseInactive() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+        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(mMockCameraDevice);
+        verify(fakeUseCase.mDeviceStateCallback, never()).onOpened(mMockCameraDevice);
+
+        sessionConfiguration.getSessionStateCallback().onConfigured(mMockCameraCaptureSession);
+        verify(fakeUseCase.mSessionStateCallback, never()).onConfigured(mMockCameraCaptureSession);
+
+        sessionConfiguration.getCameraCaptureCallback().onCaptureCompleted(null);
+        verify(fakeUseCase.mCameraCaptureCallback, never()).onCaptureCompleted(null);
+    }
+
+    @Test
+    public void updateUseCase() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing0)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+        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(mCameraId, 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(expected = IllegalArgumentException.class)
+    public void setUseCaseOnlineWithWrongCamera() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing1)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+
+        // Should throw IllegalArgumentException
+        useCaseAttachState.setUseCaseOnline(fakeUseCase);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setUseCaseActiveWithWrongCamera() {
+        UseCaseAttachState useCaseAttachState = new UseCaseAttachState(mCameraId);
+        FakeUseCaseConfiguration configuration =
+                new FakeUseCaseConfiguration.Builder()
+                        .setTargetName("UseCase")
+                        .setLensFacing(mCameraLensFacing1)
+                        .build();
+        TestUseCase fakeUseCase = new TestUseCase(configuration, mCameraId);
+
+        // Should throw IllegalArgumentException
+        useCaseAttachState.setUseCaseActive(fakeUseCase);
+    }
+
+    private static class TestUseCase extends FakeUseCase {
+        private final Surface mSurface = Mockito.mock(Surface.class);
+        private final CameraDevice.StateCallback mDeviceStateCallback =
+                Mockito.mock(CameraDevice.StateCallback.class);
+        private final CameraCaptureSession.StateCallback mSessionStateCallback =
+                Mockito.mock(CameraCaptureSession.StateCallback.class);
+        private final CameraCaptureCallback mCameraCaptureCallback =
+                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(mSurface));
+            builder.setDeviceStateCallback(mDeviceStateCallback);
+            builder.setSessionStateCallback(mSessionStateCallback);
+            builder.setCameraCaptureCallback(mCameraCaptureCallback);
+
+            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/UseCaseGroupLifecycleControllerTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerTest.java
new file mode 100644
index 0000000..98bf4ea
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupLifecycleControllerTest.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 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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class UseCaseGroupLifecycleControllerTest {
+    private final UseCaseGroup.StateChangeListener mMockListener =
+            Mockito.mock(UseCaseGroup.StateChangeListener.class);
+    private UseCaseGroupLifecycleController mUseCaseGroupLifecycleController;
+    private FakeLifecycleOwner mLifecycleOwner;
+
+    @Before
+    public void setUp() {
+        mLifecycleOwner = new FakeLifecycleOwner();
+    }
+
+    @Test
+    public void groupCanBeMadeObserverOfLifecycle() {
+        assertThat(mLifecycleOwner.getObserverCount()).isEqualTo(0);
+
+        mUseCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        mLifecycleOwner.getLifecycle(), new UseCaseGroup());
+
+        assertThat(mLifecycleOwner.getObserverCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void groupCanStopObservingALifeCycle() {
+        mUseCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        mLifecycleOwner.getLifecycle(), new UseCaseGroup());
+        assertThat(mLifecycleOwner.getObserverCount()).isEqualTo(1);
+
+        mUseCaseGroupLifecycleController.release();
+
+        assertThat(mLifecycleOwner.getObserverCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void groupCanBeReleasedMultipleTimes() {
+        mUseCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        mLifecycleOwner.getLifecycle(), new UseCaseGroup());
+
+        mUseCaseGroupLifecycleController.release();
+        mUseCaseGroupLifecycleController.release();
+    }
+
+    @Test
+    public void lifecycleStart_triggersOnActive() {
+        mUseCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        mLifecycleOwner.getLifecycle(), new UseCaseGroup());
+        mUseCaseGroupLifecycleController.getUseCaseGroup().setListener(mMockListener);
+
+        mLifecycleOwner.start();
+
+        verify(mMockListener, times(1))
+                .onGroupActive(mUseCaseGroupLifecycleController.getUseCaseGroup());
+    }
+
+    @Test
+    public void lifecycleStop_triggersOnInactive() {
+        mUseCaseGroupLifecycleController =
+                new UseCaseGroupLifecycleController(
+                        mLifecycleOwner.getLifecycle(), new UseCaseGroup());
+        mUseCaseGroupLifecycleController.getUseCaseGroup().setListener(mMockListener);
+        mLifecycleOwner.start();
+
+        mLifecycleOwner.stop();
+
+        verify(mMockListener, times(1))
+                .onGroupInactive(mUseCaseGroupLifecycleController.getUseCaseGroup());
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryTest.java
new file mode 100644
index 0000000..b874d09
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupRepositoryTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.FakeLifecycleOwner;
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseGroupRepositoryTest {
+
+    private FakeLifecycleOwner mLifecycle;
+    private UseCaseGroupRepository mRepository;
+    private Map<LifecycleOwner, UseCaseGroupLifecycleController> mUseCasesMap;
+
+    @Before
+    public void setUp() {
+        mLifecycle = new FakeLifecycleOwner();
+        mRepository = new UseCaseGroupRepository();
+        mUseCasesMap = mRepository.getUseCasesMap();
+    }
+
+    @Test
+    public void repositoryStartsEmpty() {
+        assertThat(mUseCasesMap).isEmpty();
+    }
+
+    @Test
+    public void newUseCaseGroupIsCreated_whenNoGroupExistsForLifecycleInRepository() {
+        UseCaseGroupLifecycleController group = mRepository.getOrCreateUseCaseGroup(mLifecycle);
+
+        assertThat(mUseCasesMap).containsExactly(mLifecycle, group);
+    }
+
+    @Test
+    public void existingUseCaseGroupIsReturned_whenGroupExistsForLifecycleInRepository() {
+        UseCaseGroupLifecycleController firstGroup = mRepository.getOrCreateUseCaseGroup(
+                mLifecycle);
+        UseCaseGroupLifecycleController secondGroup = mRepository.getOrCreateUseCaseGroup(
+                mLifecycle);
+
+        assertThat(firstGroup).isSameAs(secondGroup);
+        assertThat(mUseCasesMap).containsExactly(mLifecycle, firstGroup);
+    }
+
+    @Test
+    public void differentUseCaseGroupsAreCreated_forDifferentLifecycles() {
+        UseCaseGroupLifecycleController firstGroup = mRepository.getOrCreateUseCaseGroup(
+                mLifecycle);
+        FakeLifecycleOwner secondLifecycle = new FakeLifecycleOwner();
+        UseCaseGroupLifecycleController secondGroup =
+                mRepository.getOrCreateUseCaseGroup(secondLifecycle);
+
+        assertThat(mUseCasesMap)
+                .containsExactly(mLifecycle, firstGroup, secondLifecycle, secondGroup);
+    }
+
+    @Test
+    public void useCaseGroupObservesLifecycle() {
+        mRepository.getOrCreateUseCaseGroup(mLifecycle);
+
+        // One observer is the use case group. The other observer removes the use case from the
+        // repository when the lifecycle is destroyed.
+        assertThat(mLifecycle.getObserverCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void useCaseGroupIsRemovedFromRepository_whenLifecycleIsDestroyed() {
+        mRepository.getOrCreateUseCaseGroup(mLifecycle);
+        mLifecycle.destroy();
+
+        assertThat(mUseCasesMap).isEmpty();
+    }
+
+    @Test
+    public void useCaseIsCleared_whenLifecycleIsDestroyed() {
+        UseCaseGroupLifecycleController group = mRepository.getOrCreateUseCaseGroup(mLifecycle);
+        FakeUseCase useCase = new FakeUseCase();
+        group.getUseCaseGroup().addUseCase(useCase);
+
+        assertThat(useCase.isCleared()).isFalse();
+
+        mLifecycle.destroy();
+
+        assertThat(useCase.isCleared()).isTrue();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void exception_whenCreatingWithDestroyedLifecycle() {
+        mLifecycle.destroy();
+
+        // Should throw IllegalArgumentException
+        mRepository.getOrCreateUseCaseGroup(mLifecycle);
+    }
+}
diff --git a/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupTest.java b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupTest.java
new file mode 100644
index 0000000..79ca51d
--- /dev/null
+++ b/camera/core/src/androidTest/java/androidx/camera/core/UseCaseGroupTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class UseCaseGroupTest {
+    private final UseCaseGroup.StateChangeListener mMockListener =
+            Mockito.mock(UseCaseGroup.StateChangeListener.class);
+    private UseCaseGroup mUseCaseGroup;
+    private FakeUseCase mFakeUseCase;
+    private FakeOtherUseCase mFakeOtherUseCase;
+
+    @Before
+    public void setUp() {
+        FakeUseCaseConfiguration fakeUseCaseConfiguration = new FakeUseCaseConfiguration.Builder()
+                .setTargetName("fakeUseCaseConfiguration")
+                .build();
+        FakeOtherUseCaseConfiguration fakeOtherUseCaseConfiguration =
+                new FakeOtherUseCaseConfiguration.Builder()
+                        .setTargetName("fakeOtherUseCaseConfiguration")
+                        .build();
+        mUseCaseGroup = new UseCaseGroup();
+        mFakeUseCase = new FakeUseCase(fakeUseCaseConfiguration);
+        mFakeOtherUseCase = new FakeOtherUseCase(fakeOtherUseCaseConfiguration);
+    }
+
+    @Test
+    public void groupStartsEmpty() {
+        assertThat(mUseCaseGroup.getUseCases()).isEmpty();
+    }
+
+    @Test
+    public void newUseCaseIsAdded_whenNoneExistsInGroup() {
+        assertThat(mUseCaseGroup.addUseCase(mFakeUseCase)).isTrue();
+        assertThat(mUseCaseGroup.getUseCases()).containsExactly(mFakeUseCase);
+    }
+
+    @Test
+    public void multipleUseCases_canBeAdded() {
+        assertThat(mUseCaseGroup.addUseCase(mFakeUseCase)).isTrue();
+        assertThat(mUseCaseGroup.addUseCase(mFakeOtherUseCase)).isTrue();
+
+        assertThat(mUseCaseGroup.getUseCases()).containsExactly(mFakeUseCase, mFakeOtherUseCase);
+    }
+
+    @Test
+    public void groupBecomesEmpty_afterGroupIsCleared() {
+        mUseCaseGroup.addUseCase(mFakeUseCase);
+        mUseCaseGroup.clear();
+
+        assertThat(mUseCaseGroup.getUseCases()).isEmpty();
+    }
+
+    @Test
+    public void useCaseIsCleared_afterGroupIsCleared() {
+        mUseCaseGroup.addUseCase(mFakeUseCase);
+        assertThat(mFakeUseCase.isCleared()).isFalse();
+
+        mUseCaseGroup.clear();
+
+        assertThat(mFakeUseCase.isCleared()).isTrue();
+    }
+
+    @Test
+    public void useCaseRemoved_afterRemovedCalled() {
+        mUseCaseGroup.addUseCase(mFakeUseCase);
+
+        mUseCaseGroup.removeUseCase(mFakeUseCase);
+
+        assertThat(mUseCaseGroup.getUseCases()).isEmpty();
+    }
+
+    @Test
+    public void listenerOnGroupActive_ifUseCaseGroupStarted() {
+        mUseCaseGroup.setListener(mMockListener);
+        mUseCaseGroup.start();
+
+        verify(mMockListener, times(1)).onGroupActive(mUseCaseGroup);
+    }
+
+    @Test
+    public void listenerOnGroupInactive_ifUseCaseGroupStopped() {
+        mUseCaseGroup.setListener(mMockListener);
+        mUseCaseGroup.stop();
+
+        verify(mMockListener, times(1)).onGroupInactive(mUseCaseGroup);
+    }
+
+    @Test
+    public void setListener_replacesPreviousListener() {
+        mUseCaseGroup.setListener(mMockListener);
+        mUseCaseGroup.setListener(null);
+
+        mUseCaseGroup.start();
+        verify(mMockListener, never()).onGroupActive(mUseCaseGroup);
+    }
+}
diff --git a/camera/core/src/main/AndroidManifest.xml b/camera/core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..818bb3c
--- /dev/null
+++ b/camera/core/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?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 package="androidx.camera.core"/>
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..dfe2561
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AndroidImageProxy.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.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.
+     */
+    private static final boolean SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+
+    @GuardedBy("this")
+    private final Image mImage;
+
+    @GuardedBy("this")
+    private final PlaneProxy[] mPlanes;
+
+    @GuardedBy("this")
+    private long mTimestamp;
+
+    /**
+     * Creates a new instance which wraps the given image.
+     *
+     * @param image to wrap
+     * @return new {@link AndroidImageProxy} instance
+     */
+    AndroidImageProxy(Image image) {
+        mImage = image;
+
+        Image.Plane[] originalPlanes = image.getPlanes();
+        if (originalPlanes != null) {
+            mPlanes = new PlaneProxy[originalPlanes.length];
+            for (int i = 0; i < originalPlanes.length; ++i) {
+                mPlanes[i] = new PlaneProxy(originalPlanes[i]);
+            }
+        } else {
+            mPlanes = new PlaneProxy[0];
+        }
+
+        mTimestamp = image.getTimestamp();
+    }
+
+    @Override
+    public synchronized void close() {
+        mImage.close();
+    }
+
+    @Override
+    public synchronized Rect getCropRect() {
+        return mImage.getCropRect();
+    }
+
+    @Override
+    public synchronized void setCropRect(Rect rect) {
+        mImage.setCropRect(rect);
+    }
+
+    @Override
+    public synchronized int getFormat() {
+        return mImage.getFormat();
+    }
+
+    @Override
+    public synchronized int getHeight() {
+        return mImage.getHeight();
+    }
+
+    @Override
+    public synchronized int getWidth() {
+        return mImage.getWidth();
+    }
+
+    @Override
+    public synchronized long getTimestamp() {
+        if (SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK) {
+            return mImage.getTimestamp();
+        } else {
+            return mTimestamp;
+        }
+    }
+
+    @Override
+    public synchronized void setTimestamp(long timestamp) {
+        if (SET_TIMESTAMP_AVAILABLE_IN_FRAMEWORK) {
+            mImage.setTimestamp(timestamp);
+        } else {
+            mTimestamp = timestamp;
+        }
+    }
+
+    @Override
+    public synchronized ImageProxy.PlaneProxy[] getPlanes() {
+        return mPlanes;
+    }
+
+    /** 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 mPlane;
+
+        PlaneProxy(Image.Plane plane) {
+            mPlane = plane;
+        }
+
+        @Override
+        public synchronized int getRowStride() {
+            return mPlane.getRowStride();
+        }
+
+        @Override
+        public synchronized int getPixelStride() {
+            return mPlane.getPixelStride();
+        }
+
+        @Override
+        public synchronized ByteBuffer getBuffer() {
+            return mPlane.getBuffer();
+        }
+    }
+
+    /**
+     * The {@link Image} that comes from the framework does not contain any additional metadata, so
+     * will always return null.
+     */
+    @Override
+    public ImageInfo getImageInfo() {
+        return null;
+    }
+
+    @Override
+    public synchronized Image getImage() {
+        return mImage;
+    }
+}
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..2a304e0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
@@ -0,0 +1,110 @@
+/*
+ * 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 mImageReader;
+
+    /**
+     * Creates a new instance which wraps the given image reader.
+     *
+     * @param imageReader to wrap
+     * @return new {@link AndroidImageReaderProxy} instance
+     */
+    AndroidImageReaderProxy(ImageReader imageReader) {
+        mImageReader = imageReader;
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireLatestImage() {
+        Image image = mImageReader.acquireLatestImage();
+        if (image == null) {
+            return null;
+        }
+        return new AndroidImageProxy(image);
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireNextImage() {
+        Image image = mImageReader.acquireNextImage();
+        if (image == null) {
+            return null;
+        }
+        return new AndroidImageProxy(image);
+    }
+
+    @Override
+    public synchronized void close() {
+        mImageReader.close();
+    }
+
+    @Override
+    public synchronized int getHeight() {
+        return mImageReader.getHeight();
+    }
+
+    @Override
+    public synchronized int getWidth() {
+        return mImageReader.getWidth();
+    }
+
+    @Override
+    public synchronized int getImageFormat() {
+        return mImageReader.getImageFormat();
+    }
+
+    @Override
+    public synchronized int getMaxImages() {
+        return mImageReader.getMaxImages();
+    }
+
+    @Override
+    public synchronized Surface getSurface() {
+        return mImageReader.getSurface();
+    }
+
+    @Override
+    public synchronized void setOnImageAvailableListener(
+            @Nullable final ImageReaderProxy.OnImageAvailableListener listener,
+            @Nullable Handler handler) {
+        ImageReader.OnImageAvailableListener transformedListener =
+                new ImageReader.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReader reader) {
+                        listener.onImageAvailable(AndroidImageReaderProxy.this);
+                    }
+                };
+        mImageReader.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..1d40315
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/AppConfiguration.java
@@ -0,0 +1,310 @@
+/*
+ * 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.Set;
+import java.util.UUID;
+
+/**
+ * Configuration for adding implementation and user-specific behavior to CameraX.
+ *
+ * <p>The AppConfiguration
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+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 mConfig;
+
+    AppConfiguration(OptionsBundle options) {
+        mConfig = 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 mConfig;
+    }
+
+    /** A builder for generating {@link AppConfiguration} objects. */
+    public static final class Builder
+            implements TargetConfiguration.Builder<CameraX, AppConfiguration, Builder> {
+
+        private final MutableOptionsBundle mMutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            mMutableConfig = 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 mMutableConfig;
+        }
+
+        /** The solution for the unchecked cast warning. */
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public AppConfiguration build() {
+            return new AppConfiguration(OptionsBundle.from(mMutableConfig));
+        }
+
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // Implementations of TargetConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetClass(Class<CameraX> 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();
+        }
+
+        @Override
+        public Builder setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // Implementations of TargetConfiguration default methods
+
+    @Override
+    @Nullable
+    public Class<CameraX> getTargetClass(
+            @Nullable Class<CameraX> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<CameraX> storedClass = (Class<CameraX>) retrieveOption(
+                OPTION_TARGET_CLASS,
+                valueIfMissing);
+        return storedClass;
+    }
+
+    @Override
+    public Class<CameraX> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<CameraX> storedClass = (Class<CameraX>) retrieveOption(
+                OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    @Override
+    @Nullable
+    public String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    @Override
+    public String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+}
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..aebdcf1
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/BaseCamera.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 androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Collection;
+
+/**
+ * The base camera interface. It is controlled by the change of state in use cases.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface BaseCamera extends BaseUseCase.StateChangeListener,
+        CameraControl.ControlUpdateListener {
+    /**
+     * 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. */
+    CameraControl getCameraControl();
+
+    /** Returns an interface to retrieve characteristics of the camera. */
+    CameraInfo getCameraInfo() throws CameraInfoUnavailableException;
+}
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..3a01a3c
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/BaseUseCase.java
@@ -0,0 +1,490 @@
+/*
+ * 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.CameraX.LensFacing;
+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 the set of arguments in a use case to arguments
+ * that are usable by a camera. BaseUseCase also will communicate 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> mListeners = 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> mAttachedCameraControlMap = 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> mAttachedCameraIdToSessionConfigurationMap =
+            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> mAttachedSurfaceResolutionMap = new HashMap<>();
+
+    private State mState = State.INACTIVE;
+
+    private UseCaseConfiguration<?> mUseCaseConfiguration;
+
+    /**
+     * 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 mImageFormat = 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 use case configuration 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.
+     *
+     * @param lensFacing The {@link LensFacing} that the default builder will target to.
+     * @return A builder pre-populated with use case default options.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Nullable
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder(
+            CameraX.LensFacing lensFacing) {
+        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}.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected void updateUseCaseConfiguration(UseCaseConfiguration<?> useCaseConfiguration) {
+        CameraX.LensFacing lensFacing =
+                ((CameraDeviceConfiguration) useCaseConfiguration).getLensFacing(null);
+
+        UseCaseConfiguration.Builder<?, ?, ?> defaultBuilder = getDefaultBuilder(lensFacing);
+        if (defaultBuilder == null) {
+            Log.w(
+                    TAG,
+                    "No default configuration available. Relying solely on user-supplied options.");
+            mUseCaseConfiguration = useCaseConfiguration;
+        } else {
+            mUseCaseConfiguration = 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.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    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}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public Set<String> getAttachedCameraIds() {
+        return mAttachedCameraIdToSessionConfigurationMap.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()}.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected void attachToCamera(String cameraId, SessionConfiguration sessionConfiguration) {
+        mAttachedCameraIdToSessionConfigurationMap.put(cameraId, sessionConfiguration);
+    }
+
+    /**
+     * Add a {@link StateChangeListener}, which listens to this BaseUseCase's active and inactive
+     * transition events.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void addStateChangeListener(StateChangeListener listener) {
+        mListeners.add(listener);
+    }
+
+    /**
+     * Attach a CameraControl to this use case.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public final void attachCameraControl(String cameraId, CameraControl cameraControl) {
+        mAttachedCameraControlMap.put(cameraId, cameraControl);
+        onCameraControlReady(cameraId);
+    }
+
+    /** Detach a CameraControl from this use case. */
+    final void detachCameraControl(String cameraId) {
+        mAttachedCameraControlMap.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.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void removeStateChangeListener(StateChangeListener listener) {
+        mListeners.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
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public SessionConfiguration getSessionConfiguration(String cameraId) {
+        SessionConfiguration sessionConfiguration =
+                mAttachedCameraIdToSessionConfigurationMap.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.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected final void notifyActive() {
+        mState = State.ACTIVE;
+        notifyState();
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that it has
+     * transitioned to an inactive state.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected final void notifyInactive() {
+        mState = State.INACTIVE;
+        notifyState();
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that the
+     * settings have been updated.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected final void notifyUpdated() {
+        for (StateChangeListener listener : mListeners) {
+            listener.onUseCaseUpdated(this);
+        }
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase that the use
+     * case needs to be completely reset.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected final void notifyReset() {
+        for (StateChangeListener listener : mListeners) {
+            listener.onUseCaseReset(this);
+        }
+    }
+
+    /**
+     * Notify all {@link StateChangeListener} that are listening to this BaseUseCase of its current
+     * state.
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected final void notifyState() {
+        switch (mState) {
+            case INACTIVE:
+                for (StateChangeListener listener : mListeners) {
+                    listener.onUseCaseInactive(this);
+                }
+                break;
+            case ACTIVE:
+                for (StateChangeListener listener : mListeners) {
+                    listener.onUseCaseActive(this);
+                }
+                break;
+        }
+    }
+
+    /** Clears internal state of this use case. */
+    @CallSuper
+    protected void clear() {
+        mListeners.clear();
+    }
+
+    public String getName() {
+        return mUseCaseConfiguration.getTargetName("<UnknownUseCase-" + this.hashCode() + ">");
+    }
+
+    /**
+     * Retrieves the configuration used by this use case.
+     *
+     * @return the configuration used by this use case.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public UseCaseConfiguration<?> getUseCaseConfiguration() {
+        return mUseCaseConfiguration;
+    }
+
+    /**
+     * 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.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public Size getAttachedSurfaceResolution(String cameraId) {
+        return mAttachedSurfaceResolutionMap.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()) {
+            mAttachedSurfaceResolutionMap.put(entry.getKey(), entry.getValue());
+        }
+    }
+
+    /**
+     * Called when binding new use cases via {@link CameraX#bindToLifecycle(LifecycleOwner,
+     * BaseUseCase...)}. Override to create 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
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected void onCameraControlReady(String cameraId) {
+    }
+
+    /**
+     * Retrieves a previously attached {@link CameraControl}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected CameraControl getCameraControl(String cameraId) {
+        CameraControl cameraControl = mAttachedCameraControlMap.get(cameraId);
+        if (cameraControl == null) {
+            return CameraControl.DEFAULT_EMPTY_INSTANCE;
+        }
+        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 mImageFormat;
+    }
+
+    protected void setImageFormat(int imageFormat) {
+        mImageFormat = 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);
+    }
+}
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..fa5d258
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureCallback.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * A callback object for tracking the progress of a capture request submitted to the camera device.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+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..7329076
--- /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> mCallbacks = 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)) {
+                    mCallbacks.add(callback);
+                }
+            }
+        }
+
+        @Override
+        public void onCaptureCompleted(CameraCaptureResult result) {
+            for (CameraCaptureCallback callback : mCallbacks) {
+                callback.onCaptureCompleted(result);
+            }
+        }
+
+        @Override
+        public void onCaptureFailed(CameraCaptureFailure failure) {
+            for (CameraCaptureCallback callback : mCallbacks) {
+                callback.onCaptureFailed(failure);
+            }
+        }
+
+        @NonNull
+        public List<CameraCaptureCallback> getCallbacks() {
+            return mCallbacks;
+        }
+    }
+}
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..8ebd291
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureFailure.java
@@ -0,0 +1,56 @@
+/*
+ * 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 report of failed capture for a single image capture.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CameraCaptureFailure {
+
+    private final Reason mReason;
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public CameraCaptureFailure(Reason reason) {
+        mReason = 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 mReason;
+    }
+
+    /**
+     * 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..346b4f3
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureMetaData.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 androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * This class defines the enumeration constants used for querying the camera capture mode and
+ * results.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+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..e75a30e
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureResult.java
@@ -0,0 +1,113 @@
+/*
+ * 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 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
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface CameraCaptureResult {
+
+    /** Returns the current auto focus mode of operation. */
+    @NonNull
+    AfMode getAfMode();
+
+    /** Returns the current auto focus state. */
+    @NonNull
+    AfState getAfState();
+
+    /** Returns the current auto exposure state. */
+    @NonNull
+    AeState getAeState();
+
+    /** Returns the current auto white balance state.*/
+    @NonNull
+    AwbState getAwbState();
+
+    /** Returns the current flash state. */
+    @NonNull
+    FlashState getFlashState();
+
+    /**
+     * Returns the timestamp in nanoseconds.
+     *
+     * <p> If the timestamp was unavailable then it will return {@code -1L}.
+     */
+    long getTimestamp();
+
+    /** Returns the tag associated with the capture request. */
+    Object getTag();
+
+    /** 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;
+        }
+
+        @Override
+        public long getTimestamp() {
+            return -1L;
+        }
+
+        @Override
+        public Object getTag() {
+            return null;
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CameraCaptureResultImageInfo.java b/camera/core/src/main/java/androidx/camera/core/CameraCaptureResultImageInfo.java
new file mode 100644
index 0000000..613ad4a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureResultImageInfo.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/** An ImageInfo that is created by a {@link CameraCaptureResult}. */
+final class CameraCaptureResultImageInfo implements ImageInfo {
+    private final CameraCaptureResult mCameraCaptureResult;
+
+    /** Create an {@link ImageInfo} using data from {@link CameraCaptureResult}. */
+    CameraCaptureResultImageInfo(CameraCaptureResult cameraCaptureResult) {
+        mCameraCaptureResult = cameraCaptureResult;
+    }
+
+    @Override
+    public Object getTag() {
+        return mCameraCaptureResult.getTag();
+    }
+
+    @Override
+    public long getTimestamp() {
+        return mCameraCaptureResult.getTimestamp();
+    }
+}
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..72f1be0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraCaptureSessionStateCallbacks.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.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.
+     **/
+    public static CameraCaptureSession.StateCallback createNoOpCallback() {
+        return new NoOpSessionStateCallback();
+    }
+
+    /**
+     * Returns a session state callback which calls a list of other callbacks.
+     */
+    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.
+     */
+    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> mCallbacks = 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)) {
+                    mCallbacks.add(callback);
+                }
+            }
+        }
+
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : mCallbacks) {
+                callback.onConfigured(session);
+            }
+        }
+
+        @Override
+        public void onActive(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : mCallbacks) {
+                callback.onActive(session);
+            }
+        }
+
+        @Override
+        public void onClosed(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : mCallbacks) {
+                callback.onClosed(session);
+            }
+        }
+
+        @Override
+        public void onReady(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : mCallbacks) {
+                callback.onReady(session);
+            }
+        }
+
+        @RequiresApi(api = Build.VERSION_CODES.O)
+        @Override
+        public void onCaptureQueueEmpty(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : mCallbacks) {
+                callback.onCaptureQueueEmpty(session);
+            }
+        }
+
+        @RequiresApi(api = Build.VERSION_CODES.M)
+        @Override
+        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
+            for (CameraCaptureSession.StateCallback callback : mCallbacks) {
+                callback.onSurfacePrepared(session, surface);
+            }
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            for (CameraCaptureSession.StateCallback callback : mCallbacks) {
+                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..b17be90
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraControl.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 android.graphics.Rect;
+import android.os.Handler;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * 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
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface CameraControl {
+    /**
+     * 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);
+
+    /**
+     * Adjusts the camera output according to the properties in some local regions.
+     *
+     * <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
+     */
+    void focus(Rect focus, Rect metering);
+
+    /** Returns the current flash mode. */
+    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();
+
+    /** Returns if the focus is currently locked or not. */
+    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);
+
+    /**
+     * Performs a single capture request.
+     *
+     * @param captureRequestConfiguration
+     */
+    void submitSingleRequest(CaptureRequestConfiguration captureRequestConfiguration);
+
+    CameraControl DEFAULT_EMPTY_INSTANCE = new CameraControl() {
+        @Override
+        public void setCropRegion(Rect crop) {
+        }
+
+        @Override
+        public void focus(Rect focus, Rect metering, @Nullable OnFocusCompletedListener listener,
+                @Nullable Handler handler) {
+        }
+
+        @Override
+        public void focus(Rect focus, Rect metering) {
+        }
+
+        @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 void submitSingleRequest(
+                CaptureRequestConfiguration captureRequestConfiguration) {
+        }
+    };
+
+    /** Listener called when CameraControl need to notify event. */
+    interface ControlUpdateListener {
+
+        /** Called when CameraControl has updated session configuration. */
+        void onCameraControlUpdateSessionConfiguration(SessionConfiguration sessionConfiguration);
+
+        /** Called when CameraControl need to send single request. */
+        void onCameraControlSingleRequest(CaptureRequestConfiguration captureRequestConfiguration);
+    }
+}
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..f3f91ee
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceConfiguration.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.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);
+
+    /**
+     * Returns the lens-facing direction of the camera 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
+    CameraX.LensFacing getLensFacing(@Nullable LensFacing 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.
+     */
+    CameraX.LensFacing getLensFacing();
+
+    // 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.
+         */
+        B setLensFacing(CameraX.LensFacing lensFacing);
+    }
+}
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..d033c91
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceStateCallbacks.java
@@ -0,0 +1,119 @@
+/*
+ * 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.
+     */
+    public static CameraDevice.StateCallback createNoOpCallback() {
+        return new NoOpDeviceStateCallback();
+    }
+
+    /**
+     * Returns a device state callback which calls a list of other callbacks.
+     */
+    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.
+     */
+    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> mCallbacks = 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)) {
+                    mCallbacks.add(callback);
+                }
+            }
+        }
+
+        @Override
+        public void onOpened(CameraDevice cameraDevice) {
+            for (CameraDevice.StateCallback callback : mCallbacks) {
+                callback.onOpened(cameraDevice);
+            }
+        }
+
+        @Override
+        public void onClosed(CameraDevice cameraDevice) {
+            for (CameraDevice.StateCallback callback : mCallbacks) {
+                callback.onClosed(cameraDevice);
+            }
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice cameraDevice) {
+            for (CameraDevice.StateCallback callback : mCallbacks) {
+                callback.onDisconnected(cameraDevice);
+            }
+        }
+
+        @Override
+        public void onError(CameraDevice cameraDevice, int error) {
+            for (CameraDevice.StateCallback callback : mCallbacks) {
+                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..7576401
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraDeviceSurfaceManager.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.core;
+
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+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
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+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..e8ebc7a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.camera.core.CameraX.LensFacing;
+
+import java.util.Set;
+
+/**
+ * The factory class that creates {@link BaseCamera} instances.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface CameraFactory {
+
+    /** Get the camera with the associated id. */
+    BaseCamera getCamera(String cameraId);
+
+    /** Get ids for all the available cameras. */
+    Set<String> getAvailableCameraIds() throws CameraInfoUnavailableException;
+
+    /** Get the id of the camera with the specified lens facing. */
+    @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..ab4cf7b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraInfo.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.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.
+     */
+    int getSensorRotationDegrees();
+
+    /**
+     * 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..1138d33
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraRepository.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 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 mCamerasLock = new Object();
+
+    @GuardedBy("mCamerasLock")
+    private final Map<String, BaseCamera> mCameras = new HashMap<>();
+
+    /**
+     * Initializes the repository from a {@link Context}.
+     *
+     * <p>All cameras queried from the {@link CameraFactory} will be added to the repository.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void init(CameraFactory cameraFactory) {
+        synchronized (mCamerasLock) {
+            try {
+                Set<String> camerasList = cameraFactory.getAvailableCameraIds();
+                for (String id : camerasList) {
+                    Log.d(TAG, "Added camera: " + id);
+                    mCameras.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
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public BaseCamera getCamera(String cameraId) {
+        synchronized (mCamerasLock) {
+            BaseCamera camera = mCameras.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 (mCamerasLock) {
+            return Collections.unmodifiableSet(mCameras.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 (mCamerasLock) {
+            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("mCamerasLock")
+    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 (mCamerasLock) {
+            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("mCamerasLock")
+    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..ca3a451
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraX.java
@@ -0,0 +1,569 @@
+/*
+ * 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.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.ArrayList;
+import java.util.Collection;
+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 camera
+ * 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();
+    final CameraRepository mCameraRepository = new CameraRepository();
+    private final AtomicBoolean mInitialized = new AtomicBoolean(false);
+    private final UseCaseGroupRepository mUseCaseGroupRepository = new UseCaseGroupRepository();
+    private final ErrorHandler mErrorHandler = new ErrorHandler();
+    private CameraFactory mCameraFactory;
+    private CameraDeviceSurfaceManager mSurfaceManager;
+    private UseCaseConfigurationFactory mDefaultConfigFactory;
+    private Context mContext;
+    /** Prevents construction. */
+    private CameraX() {
+    }
+
+    /**
+     * Binds the collection of {@link BaseUseCase} to a {@link LifecycleOwner}.
+     *
+     * <p>If the lifecycleOwner contains a {@link 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 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}.
+     *
+     * <p> Currently up to 3 use cases may be bound at any time.  Exceeding this will throw an
+     * IllegalArgumentException.
+     *
+     * @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.mUseCaseGroupRepository.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.mUseCaseGroupRepository.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.mUseCaseGroupRepository.getUseCaseGroups();
+
+        Map<String, List<BaseUseCase>> detachingUseCaseMap = new HashMap<>();
+
+        for (BaseUseCase useCase : useCases) {
+            for (UseCaseGroupLifecycleController useCaseGroupLifecycleController : useCaseGroups) {
+                UseCaseGroup useCaseGroup = useCaseGroupLifecycleController.getUseCaseGroup();
+                if (useCaseGroup.removeUseCase(useCase)) {
+                    // Saves all detaching use cases and detach them at once.
+                    for (String cameraId : useCase.getAttachedCameraIds()) {
+                        List<BaseUseCase> useCasesOnCameraId = detachingUseCaseMap.get(cameraId);
+                        if (useCasesOnCameraId == null) {
+                            useCasesOnCameraId = new ArrayList<>();
+                            detachingUseCaseMap.put(cameraId, useCasesOnCameraId);
+                        }
+                        useCasesOnCameraId.add(useCase);
+                    }
+                }
+            }
+        }
+
+        for (String cameraId : detachingUseCaseMap.keySet()) {
+            detach(cameraId, detachingUseCaseMap.get(cameraId));
+        }
+
+        for (BaseUseCase useCase : useCases) {
+            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.mUseCaseGroupRepository.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
+     * @param lensFacing The {@link LensFacing} that the default configuration will target to.
+     * @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, LensFacing lensFacing) {
+        return INSTANCE.getDefaultConfigFactory().getConfiguration(configType, lensFacing);
+    }
+
+    /**
+     * 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.mErrorHandler.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.mErrorHandler.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 the context used for CameraX.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static Context getContext() {
+        return INSTANCE.mContext;
+    }
+
+    /**
+     * Returns true if CameraX is initialized.
+     *
+     * <p>Any previous call to {@link #init(Context, AppConfiguration)} would have initialized
+     * CameraX.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static boolean isInitialized() {
+        return INSTANCE.mInitialized.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);
+        }
+
+        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 useCases the list of use case to remove the callback from.
+     */
+    private static void detach(String cameraId, List<BaseUseCase> useCases) {
+        BaseCamera camera = INSTANCE.getCameraRepository().getCamera(cameraId);
+        if (camera == null) {
+            throw new IllegalArgumentException("Invalid camera: " + cameraId);
+        }
+
+        for (BaseUseCase useCase : useCases) {
+            useCase.removeStateChangeListener(camera);
+            useCase.detachCameraControl(cameraId);
+        }
+        camera.removeOnlineUseCase(useCases);
+    }
+
+    private static void calculateSuggestedResolutions(BaseUseCase... useCases) {
+        Collection<UseCaseGroupLifecycleController> controllers =
+                INSTANCE.mUseCaseGroupRepository.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 (mCameraFactory == null) {
+            throw new IllegalStateException("CameraX not initialized yet.");
+        }
+
+        return mCameraFactory;
+    }
+
+    /**
+     * Returns the {@link CameraDeviceSurfaceManager} instance.
+     *
+     * @throws IllegalStateException if the {@link CameraDeviceSurfaceManager} has not been set, due
+     *                               to being uninitialized.
+     */
+    private CameraDeviceSurfaceManager getCameraDeviceSurfaceManager() {
+        if (mSurfaceManager == null) {
+            throw new IllegalStateException("CameraX not initialized yet.");
+        }
+
+        return mSurfaceManager;
+    }
+
+    private UseCaseConfigurationFactory getDefaultConfigFactory() {
+        if (mDefaultConfigFactory == null) {
+            throw new IllegalStateException("CameraX not initialized yet.");
+        }
+
+        return mDefaultConfigFactory;
+    }
+
+    private void initInternal(Context context, AppConfiguration appConfiguration) {
+        if (mInitialized.getAndSet(true)) {
+            return;
+        }
+
+        mContext = context.getApplicationContext();
+        mCameraFactory = appConfiguration.getCameraFactory(null);
+        if (mCameraFactory == null) {
+            throw new IllegalStateException(
+                    "Invalid app configuration provided. Missing CameraFactory.");
+        }
+
+        mSurfaceManager = appConfiguration.getDeviceSurfaceManager(null);
+        if (mSurfaceManager == null) {
+            throw new IllegalStateException(
+                    "Invalid app configuration provided. Missing CameraDeviceSurfaceManager.");
+        }
+
+        mDefaultConfigFactory = appConfiguration.getUseCaseConfigRepository(null);
+        if (mDefaultConfigFactory == null) {
+            throw new IllegalStateException(
+                    "Invalid app configuration provided. Missing UseCaseConfigurationFactory.");
+        }
+
+        mCameraRepository.init(mCameraFactory);
+    }
+
+    private UseCaseGroupLifecycleController getOrCreateUseCaseGroup(LifecycleOwner lifecycleOwner) {
+        return mUseCaseGroupRepository.getOrCreateUseCaseGroup(
+                lifecycleOwner, new UseCaseGroupRepository.UseCaseGroupSetup() {
+                    @Override
+                    public void setup(UseCaseGroup useCaseGroup) {
+                        useCaseGroup.setListener(mCameraRepository);
+                    }
+                });
+    }
+
+    private CameraRepository getCameraRepository() {
+        return mCameraRepository;
+    }
+
+    /** 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..09b9120
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CameraXThreads.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 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 {
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final String TAG = "CameraX-";
+
+    private CameraXThreads() {
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureBundle.java b/camera/core/src/main/java/androidx/camera/core/CaptureBundle.java
new file mode 100644
index 0000000..cfcb04d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureBundle.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 androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An ordered collection of {@link CaptureStage}.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CaptureBundle {
+    private List<CaptureStage> mCaptureStages = new ArrayList<>();
+
+    /** Add a new {@link CaptureStage} to the bundle. */
+    public void addCaptureStage(@NonNull CaptureStage captureStage) {
+        mCaptureStages.add(captureStage);
+    }
+
+    /**
+     * Returns the list of {@link CaptureStage} which is in the order that they were inserted.
+     */
+    public List<CaptureStage> getCaptureStages() {
+        return Collections.unmodifiableList(mCaptureStages);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureBundles.java b/camera/core/src/main/java/androidx/camera/core/CaptureBundles.java
new file mode 100644
index 0000000..4ee9595
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureBundles.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/**
+ * Different implementations of {@link CaptureBundle}.
+ */
+final class CaptureBundles {
+    /** Creates a {@link CaptureBundle} which contain a single default {@link CaptureStage}. */
+    static CaptureBundle singleDefaultCaptureBundle() {
+        CaptureBundle captureBundle = new CaptureBundle();
+        captureBundle.addCaptureStage(new CaptureStage.DefaultCaptureStage());
+        return captureBundle;
+    }
+
+    private CaptureBundles() {}
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureProcessor.java b/camera/core/src/main/java/androidx/camera/core/CaptureProcessor.java
new file mode 100644
index 0000000..7894b29
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureProcessor.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;
+
+import android.view.Surface;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * A processing step of the image capture pipeline.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface CaptureProcessor {
+    /**
+     * This gets called to update where the CaptureProcessor should write the output of {@link
+     * #process(ImageProxyBundle)}.
+     *
+     * @param surface The {@link Surface} that the CaptureProcessor should write data into.
+     * @param imageFormat The format of that the surface expects.
+     */
+    void onOutputSurface(Surface surface, int imageFormat);
+
+    /**
+     * Process a {@link ImageProxyBundle} for the set of captures that were
+     * requested.
+     *
+     * <p> The result of the processing step should be written to the {@link Surface} that was
+     * received by {@link #onOutputSurface(Surface, int)}.
+     * @param bundle The set of images to process. The ImageProxyBundle and the {@link ImageProxy}
+     *               that are retrieved from it will become invalid after this method completes, so
+     *               no references to them should be kept.
+     */
+    void process(ImageProxyBundle bundle);
+}
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..c2c0ad0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureRequestConfiguration.java
@@ -0,0 +1,297 @@
+/*
+ * 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
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class CaptureRequestConfiguration {
+
+    /** The set of {@link Surface} that data from the camera will be put into. */
+    final List<DeferrableSurface> mSurfaces;
+
+    /** The parameters used to configure the {@link CaptureRequest}. */
+    final Map<Key<?>, CaptureRequestParameter<?>> mCaptureRequestParameters;
+
+    final Configuration mImplementationOptions;
+
+    /**
+     * The templates used for configuring a {@link CaptureRequest}. This must match the constants
+     * defined by {@link CameraDevice}
+     */
+    final int mTemplateType;
+
+    /** The camera capture callback for a {@link CameraCaptureSession}. */
+    final CameraCaptureCallback mCameraCaptureCallback;
+
+    /** True if this capture request needs a repeating surface */
+    private final boolean mUseRepeatingSurface;
+
+    /** The tag for associating capture result with capture request. */
+    private final Object mTag;
+
+    /**
+     * 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,
+            Object tag) {
+        mSurfaces = surfaces;
+        mCaptureRequestParameters = captureRequestParameters;
+        mImplementationOptions = implementationOptions;
+        mTemplateType = templateType;
+        mCameraCaptureCallback = cameraCaptureCallback;
+        mUseRepeatingSurface = useRepeatingSurface;
+        mTag = tag;
+    }
+
+    /** Get all the surfaces that the request will write data to. */
+    public List<DeferrableSurface> getSurfaces() {
+        return Collections.unmodifiableList(mSurfaces);
+    }
+
+    public Map<Key<?>, CaptureRequestParameter<?>> getCameraCharacteristics() {
+        return Collections.unmodifiableMap(mCaptureRequestParameters);
+    }
+
+    public Configuration getImplementationOptions() {
+        return mImplementationOptions;
+    }
+
+    int getTemplateType() {
+        return mTemplateType;
+    }
+
+    public boolean isUseRepeatingSurface() {
+        return mUseRepeatingSurface;
+    }
+
+    public CameraCaptureCallback getCameraCaptureCallback() {
+        return mCameraCaptureCallback;
+    }
+
+    public Object getTag() {
+        return mTag;
+    }
+
+    /**
+     * Return the builder of a {@link CaptureRequest} which can be issued.
+     *
+     * <p>Returns {@code null} if a valid {@link CaptureRequest} can not be constructed.
+     */
+    @Nullable
+    public CaptureRequest.Builder buildCaptureRequest(@Nullable CameraDevice device)
+            throws CameraAccessException {
+        if (device == null) {
+            return null;
+        }
+        CaptureRequest.Builder builder = device.createCaptureRequest(mTemplateType);
+
+        for (CaptureRequestParameter<?> captureRequestParameter :
+                mCaptureRequestParameters.values()) {
+            captureRequestParameter.apply(builder);
+        }
+
+        List<Surface> surfaceList = DeferrableSurfaces.surfaceList(mSurfaces);
+
+        if (surfaceList.isEmpty()) {
+            return null;
+        }
+
+        for (Surface surface : surfaceList) {
+            builder.addTarget(surface);
+        }
+
+        builder.setTag(mTag);
+
+        return builder;
+    }
+
+    /**
+     * Builder for easy modification/rebuilding of a {@link CaptureRequestConfiguration}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class Builder {
+        private final Set<DeferrableSurface> mSurfaces = new HashSet<>();
+        private final Map<Key<?>, CaptureRequestParameter<?>> mCaptureRequestParameters =
+                new HashMap<>();
+        private MutableConfiguration mImplementationOptions = MutableOptionsBundle.create();
+        private int mTemplateType = -1;
+        private CameraCaptureCallback mCameraCaptureCallback =
+                CameraCaptureCallbacks.createNoOpCallback();
+        private boolean mUseRepeatingSurface = false;
+        private Object mTag = null;
+
+        public Builder() {
+        }
+
+        private Builder(CaptureRequestConfiguration base) {
+            mSurfaces.addAll(base.mSurfaces);
+            mCaptureRequestParameters.putAll(base.mCaptureRequestParameters);
+            mImplementationOptions = MutableOptionsBundle.from(base.mImplementationOptions);
+            mTemplateType = base.mTemplateType;
+            mCameraCaptureCallback = base.mCameraCaptureCallback;
+            mUseRepeatingSurface = base.isUseRepeatingSurface();
+            mTag = base.getTag();
+        }
+
+        /** Create a {@link Builder} from a {@link CaptureRequestConfiguration} */
+        public static Builder from(CaptureRequestConfiguration base) {
+            return new Builder(base);
+        }
+
+        int getTemplateType() {
+            return mTemplateType;
+        }
+
+        /**
+         * Set the template characteristics of the CaptureRequestConfiguration.
+         *
+         * @param templateType Template constant that must match those defined by {@link
+         *                     CameraDevice}
+         */
+        public void setTemplateType(int templateType) {
+            mTemplateType = templateType;
+        }
+
+        CameraCaptureCallback getCameraCaptureCallback() {
+            return mCameraCaptureCallback;
+        }
+
+        public void setCameraCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+            mCameraCaptureCallback = cameraCaptureCallback;
+        }
+
+        /** Add a surface that the request will write data to. */
+        public void addSurface(DeferrableSurface surface) {
+            mSurfaces.add(surface);
+        }
+
+        /** Remove a surface that the request will write data to. */
+        public void removeSurface(DeferrableSurface surface) {
+            mSurfaces.remove(surface);
+        }
+
+        /** Remove all the surfaces that the request will write data to. */
+        public void clearSurfaces() {
+            mSurfaces.clear();
+        }
+
+        Set<DeferrableSurface> getSurfaces() {
+            return mSurfaces;
+        }
+
+        /** Add a {@link CaptureRequest.Key}-value pair to the request. */
+        public <T> void addCharacteristic(Key<T> key, T value) {
+            mCaptureRequestParameters.put(key, CaptureRequestParameter.create(key, value));
+        }
+
+        /** Add a set of {@link CaptureRequest.Key}-value pairs to the request. */
+        public void addCharacteristics(Map<Key<?>, CaptureRequestParameter<?>> characteristics) {
+            mCaptureRequestParameters.putAll(characteristics);
+        }
+
+        public void setImplementationOptions(Configuration config) {
+            mImplementationOptions = MutableOptionsBundle.from(config);
+        }
+
+        /** Add a set of implementation specific options to the request. */
+        public void addImplementationOptions(Configuration config) {
+            for (Option<?> option : config.listOptions()) {
+                @SuppressWarnings("unchecked") // Options/values are being copied directly
+                        Option<Object> objectOpt = (Option<Object>) option;
+                mImplementationOptions.insertOption(objectOpt, config.retrieveOption(objectOpt));
+            }
+        }
+
+        Map<Key<?>, CaptureRequestParameter<?>> getCharacteristic() {
+            return mCaptureRequestParameters;
+        }
+
+        boolean isUseRepeatingSurface() {
+            return mUseRepeatingSurface;
+        }
+
+        public void setUseRepeatingSurface(boolean useRepeatingSurface) {
+            mUseRepeatingSurface = useRepeatingSurface;
+        }
+
+        public void setTag(Object tag) {
+            mTag = tag;
+        }
+
+        /**
+         * 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<>(mSurfaces),
+                    new HashMap<>(mCaptureRequestParameters),
+                    OptionsBundle.from(mImplementationOptions),
+                    mTemplateType,
+                    mCameraCaptureCallback,
+                    mUseRepeatingSurface,
+                    mTag);
+        }
+    }
+}
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..c3f3012
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureRequestParameter.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 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.
+ *
+ * @param <T> the type of the value
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+@AutoValue
+public abstract class CaptureRequestParameter<T> {
+    /** Prevent subclassing. */
+    CaptureRequestParameter() {
+    }
+
+    /** Creates an instance of CaptureRequestParameter with the corresponding key value pair. */
+    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());
+    }
+
+    /** Returns the key of the CaptureRequestParameter. */
+    public abstract CaptureRequest.Key<T> getKey();
+
+    /** Returns the value of the CaptureRequestParameter. */
+    public abstract T getValue();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/CaptureStage.java b/camera/core/src/main/java/androidx/camera/core/CaptureStage.java
new file mode 100644
index 0000000..29c809a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CaptureStage.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+/**
+ * A {@link CaptureRequestConfiguration} with an identifier.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface CaptureStage {
+
+    /** Returns the identifier for the capture. */
+    int getId();
+
+    /**
+     * Returns the configuration for the capture.
+     */
+    CaptureRequestConfiguration getCaptureRequestConfiguration();
+
+    /**
+     * A capture stage which contains no additional implementation options
+     */
+    final class DefaultCaptureStage implements CaptureStage {
+        private final CaptureRequestConfiguration mCaptureRequestConfiguration;
+
+        DefaultCaptureStage() {
+            CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+            mCaptureRequestConfiguration = builder.build();
+        }
+
+        @Override
+        public int getId() {
+            return 0;
+        }
+
+        @Override
+        public CaptureRequestConfiguration getCaptureRequestConfiguration() {
+            return mCaptureRequestConfiguration;
+        }
+    }
+}
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..0563fb7
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/CheckedSurfaceTexture.java
@@ -0,0 +1,153 @@
+/*
+ * 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 extends DeferrableSurface {
+    private final OnTextureChangedListener mOutputChangedListener;
+    private final Handler mMainThreadHandler;
+    @Nullable
+    SurfaceTexture mSurfaceTexture;
+    @Nullable
+    Surface mSurface;
+    @Nullable
+    private Size mResolution;
+    CheckedSurfaceTexture(
+            OnTextureChangedListener outputChangedListener, Handler mainThreadHandler) {
+        mOutputChangedListener = outputChangedListener;
+        mMainThreadHandler = 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) {
+        mResolution = resolution;
+    }
+
+    @UiThread
+    void resetSurfaceTexture() {
+        if (mResolution == null) {
+            throw new IllegalStateException(
+                    "setResolution() must be called before resetSurfaceTexture()");
+        }
+
+        release();
+        mSurfaceTexture = createDetachedSurfaceTexture(mResolution);
+        mSurface = new Surface(mSurfaceTexture);
+        mOutputChangedListener.onTextureChanged(mSurfaceTexture, mResolution);
+    }
+
+    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() {
+        final SettableFuture<Surface> deferredSurface = SettableFuture.create();
+        Runnable checkAndSetRunnable =
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        if (CheckedSurfaceTexture.this.surfaceTextureReleased(mSurfaceTexture)) {
+                            // Reset the surface texture and notify the listener
+                            CheckedSurfaceTexture.this.resetSurfaceTexture();
+                        }
+
+                        deferredSurface.set(mSurface);
+                    }
+                };
+
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+            checkAndSetRunnable.run();
+        } else {
+            mMainThreadHandler.post(checkAndSetRunnable);
+        }
+
+        return deferredSurface;
+    }
+
+    void release() {
+        if (mSurface != null) {
+            mSurface.release();
+            mSurface = 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..f24a341
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/Configuration.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.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 options 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
+        boolean containsOption(Option<?> id);
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        <ValueT> ValueT retrieveOption(Option<ValueT> id);
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing);
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        void findOptions(String idStem, OptionMatcher matcher);
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        Set<Option<?>> 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)
+        <ValueT> T 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> T removeOption(Option<ValueT> opt);
+
+        /**
+         * 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);
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public static <T> Option<T> create(String name, TypeReference<T> valueType) {
+            return create(name, valueType, /*token=*/ null);
+        }
+
+        /** @hide */
+        @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..d247465
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ConfigurationProvider.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ *
+ * @param <C> the {@link Configuration} type provided
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface ConfigurationProvider<C extends Configuration> {
+
+    /** Retrieve the {@link androidx.camera.core.Configuration} object.
+     *
+     * @param lensFacing The {@link CameraX.LensFacing} that the configuration provider will
+     *                   target to.
+     * */
+    C getConfiguration(CameraX.LensFacing lensFacing);
+}
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..7bb7c76
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/DeferrableSurface.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.core;
+
+import android.annotation.SuppressLint;
+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.annotation.VisibleForTesting;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A reference to a {@link Surface} whose creation can be deferred to a later time.
+ *
+ * <p>A {@link OnSurfaceDetachedListener} can also be set to be notified of surface detach event. It
+ * can be used to safely close the surface.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public abstract class DeferrableSurface {
+    // The count of attachment.
+    @GuardedBy("mLock")
+    private int mAttachedCount = 0;
+
+    // Listener to be called when surface is detached totally.
+    @Nullable
+    @GuardedBy("mLock")
+    private OnSurfaceDetachedListener mOnSurfaceDetachedListener = null;
+
+    @Nullable
+    @GuardedBy("mLock")
+    private Executor mListenerExecutor = null;
+
+    // Lock used for accessing states.
+    private final Object mLock = new Object();
+
+    /** Returns a {@link Surface} that is wrapped in a {@link ListenableFuture}. */
+    @Nullable
+    public abstract ListenableFuture<Surface> getSurface();
+
+    /** Notifies this surface is attached */
+    public void notifySurfaceAttached() {
+        synchronized (mLock) {
+            mAttachedCount++;
+        }
+    }
+
+    /**
+     * Notifies this surface is detached. OnSurfaceDetachedLisener will be called if it is detached
+     * totally
+     */
+    public void notifySurfaceDetached() {
+        OnSurfaceDetachedListener listener = null;
+        Executor listenerExecutor = null;
+
+        synchronized (mLock) {
+            if (mAttachedCount == 0) {
+                throw new IllegalStateException("Detaching occurs more times than attaching");
+            }
+
+            mAttachedCount--;
+            if (mAttachedCount == 0) {
+                listener = mOnSurfaceDetachedListener;
+                listenerExecutor = mListenerExecutor;
+            }
+        }
+
+        if (listener != null && listenerExecutor != null) {
+            callOnSurfaceDetachedListener(listener, listenerExecutor);
+        }
+    }
+
+    /**
+     * Sets the listener to be called when surface is detached totally.
+     *
+     * <p>If the surface is currently not attached, the listener will be called immediately. When
+     * clearing resource like ImageReader, to close it safely you can call this method and close the
+     * resources in the listener. This can ensure the surface is closed after it is no longer held
+     * in camera.
+     */
+    @SuppressLint("RestrictedApi") // TODO(b/124323692): Remove after aosp/900913 is merged
+    public void setOnSurfaceDetachedListener(@NonNull Executor executor,
+            @NonNull OnSurfaceDetachedListener listener) {
+        Preconditions.checkNotNull(executor);
+        Preconditions.checkNotNull(listener);
+        boolean shouldCallListenerNow = false;
+        synchronized (mLock) {
+            mOnSurfaceDetachedListener = listener;
+            mListenerExecutor = executor;
+            // Calls the listener immediately if the surface is not attached right now.
+            if (mAttachedCount == 0) {
+                shouldCallListenerNow = true;
+            }
+        }
+
+        if (shouldCallListenerNow) {
+            callOnSurfaceDetachedListener(listener, executor);
+        }
+    }
+
+    @SuppressLint("RestrictedApi") // TODO(b/124323692): Remove after aosp/900913 is merged
+    private static void callOnSurfaceDetachedListener(
+            @NonNull final OnSurfaceDetachedListener listener, @NonNull Executor executor) {
+        Preconditions.checkNotNull(executor);
+        Preconditions.checkNotNull(listener);
+
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                listener.onSurfaceDetached();
+            }
+        });
+    }
+
+    @VisibleForTesting
+    int getAttachedCount() {
+        synchronized (mLock) {
+            return mAttachedCount;
+        }
+    }
+
+    /**
+     * The listener to be called when surface is detached totally.
+     */
+    public interface OnSurfaceDetachedListener {
+        /**
+         * Called when surface is totally detached.
+         */
+        void onSurfaceDetached();
+    }
+
+}
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..2c48b1d
--- /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.<Surface>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.<Surface>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..e4438fb
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ErrorHandler.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.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 mErrorLock = new Object();
+
+    @GuardedBy("mErrorLock")
+    private ErrorListener mListener = new PrintingErrorListener();
+
+    @GuardedBy("mErrorLock")
+    private Handler mHandler = 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(final ErrorCode error, final String message) {
+        synchronized (mErrorLock) {
+            final ErrorListener listenerReference = mListener;
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    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 (mErrorLock) {
+            if (handler == null) {
+                mHandler = new Handler(Looper.getMainLooper());
+            } else {
+                mHandler = handler;
+            }
+            if (listener == null) {
+                mListener = new PrintingErrorListener();
+            } else {
+                mListener = 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..92e43ce
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/Exif.java
@@ -0,0 +1,664 @@
+/*
+ * 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 mExifInterface;
+
+    // When true, avoid saving any time. This is a privacy issue.
+    private boolean mRemoveTimestamp = false;
+
+    private Exif(ExifInterface exifInterface) {
+        mExifInterface = exifInterface;
+    }
+
+    /**
+     * Returns an Exif from the exif data contained in the file.
+     *
+     * @param file the file to read exif data from
+     */
+    public static Exif createFromFile(File file) throws IOException {
+        return createFromFileString(file.toString());
+    }
+
+    /**
+     * Returns an Exif from the exif data contained in the file at the filePath
+     *
+     * @param filePath the path to the file to read exif data from
+     */
+    public static Exif createFromFileString(String filePath) throws IOException {
+        return new Exif(new ExifInterface(filePath));
+    }
+
+    /**
+     * Returns an Exif from the exif data contain in the input stream.
+     * @param is the input stream to read exif data from
+     */
+    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 (!mRemoveTimestamp) {
+            attachLastModifiedTimestamp();
+        }
+        mExifInterface.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 mExifInterface.getAttributeInt(
+                ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
+    }
+
+    /** Returns the width of the photo in pixels. */
+    public int getWidth() {
+        return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0);
+    }
+
+    /** Returns the height of the photo in pixels. */
+    public int getHeight() {
+        return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0);
+    }
+
+    @Nullable
+    public String getDescription() {
+        return mExifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION);
+    }
+
+    /** Sets the description for the exif. */
+    public void setDescription(@Nullable String description) {
+        mExifInterface.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);
+
+        mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, datetime);
+
+        try {
+            String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
+            mExifInterface.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(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME));
+        if (timestamp == INVALID_TIMESTAMP) {
+            return INVALID_TIMESTAMP;
+        }
+
+        String subSecs = mExifInterface.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(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+        if (timestamp == INVALID_TIMESTAMP) {
+            return INVALID_TIMESTAMP;
+        }
+
+        String subSecs = mExifInterface.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 = mExifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
+        double[] latlng = mExifInterface.getLatLong();
+        double altitude = mExifInterface.getAltitude(0);
+        double speed = mExifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0);
+        String speedRef = mExifInterface.getAttribute(ExifInterface.TAG_GPS_SPEED_REF);
+        speedRef = speedRef == null ? KILOMETERS_PER_HOUR : speedRef; // Ensure speedRef is not null
+        long timestamp =
+                parseTimestamp(
+                        mExifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP),
+                        mExifInterface.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));
+            mExifInterface.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;
+            }
+        }
+        mExifInterface.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;
+        }
+        mExifInterface.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;
+        }
+        mExifInterface.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);
+
+        mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, datetime);
+        mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, datetime);
+
+        try {
+            String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime());
+            mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subsec);
+            mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subsec);
+        } catch (ParseException e) {
+        }
+
+        mRemoveTimestamp = false;
+    }
+
+    /** Removes the timestamp from the file. */
+    public void removeTimestamp() {
+        mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null);
+        mRemoveTimestamp = true;
+    }
+
+    /** Attaches the given location to the file. */
+    public void attachLocation(Location location) {
+        mExifInterface.setGpsInfo(location);
+    }
+
+    /** Removes the location from the file. */
+    public void removeLocation() {
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null);
+        mExifInterface.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, null);
+        mExifInterface.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 mMph;
+
+            Converter(double mph) {
+                mMph = mph;
+            }
+
+            double toKilometersPerHour() {
+                return mMph / 0.621371;
+            }
+
+            double toMilesPerHour() {
+                return mMph;
+            }
+
+            double toKnots() {
+                return mMph / 1.15078;
+            }
+
+            double toMetersPerSecond() {
+                return mMph / 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..18d19f3
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ExtendableUseCaseConfigFactory.java
@@ -0,0 +1,58 @@
+/*
+ * 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<?>> mDefaultProviders = 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) {
+        mDefaultProviders.put(configType, defaultProvider);
+    }
+
+    @Nullable
+    @Override
+    public <C extends UseCaseConfiguration<?>> C getConfiguration(Class<C> configType,
+            CameraX.LensFacing lensFacing) {
+        @SuppressWarnings("unchecked") // Providers only could have been inserted with
+                // installDefaultProvider(), so the class should return the correct type.
+                ConfigurationProvider<C> provider =
+                (ConfigurationProvider<C>) mDefaultProviders.get(configType);
+        if (provider != null) {
+            return provider.getConfiguration(lensFacing);
+        }
+        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..a21fd50
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/FixedSizeSurfaceTexture.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.graphics.SurfaceTexture;
+import android.os.Build.VERSION_CODES;
+import android.util.Size;
+
+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 android.view.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 android.view.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 android.view.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..60ac352
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ForwardingImageProxy.java
@@ -0,0 +1,134 @@
+/*
+ * 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.media.Image;
+
+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 mImage;
+
+    @GuardedBy("this")
+    private final Set<OnImageCloseListener> mOnImageCloseListeners = new HashSet<>();
+
+    /**
+     * Creates a new instance which wraps the given image.
+     *
+     * @param image to wrap
+     * @return new {@link AndroidImageProxy} instance
+     */
+    protected ForwardingImageProxy(ImageProxy image) {
+        mImage = image;
+    }
+
+    @Override
+    public synchronized void close() {
+        mImage.close();
+        notifyOnImageCloseListeners();
+    }
+
+    @Override
+    public synchronized Rect getCropRect() {
+        return mImage.getCropRect();
+    }
+
+    @Override
+    public synchronized void setCropRect(Rect rect) {
+        mImage.setCropRect(rect);
+    }
+
+    @Override
+    public synchronized int getFormat() {
+        return mImage.getFormat();
+    }
+
+    @Override
+    public synchronized int getHeight() {
+        return mImage.getHeight();
+    }
+
+    @Override
+    public synchronized int getWidth() {
+        return mImage.getWidth();
+    }
+
+    @Override
+    public synchronized long getTimestamp() {
+        return mImage.getTimestamp();
+    }
+
+    @Override
+    public synchronized void setTimestamp(long timestamp) {
+        mImage.setTimestamp(timestamp);
+    }
+
+    @Override
+    public synchronized ImageProxy.PlaneProxy[] getPlanes() {
+        return mImage.getPlanes();
+    }
+
+    @Override
+    public synchronized ImageInfo getImageInfo() {
+        return mImage.getImageInfo();
+    }
+
+    @Override
+    public synchronized Image getImage() {
+        return mImage.getImage();
+    }
+
+    /**
+     * Adds a listener for close calls on this image.
+     *
+     * @param listener to add
+     */
+    synchronized void addOnImageCloseListener(OnImageCloseListener listener) {
+        mOnImageCloseListeners.add(listener);
+    }
+
+    /** Notifies the listeners that this image has been closed. */
+    protected synchronized void notifyOnImageCloseListeners() {
+        for (OnImageCloseListener listener : mOnImageCloseListeners) {
+            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..3b92666
--- /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> mImageReaders;
+
+    /**
+     * 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) {
+        mImageReaders = 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 : mImageReaders) {
+            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..30c2b2f
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCase.java
@@ -0,0 +1,379 @@
+/*
+ * 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.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";
+    final AtomicReference<Analyzer> mSubscribedAnalyzer;
+    final AtomicInteger mRelativeRotation = new AtomicInteger();
+    private final Handler mHandler;
+    private final ImageAnalysisUseCaseConfiguration.Builder mUseCaseConfigBuilder;
+    @Nullable
+    ImageReaderProxy mImageReader;
+    @Nullable
+    private DeferrableSurface mDeferrableSurface;
+
+    /**
+     * Creates a new image analysis use case from the given configuration.
+     *
+     * @param configuration for this use case instance
+     */
+    public ImageAnalysisUseCase(ImageAnalysisUseCaseConfiguration configuration) {
+        super(configuration);
+        mUseCaseConfigBuilder = ImageAnalysisUseCaseConfiguration.Builder.fromConfig(configuration);
+
+        // Get the combined configuration with defaults
+        ImageAnalysisUseCaseConfiguration combinedConfig =
+                (ImageAnalysisUseCaseConfiguration) getUseCaseConfiguration();
+        mSubscribedAnalyzer = new AtomicReference<>();
+        mHandler = combinedConfig.getCallbackHandler(null);
+        if (mHandler == null) {
+            throw new IllegalStateException("No default mHandler 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) {
+            mUseCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(mUseCaseConfigBuilder.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 mSubscribedAnalyzer.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 = mSubscribedAnalyzer.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 (mDeferrableSurface != null) {
+            mDeferrableSurface.setOnSurfaceDetachedListener(
+                    MainThreadExecutor.getInstance(),
+                    new DeferrableSurface.OnSurfaceDetachedListener() {
+                        @Override
+                        public void onSurfaceDetached() {
+                            if (mImageReader != null) {
+                                mImageReader.close();
+                                mImageReader = null;
+                            }
+                        }
+                    });
+        }
+        super.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @Nullable
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
+        ImageAnalysisUseCaseConfiguration defaults = CameraX.getDefaultUseCaseConfiguration(
+                ImageAnalysisUseCaseConfiguration.class, lensFacing);
+        if (defaults != null) {
+            return ImageAnalysisUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        final 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 (mImageReader != null) {
+            mImageReader.close();
+        }
+
+        mImageReader =
+                ImageReaderProxys.createCompatibleReader(
+                        cameraId,
+                        resolution.getWidth(),
+                        resolution.getHeight(),
+                        getImageFormat(),
+                        configuration.getImageQueueDepth(),
+                        mHandler);
+
+        tryUpdateRelativeRotation(cameraId);
+        mImageReader.setOnImageAvailableListener(
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        Analyzer analyzer = mSubscribedAnalyzer.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, mRelativeRotation.get());
+                            }
+                        }
+                    }
+                },
+                mHandler);
+
+        SessionConfiguration.Builder sessionConfigBuilder =
+                SessionConfiguration.Builder.createFrom(configuration);
+
+        mDeferrableSurface = new ImmediateSurface(mImageReader.getSurface());
+
+        sessionConfigBuilder.addSurface(mDeferrableSurface);
+
+        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);
+            mRelativeRotation.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 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)
+                            .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(LensFacing lensFacing) {
+            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..58dd3eb9
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageAnalysisUseCaseConfiguration.java
@@ -0,0 +1,573 @@
+/*
+ * 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.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageAnalysisUseCase.ImageReaderMode;
+
+import java.util.Set;
+import java.util.UUID;
+
+/** 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 mConfig;
+
+    ImageAnalysisUseCaseConfiguration(OptionsBundle config) {
+        mConfig = 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 mConfig;
+    }
+
+    /** 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 mMutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            mMutableConfig = 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 mMutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public ImageAnalysisUseCaseConfiguration build() {
+            return new ImageAnalysisUseCaseConfiguration(OptionsBundle.from(mMutableConfig));
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // Implementations of TargetConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetClass(Class<ImageAnalysisUseCase> 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();
+        }
+
+        @Override
+        public Builder setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+
+        // Implementations of CameraDeviceConfiguration.Builder default methods
+
+        @Override
+        public Builder setLensFacing(CameraX.LensFacing lensFacing) {
+            getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+            return builder();
+        }
+
+        // Implementations of ImageOutputConfiguration.Builder default methods
+
+        @Override
+        public Builder setTargetAspectRatio(Rational aspectRatio) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return builder();
+        }
+
+        @Override
+        public Builder setTargetRotation(@RotationValue int rotation) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setMaxResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return builder();
+        }
+
+        // Implementations of ThreadConfiguration.Builder default methods
+
+        @Override
+        public Builder setCallbackHandler(Handler handler) {
+            getMutableConfiguration().insertOption(OPTION_CALLBACK_HANDLER, handler);
+            return builder();
+        }
+
+        // Implementations of UseCaseConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+            getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+            getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // Implementations of TargetConfiguration default methods
+
+    @Override
+    @Nullable
+    public Class<ImageAnalysisUseCase> getTargetClass(
+            @Nullable Class<ImageAnalysisUseCase> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<ImageAnalysisUseCase> storedClass =
+                (Class<ImageAnalysisUseCase>) retrieveOption(
+                        OPTION_TARGET_CLASS,
+                        valueIfMissing);
+        return storedClass;
+    }
+
+    @Override
+    public Class<ImageAnalysisUseCase> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<ImageAnalysisUseCase> storedClass =
+                (Class<ImageAnalysisUseCase>) retrieveOption(
+                        OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    @Override
+    @Nullable
+    public String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    @Override
+    public String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    // Implementations of CameraDeviceConfiguration default methods
+
+    @Override
+    @Nullable
+    public CameraX.LensFacing getLensFacing(@Nullable CameraX.LensFacing valueIfMissing) {
+        return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+    }
+
+    @Override
+    public CameraX.LensFacing getLensFacing() {
+        return retrieveOption(OPTION_LENS_FACING);
+    }
+
+    // Implementations of ImageOutputConfiguration default methods
+
+    @Override
+    @Nullable
+    public Rational getTargetAspectRatio(@Nullable Rational valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO, valueIfMissing);
+    }
+
+    @Override
+    public Rational getTargetAspectRatio() {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation(int valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ROTATION, valueIfMissing);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation() {
+        return retrieveOption(OPTION_TARGET_ROTATION);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_MAX_RESOLUTION, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution() {
+        return retrieveOption(OPTION_MAX_RESOLUTION);
+    }
+
+    // Implementations of ThreadConfiguration default methods
+
+    @Override
+    @Nullable
+    public Handler getCallbackHandler(@Nullable Handler valueIfMissing) {
+        return retrieveOption(OPTION_CALLBACK_HANDLER, valueIfMissing);
+    }
+
+    @Override
+    public Handler getCallbackHandler() {
+        return retrieveOption(OPTION_CALLBACK_HANDLER);
+    }
+
+    // Implementations of UseCaseConfiguration default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration valueIfMissing) {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration getDefaultSessionConfiguration() {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+        return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+        return retrieveOption(OPTION_CONFIG_UNPACKER);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority(int valueIfMissing) {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority() {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+}
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..39819b1
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCase.java
@@ -0,0 +1,1172 @@
+/*
+ * 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.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.base.Function;
+import com.google.common.util.concurrent.AsyncCallable;
+import com.google.common.util.concurrent.AsyncFunction;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+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 mHandler;
+    final Handler mMainHandler = new Handler(Looper.getMainLooper());
+    private final SessionConfiguration.Builder mSessionConfigBuilder;
+    final ArrayDeque<ImageCaptureRequest> mImageCaptureRequests = new ArrayDeque<>();
+    private final ExecutorService mExecutor =
+            Executors.newFixedThreadPool(
+                    1,
+                    new ThreadFactory() {
+                        private final AtomicInteger mId = new AtomicInteger(0);
+
+                        @Override
+                        public Thread newThread(Runnable r) {
+                            return new Thread(
+                                    r,
+                                    CameraXThreads.TAG + "image_capture_" + mId.getAndIncrement());
+                        }
+                    });
+    private final CaptureCallbackChecker mSessionCallbackChecker = new CaptureCallbackChecker();
+    private final CaptureMode mCaptureMode;
+
+    /** The set of requests that will be sent to the camera for the final captured image. */
+    private final CaptureBundle mCaptureBundle;
+
+    /**
+     * Processing that gets done to the mCaptureBundle to produce the final image that is produced
+     * by {@link #takePicture(OnImageCapturedListener)}
+     */
+    private final CaptureProcessor mCaptureProcessor;
+
+    /** Callback used to match the {@link ImageProxy} with the {@link ImageInfo}. */
+    private CameraCaptureCallback mMetadataMatchingCaptureCallback;
+
+    private final ImageCaptureUseCaseConfiguration.Builder mUseCaseConfigBuilder;
+    private ImageCaptureUseCaseConfiguration mConfiguration;
+    ImageReaderProxy mImageReader;
+    private DeferrableSurface mDeferrableSurface;
+    /**
+     * 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 mEnableCheck3AConverged;
+    /** Current flash mode. */
+    private FlashMode mFlashMode;
+
+    /**
+     * Creates a new image capture use case from the given configuration.
+     *
+     * @param userConfiguration for this use case instance
+     */
+    public ImageCaptureUseCase(ImageCaptureUseCaseConfiguration userConfiguration) {
+        super(userConfiguration);
+        mUseCaseConfigBuilder =
+                ImageCaptureUseCaseConfiguration.Builder.fromConfig(userConfiguration);
+        // Ensure we're using the combined configuration (user config + defaults)
+        mConfiguration = (ImageCaptureUseCaseConfiguration) getUseCaseConfiguration();
+        mCaptureMode = mConfiguration.getCaptureMode();
+        mFlashMode = mConfiguration.getFlashMode();
+
+        mCaptureProcessor = mConfiguration.getCaptureProcessor(null);
+
+        if (mCaptureProcessor != null) {
+            setImageFormat(ImageFormat.YUV_420_888);
+        } else {
+            setImageFormat(ImageReaderFormatRecommender.chooseCombo().imageCaptureFormat());
+        }
+
+        mCaptureBundle = mConfiguration.getCaptureBundle(
+                CaptureBundles.singleDefaultCaptureBundle());
+
+        if (mCaptureBundle.getCaptureStages().size() > 1 && mCaptureProcessor == null) {
+            throw new IllegalArgumentException(
+                    "ImageCaptureUseCaseConfiguration has no CaptureProcess set with "
+                            + "CaptureBundle size > 1.");
+        }
+
+        if (mCaptureMode == CaptureMode.MAX_QUALITY) {
+            mEnableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
+        } else if (mCaptureMode == CaptureMode.MIN_LATENCY) {
+            mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
+        }
+
+        mHandler = mConfiguration.getCallbackHandler(null);
+        if (mHandler == null) {
+            throw new IllegalStateException("No default handler specified.");
+        }
+
+        mSessionConfigBuilder = SessionConfiguration.Builder.createFrom(mConfiguration);
+        mSessionConfigBuilder.setCameraCaptureCallback(mSessionCallbackChecker);
+    }
+
+    private static 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
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
+        ImageCaptureUseCaseConfiguration defaults = CameraX.getDefaultUseCaseConfiguration(
+                ImageCaptureUseCaseConfiguration.class, lensFacing);
+        if (defaults != null) {
+            return ImageCaptureUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    private CameraControl getCurrentCameraControl() {
+        String cameraId = getCameraIdUnchecked(mConfiguration.getLensFacing());
+        return getCameraControl(cameraId);
+    }
+
+    /** Configures flash mode to CameraControl once it is ready. */
+    @Override
+    protected void onCameraControlReady(String cameraId) {
+        getCameraControl(cameraId).setFlashMode(mFlashMode);
+    }
+
+    /**
+     * Get the flash mode.
+     *
+     * @return the {@link FlashMode}.
+     */
+    public FlashMode getFlashMode() {
+        return mFlashMode;
+    }
+
+    /**
+     * Set the flash mode.
+     *
+     * @param flashMode the {@link FlashMode}.
+     */
+    public void setFlashMode(FlashMode flashMode) {
+        this.mFlashMode = 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)) {
+            mUseCaseConfigBuilder.setTargetAspectRatio(aspectRatio);
+            updateUseCaseConfiguration(mUseCaseConfigBuilder.build());
+            mConfiguration = (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) {
+            mUseCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(mUseCaseConfigBuilder.build());
+            mConfiguration = (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
+     */
+    public void takePicture(final OnImageCapturedListener listener) {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mMainHandler.post(
+                    new Runnable() {
+                        @Override
+                        public void run() {
+                            ImageCaptureUseCase.this.takePicture(listener);
+                        }
+                    });
+            return;
+        }
+
+        sendImageCaptureRequest(listener, mHandler);
+    }
+
+    /**
+     * 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(
+            final File saveLocation, final OnImageSavedListener imageSavedListener,
+            final Metadata metadata) {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mMainHandler.post(
+                    new Runnable() {
+                        @Override
+                        public void run() {
+                            ImageCaptureUseCase.this.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
+        final 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);
+                    }
+                };
+
+        // 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 = (mHandler != null) ? mHandler : mMainHandler;
+                        IoExecutor.getInstance()
+                                .execute(
+                                        new ImageSaver(
+                                                image,
+                                                saveLocation,
+                                                rotationDegrees,
+                                                metadata.isReversedHorizontal,
+                                                metadata.isReversedVertical,
+                                                metadata.location,
+                                                imageSavedListenerWrapper,
+                                                completionHandler));
+                    }
+
+                    @Override
+                    public void onError(
+                            UseCaseError error, String message, @Nullable Throwable cause) {
+                        imageSavedListener.onError(error, message, cause);
+                    }
+                };
+
+        // Always use the mMainHandler for the initial callback so we don't need to double post to
+        // another thread
+        sendImageCaptureRequest(imageCaptureCallbackWrapper, mMainHandler);
+    }
+
+    @UiThread
+    private void sendImageCaptureRequest(
+            OnImageCapturedListener listener, @Nullable Handler listenerHandler) {
+
+        String cameraId = getCameraIdUnchecked(mConfiguration.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(
+                            mConfiguration.getTargetRotation(Surface.ROTATION_0));
+        } catch (CameraInfoUnavailableException e) {
+            Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
+        }
+
+        Rational targetRatio = mConfiguration.getTargetAspectRatio(null);
+        targetRatio = ImageUtil.rotate(targetRatio, relativeRotation);
+
+        mImageCaptureRequests.offer(
+                new ImageCaptureRequest(listener, listenerHandler, relativeRotation, targetRatio));
+        if (mImageCaptureRequests.size() == 1) {
+            issueImageCaptureRequests();
+        }
+    }
+
+    /** Issues saved ImageCaptureRequest. */
+    @UiThread
+    void issueImageCaptureRequests() {
+        if (mImageCaptureRequests.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() {
+        final TakePictureState state = new TakePictureState();
+
+        FluentFuture.from(preTakePicture(state))
+                .transformAsync(new AsyncFunction<Void, Void>() {
+                    @Override
+                    public ListenableFuture<Void> apply(Void v) throws Exception {
+                        return ImageCaptureUseCase.this.issueTakePicture();
+                    }
+                }, mExecutor)
+                .transformAsync(new AsyncFunction<Void, Void>() {
+                    @Override
+                    public ListenableFuture<Void> apply(Void v) throws Exception {
+                        return ImageCaptureUseCase.this.postTakePicture(state);
+                    }
+                }, mExecutor)
+                .addCallback(
+                        new FutureCallback<Void>() {
+                            @Override
+                            public void onSuccess(Void result) {
+                            }
+
+                            @Override
+                            public void onFailure(Throwable t) {
+                                Log.e(TAG, "takePictureInternal onFailure", t);
+                            }
+                        },
+                        mExecutor);
+    }
+
+    @Override
+    public String toString() {
+        return TAG + ":" + getName();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void clear() {
+        if (mDeferrableSurface != null) {
+            mDeferrableSurface.setOnSurfaceDetachedListener(
+                    MainThreadExecutor.getInstance(),
+                    new DeferrableSurface.OnSurfaceDetachedListener() {
+                        @Override
+                        public void onSurfaceDetached() {
+                            if (mImageReader != null) {
+                                mImageReader.close();
+                                mImageReader = null;
+                            }
+                        }
+                    });
+        }
+        mExecutor.shutdown();
+        super.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        String cameraId = getCameraIdUnchecked(mConfiguration.getLensFacing());
+        Size resolution = suggestedResolutionMap.get(cameraId);
+        if (resolution == null) {
+            throw new IllegalArgumentException(
+                    "Suggested resolution map missing resolution for camera " + cameraId);
+        }
+
+        if (mImageReader != null) {
+            if (mImageReader.getHeight() == resolution.getHeight()
+                    && mImageReader.getWidth() == resolution.getWidth()) {
+                // Resolution does not need to be updated. Return early.
+                return suggestedResolutionMap;
+            }
+            mImageReader.close();
+        }
+
+        // Setup the ImageReader to do processing
+        if (mCaptureProcessor != null) {
+            ProcessingImageReader processingImageReader =
+                    new ProcessingImageReader(
+                            resolution.getWidth(),
+                            resolution.getHeight(),
+                            getImageFormat(), MAX_IMAGES,
+                            mHandler, mCaptureBundle, mCaptureProcessor);
+            mMetadataMatchingCaptureCallback = processingImageReader.getCameraCaptureCallback();
+            mImageReader = processingImageReader;
+        } else {
+            MetadataImageReader metadataImageReader = new MetadataImageReader(resolution.getWidth(),
+                    resolution.getHeight(), getImageFormat(), MAX_IMAGES, mHandler);
+            mMetadataMatchingCaptureCallback = metadataImageReader.getCameraCaptureCallback();
+            mImageReader = metadataImageReader;
+        }
+
+        mImageReader.setOnImageAvailableListener(
+                new ImageReaderProxy.OnImageAvailableListener() {
+                    @Override
+                    public void onImageAvailable(ImageReaderProxy imageReader) {
+                        // Call the listener so that the captured image can be processed.
+                        ImageCaptureRequest imageCaptureRequest = mImageCaptureRequests.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
+                                    mImageCaptureRequests.poll();
+
+                                    // Inform the listener
+                                    imageCaptureRequest.dispatchImage(image);
+
+                                    ImageCaptureUseCase.this.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();
+                                }
+                            }
+                        }
+                    }
+                },
+                mMainHandler);
+
+        mSessionConfigBuilder.clearSurfaces();
+
+        mDeferrableSurface = new ImmediateSurface(mImageReader.getSurface());
+        mSessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface);
+
+        attachToCamera(cameraId, mSessionConfigBuilder.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(final TakePictureState state) {
+        return FluentFuture.from(getPreCaptureStateIfNeeded())
+                .transformAsync(
+                        new AsyncFunction<CameraCaptureResult, Boolean>() {
+                            @Override
+                            public ListenableFuture<Boolean> apply(
+                                    CameraCaptureResult captureResult) throws Exception {
+                                state.mPreCaptureState = captureResult;
+                                ImageCaptureUseCase.this.triggerAfIfNeeded(state);
+
+                                if (ImageCaptureUseCase.this.isFlashRequired(state)) {
+                                    state.mIsFlashTriggered = true;
+                                    ImageCaptureUseCase.this.triggerAePrecapture(state);
+                                }
+                                return ImageCaptureUseCase.this.check3AConverged(state);
+                            }
+                        },
+                        mExecutor)
+                // Ignore the 3A convergence result.
+                .transform(new Function<Boolean, Void>() {
+                    @Override
+                    public Void apply(Boolean is3AConverged) {
+                        return null;
+                    }
+                }, mExecutor);
+    }
+
+    /**
+     * Routine after picture was taken.
+     *
+     * <p>For example, cancel 3A scan, close torch if necessary.
+     */
+    ListenableFuture<Void> postTakePicture(final TakePictureState state) {
+        return Futures.submitAsync(
+                new AsyncCallable<Void>() {
+                    @Override
+                    public ListenableFuture<Void> call() throws Exception {
+                        ImageCaptureUseCase.this.cancelAfAeTrigger(state);
+                        return Futures.immediateFuture(null);
+                    }
+                },
+                mExecutor);
+    }
+
+    /**
+     * 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 (mEnableCheck3AConverged || getFlashMode() == FlashMode.AUTO) {
+            return mSessionCallbackChecker.checkCaptureResult(
+                    new CaptureCallbackChecker.CaptureResultChecker<CameraCaptureResult>() {
+                        @Override
+                        public CameraCaptureResult check(
+                                @NonNull CameraCaptureResult captureResult) {
+                            return captureResult;
+                        }
+                    });
+        }
+        return Futures.immediateFuture(null);
+    }
+
+    boolean isFlashRequired(TakePictureState state) {
+        switch (getFlashMode()) {
+            case ON:
+                return true;
+            case AUTO:
+                return state.mPreCaptureState.getAeState() == AeState.FLASH_REQUIRED;
+            case OFF:
+                return false;
+        }
+        throw new AssertionError(getFlashMode());
+    }
+
+    ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
+        // Besides enableCheck3AConverged == true (MAX_QUALITY), if flash is triggered we also need
+        // to
+        // wait for 3A convergence.
+        if (!mEnableCheck3AConverged && !state.mIsFlashTriggered) {
+            return Futures.immediateFuture(false);
+        }
+
+        return mSessionCallbackChecker.checkCaptureResult(
+                new CaptureCallbackChecker.CaptureResultChecker<Boolean>() {
+                    @Override
+                    public Boolean check(@NonNull CameraCaptureResult 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.
+     */
+    void triggerAfIfNeeded(TakePictureState state) {
+        if (mEnableCheck3AConverged
+                && state.mPreCaptureState.getAfMode() == AfMode.ON_MANUAL_AUTO
+                && state.mPreCaptureState.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.mIsAfTriggered = true;
+        getCurrentCameraControl().triggerAf();
+    }
+
+    /**
+     * Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
+     * exposure scan.
+     */
+    void triggerAePrecapture(TakePictureState state) {
+        state.mIsAePrecaptureTriggered = 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.
+     */
+    void cancelAfAeTrigger(TakePictureState state) {
+        if (!state.mIsAfTriggered && !state.mIsAePrecaptureTriggered) {
+            return;
+        }
+        getCurrentCameraControl()
+                .cancelAfAeTrigger(state.mIsAfTriggered, state.mIsAePrecaptureTriggered);
+        state.mIsAfTriggered = false;
+        state.mIsAePrecaptureTriggered = 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. */
+    ListenableFuture<Void> issueTakePicture() {
+        List<SettableFuture<Void>> futureList = new ArrayList<>();
+
+        for (CaptureStage captureStage : mCaptureBundle.getCaptureStages()) {
+            CaptureRequestConfiguration.Builder builder = new CaptureRequestConfiguration.Builder();
+            builder.addSurface(new ImmediateSurface(mImageReader.getSurface()));
+            builder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+
+            applyPixelHdrPlusChangeForCaptureMode(mCaptureMode, builder);
+
+            builder.addImplementationOptions(
+                    captureStage.getCaptureRequestConfiguration().getImplementationOptions());
+
+            builder.setTag(captureStage.getCaptureRequestConfiguration().getTag());
+
+            final SettableFuture<Void> future = SettableFuture.create();
+            builder.setCameraCaptureCallback(new CameraCaptureCallbacks.ComboCameraCaptureCallback(
+                    Arrays.asList(
+                            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);
+                                }
+                            },
+                            mMetadataMatchingCaptureCallback
+                    ))
+            );
+
+            futureList.add(future);
+            getCurrentCameraControl().submitSingleRequest(builder.build());
+        }
+
+        return Futures.whenAllSucceed(futureList).call(
+                new Callable<Void>() {
+                    @Override
+                    public Void call() {
+                        return null;
+                    }
+                },
+                mExecutor);
+    }
+
+    /**
+     * Describes the error that occurred during an image capture operation (such as {@link
+     * ImageCaptureUseCase#takePicture(OnImageCapturedListener)}).
+     *
+     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
+     * ImageCaptureUseCase.OnImageSavedListener#onError(UseCaseError, String, Throwable)}.
+     */
+    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.
+     */
+    public abstract static class OnImageCapturedListener {
+        /**
+         * Callback for when the image has been captured.
+         *
+         * <p>The listener is responsible for closing the supplied {@link Image}.
+         */
+        public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
+            image.close();
+        }
+
+        /** Callback for when an error occurred during image capture. */
+        public 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 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)
+                            .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+
+            DEFAULT_CONFIG = builder.build();
+        }
+
+        @Override
+        public ImageCaptureUseCaseConfiguration getConfiguration(LensFacing lensFacing) {
+            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. */
+        @Nullable
+        public 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 mPreCaptureState = EmptyCameraCaptureResult.create();
+        boolean mIsAfTriggered = false;
+        boolean mIsAePrecaptureTriggered = false;
+        boolean mIsFlashTriggered = 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> mCaptureResultListeners = 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(
+                final CaptureResultChecker<T> checker, final long timeoutInMs, final T defValue) {
+            if (timeoutInMs < NO_TIMEOUT) {
+                throw new IllegalArgumentException("Invalid timeout value: " + timeoutInMs);
+            }
+            final long startTimeInMs =
+                    (timeoutInMs != NO_TIMEOUT) ? SystemClock.elapsedRealtime() : 0L;
+
+            final SettableFuture<T> future = SettableFuture.create();
+            addListener(
+                    new CaptureResultListener() {
+                        @Override
+                        public boolean onCaptureResult(@NonNull CameraCaptureResult 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#mCaptureResultListeners}.
+         */
+        private void deliverCaptureResultToListeners(@NonNull CameraCaptureResult captureResult) {
+            synchronized (mCaptureResultListeners) {
+                Set<CaptureResultListener> removeSet = null;
+                for (CaptureResultListener listener : new HashSet<>(mCaptureResultListeners)) {
+                    // Remove listener if the callback return true
+                    if (listener.onCaptureResult(captureResult)) {
+                        if (removeSet == null) {
+                            removeSet = new HashSet<>();
+                        }
+                        removeSet.add(listener);
+                    }
+                }
+                if (removeSet != null) {
+                    mCaptureResultListeners.removeAll(removeSet);
+                }
+            }
+        }
+
+        /** Add capture result listener. */
+        private void addListener(CaptureResultListener listener) {
+            synchronized (mCaptureResultListeners) {
+                mCaptureResultListeners.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 mListener;
+        @Nullable
+        Handler mHandler;
+        @RotationValue
+        int mRotationDegrees;
+        Rational mTargetRatio;
+
+        ImageCaptureRequest(
+                OnImageCapturedListener listener,
+                @Nullable Handler handler,
+                @RotationValue int rotationDegrees,
+                Rational targetRatio) {
+            mListener = listener;
+            mHandler = handler;
+            mRotationDegrees = rotationDegrees;
+            mTargetRatio = targetRatio;
+        }
+
+        void dispatchImage(final ImageProxy image) {
+            if (mHandler != null && Looper.myLooper() != mHandler.getLooper()) {
+                boolean posted =
+                        mHandler.post(
+                                new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        ImageCaptureRequest.this.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;
+            }
+
+            Size sourceSize = new Size(image.getWidth(), image.getHeight());
+            if (ImageUtil.isAspectRatioValid(sourceSize, mTargetRatio)) {
+                image.setCropRect(
+                        ImageUtil.computeCropRectFromAspectRatio(sourceSize, mTargetRatio));
+            }
+
+            mListener.onCaptureSuccess(image, mRotationDegrees);
+        }
+    }
+}
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..8e65772
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageCaptureUseCaseConfiguration.java
@@ -0,0 +1,609 @@
+/*
+ * 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.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageCaptureUseCase.CaptureMode;
+
+import java.util.Set;
+import java.util.UUID;
+
+/** 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);
+    static final Option<CaptureBundle> OPTION_CAPTURE_BUNDLE =
+            Option.create("camerax.core.imageCapture.captureBundle", CaptureBundle.class);
+    static final Option<CaptureProcessor> OPTION_CAPTURE_PROCESSOR =
+            Option.create("camerax.core.imageCapture.captureProcessor", CaptureProcessor.class);
+    private final OptionsBundle mConfig;
+
+    /** Creates a new configuration instance. */
+    ImageCaptureUseCaseConfiguration(OptionsBundle config) {
+        mConfig = config;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Configuration getConfiguration() {
+        return mConfig;
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Returns the {@link CaptureBundle}.
+     *
+     * @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
+    public CaptureBundle getCaptureBundle(@Nullable CaptureBundle valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_CAPTURE_BUNDLE, valueIfMissing);
+    }
+
+    /**
+     * Returns the {@link CaptureBundle}.
+     *
+     * @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 CaptureBundle getCaptureBundle() {
+        return getConfiguration().retrieveOption(OPTION_CAPTURE_BUNDLE);
+    }
+
+    /**
+     * Returns the {@link CaptureProcessor}.
+     *
+     * @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
+    public CaptureProcessor getCaptureProcessor(@Nullable CaptureProcessor valueIfMissing) {
+        return getConfiguration().retrieveOption(OPTION_CAPTURE_PROCESSOR, valueIfMissing);
+    }
+
+    /**
+     * Returns the {@link CaptureProcessor}.
+     *
+     * @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 CaptureProcessor getCaptureProcessor() {
+        return getConfiguration().retrieveOption(OPTION_CAPTURE_PROCESSOR);
+    }
+
+    /** 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 mMutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            mMutableConfig = 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 mMutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public ImageCaptureUseCaseConfiguration build() {
+            return new ImageCaptureUseCaseConfiguration(OptionsBundle.from(mMutableConfig));
+        }
+
+        /**
+         * 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();
+        }
+
+        /**
+         * Sets the {@link CaptureBundle}.
+         *
+         * @param captureBundle The requested capture bundle for extension.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setCaptureBundle(CaptureBundle captureBundle) {
+            getMutableConfiguration().insertOption(OPTION_CAPTURE_BUNDLE, captureBundle);
+            return builder();
+        }
+
+        /**
+         * Sets the {@link CaptureProcessor}.
+         *
+         * @param captureProcessor The requested capture processor for extension.
+         * @return The current Builder.
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        public Builder setCaptureProcessor(CaptureProcessor captureProcessor) {
+            getMutableConfiguration().insertOption(OPTION_CAPTURE_PROCESSOR, captureProcessor);
+            return builder();
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // Implementations of TargetConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetClass(Class<ImageCaptureUseCase> 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();
+        }
+
+        @Override
+        public Builder setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+
+        // Implementations of CameraDeviceConfiguration.Builder default methods
+
+        @Override
+        public Builder setLensFacing(CameraX.LensFacing lensFacing) {
+            getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+            return builder();
+        }
+
+        // Implementations of ImageOutputConfiguration.Builder default methods
+
+        @Override
+        public Builder setTargetAspectRatio(Rational aspectRatio) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return builder();
+        }
+
+        @Override
+        public Builder setTargetRotation(@RotationValue int rotation) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setMaxResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return builder();
+        }
+
+        // Implementations of ThreadConfiguration.Builder default methods
+
+        @Override
+        public Builder setCallbackHandler(Handler handler) {
+            getMutableConfiguration().insertOption(OPTION_CALLBACK_HANDLER, handler);
+            return builder();
+        }
+
+        // Implementations of UseCaseConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+            getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+            getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // Implementations of TargetConfiguration default methods
+
+    @Override
+    @Nullable
+    public Class<ImageCaptureUseCase> getTargetClass(
+            @Nullable Class<ImageCaptureUseCase> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<ImageCaptureUseCase> storedClass =
+                (Class<ImageCaptureUseCase>) retrieveOption(
+                        OPTION_TARGET_CLASS,
+                        valueIfMissing);
+        return storedClass;
+    }
+
+    @Override
+    public Class<ImageCaptureUseCase> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<ImageCaptureUseCase> storedClass =
+                (Class<ImageCaptureUseCase>) retrieveOption(
+                        OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    @Override
+    @Nullable
+    public String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    @Override
+    public String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    // Implementations of CameraDeviceConfiguration default methods
+
+    @Override
+    @Nullable
+    public CameraX.LensFacing getLensFacing(@Nullable CameraX.LensFacing valueIfMissing) {
+        return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+    }
+
+    @Override
+    public CameraX.LensFacing getLensFacing() {
+        return retrieveOption(OPTION_LENS_FACING);
+    }
+
+    // Implementations of ImageOutputConfiguration default methods
+
+    @Override
+    @Nullable
+    public Rational getTargetAspectRatio(@Nullable Rational valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO, valueIfMissing);
+    }
+
+    @Override
+    public Rational getTargetAspectRatio() {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation(int valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ROTATION, valueIfMissing);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation() {
+        return retrieveOption(OPTION_TARGET_ROTATION);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getTargetResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_RESOLUTION, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getTargetResolution() {
+        return retrieveOption(OPTION_TARGET_RESOLUTION);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_MAX_RESOLUTION, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution() {
+        return retrieveOption(OPTION_MAX_RESOLUTION);
+    }
+
+    // Implementations of ThreadConfiguration default methods
+
+    @Override
+    @Nullable
+    public Handler getCallbackHandler(@Nullable Handler valueIfMissing) {
+        return retrieveOption(OPTION_CALLBACK_HANDLER, valueIfMissing);
+    }
+
+    @Override
+    public Handler getCallbackHandler() {
+        return retrieveOption(OPTION_CALLBACK_HANDLER);
+    }
+
+    // Implementations of UseCaseConfiguration default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration valueIfMissing) {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration getDefaultSessionConfiguration() {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+        return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+        return retrieveOption(OPTION_CONFIG_UNPACKER);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority(int valueIfMissing) {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority() {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+
+}
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/ImageInfo.java b/camera/core/src/main/java/androidx/camera/core/ImageInfo.java
new file mode 100644
index 0000000..b799bca
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageInfo.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/** Metadata for an image. */
+public interface ImageInfo {
+    /** Returns the tag of the metadata. */
+    Object getTag();
+    /** Returns the timestamp of the metadata. */
+    long getTimestamp();
+}
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..f4edcf4
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageOutputConfiguration.java
@@ -0,0 +1,239 @@
+/*
+ * 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
+    Rational getTargetAspectRatio(@Nullable Rational 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.
+     */
+    Rational getTargetAspectRatio();
+
+    /**
+     * 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
+    int getTargetRotation(int 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
+    int getTargetRotation();
+
+    /**
+     * 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)
+    Size getTargetResolution(Size 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)
+    Size getTargetResolution();
+
+    // 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)
+    Size getMaxResolution(Size 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)
+    Size getMaxResolution();
+
+    /**
+     * 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.
+         */
+        T setTargetAspectRatio(Rational aspectRatio);
+
+        /**
+         * 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.
+         */
+        T setTargetRotation(@RotationValue int rotation);
+
+        /**
+         * 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)
+        T setTargetResolution(Size resolution);
+
+        /**
+         * 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)
+        T setMaxResolution(Size resolution);
+    }
+
+    /**
+     * Valid integer rotation values.
+     *
+     * @hide
+     */
+    @IntDef({Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270})
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @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..a65980d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageProxy.java
@@ -0,0 +1,140 @@
+/*
+ * 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.media.Image;
+
+import java.nio.ByteBuffer;
+
+import javax.annotation.Nullable;
+
+/** 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.
+
+    /**
+     * Returns the {@link ImageInfo}.
+     *
+     * <p> Will be null if there is no associated additional metadata.
+     */
+    @Nullable
+    ImageInfo getImageInfo();
+
+    /**
+     * Returns the android {@link Image}.
+     *
+     * <p>If the ImageProxy is a wrapper for an android {@link Image}, it will return the
+     * {@link Image}. It is possible for an ImageProxy to wrap something that isn't an
+     * {@link Image}. If that's the case then it will return null.
+     *
+     * <p>The returned image should not be closed by the application on finishing using it,
+     * instead it should be closed by the ImageProxy.
+     *
+     * @return the android image.
+     */
+    @Nullable
+    Image getImage();
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/ImageProxyBundle.java b/camera/core/src/main/java/androidx/camera/core/ImageProxyBundle.java
new file mode 100644
index 0000000..dd61793
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageProxyBundle.java
@@ -0,0 +1,40 @@
+/*
+ * 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 com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+
+/** A set of {@link ImageProxy} which are mapped an identifier. */
+public interface ImageProxyBundle {
+    /**
+     * Get a {@link ListenableFuture} for a {@link ImageProxy}.
+     *
+     * <p> The future will be satisfied when the {@link ImageProxy} for the given identifier has
+     * been generated.
+     *
+     * @param captureId The id for the captures that generated the {@link ImageProxy}.
+     */
+    ListenableFuture<ImageProxy> getImageProxy(int captureId);
+
+    /**
+     * Returns the list of identifiers for the capture that produced the data in
+     * this bundle.
+     */
+    List<Integer> getCaptureIds();
+}
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..0bbbf5b
--- /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(
+            final int rowStride, final int pixelStride, final byte[] data) {
+        return new ImageProxy.PlaneProxy() {
+            final ByteBuffer mBuffer = ByteBuffer.wrap(data);
+
+            @Override
+            public int getRowStride() {
+                return rowStride;
+            }
+
+            @Override
+            public int getPixelStride() {
+                return pixelStride;
+            }
+
+            @Override
+            public ByteBuffer getBuffer() {
+                return mBuffer;
+            }
+        };
+    }
+
+    enum DownsamplingMethod {
+        // Uses nearest sample.
+        NEAREST_NEIGHBOR,
+        // Uses average of 4 nearest samples.
+        AVERAGING,
+    }
+
+    private static final class ForwardingImageProxyImpl extends ForwardingImageProxy {
+        private final PlaneProxy[] mDownsampledPlanes;
+        private final int mDownsampledWidth;
+        private final int mDownsampledHeight;
+
+        ForwardingImageProxyImpl(
+                ImageProxy originalImage,
+                PlaneProxy[] downsampledPlanes,
+                int downsampledWidth,
+                int downsampledHeight) {
+            super(originalImage);
+            mDownsampledPlanes = downsampledPlanes;
+            mDownsampledWidth = downsampledWidth;
+            mDownsampledHeight = downsampledHeight;
+        }
+
+        @Override
+        public synchronized int getWidth() {
+            return mDownsampledWidth;
+        }
+
+        @Override
+        public synchronized int getHeight() {
+            return mDownsampledHeight;
+        }
+
+        @Override
+        public synchronized PlaneProxy[] getPlanes() {
+            return mDownsampledPlanes;
+        }
+    }
+}
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..0484303
--- /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;
+    static final List<QueuedImageReaderProxy> sSharedImageReaderProxys = new ArrayList<>();
+    private static Set<DeviceProperties> sSharedReaderWhitelist;
+    private static ImageReader sSharedImageReader;
+
+    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
+     */
+    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
+     */
+    public static ImageReaderProxy createSharedReader(
+            String cameraId, int width, int height, int format, int maxImages, Handler handler) {
+        if (sSharedImageReader == null) {
+            Size resolution =
+                    CameraX.getSurfaceManager().getMaxOutputSize(cameraId, SHARED_IMAGE_FORMAT);
+            Log.d(TAG, "Resolution of base ImageReader: " + resolution);
+            sSharedImageReader =
+                    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, sSharedImageReader.getSurface());
+        sSharedImageReaderProxys.add(imageReaderProxy);
+        sSharedImageReader.setOnImageAvailableListener(
+                new ForwardingImageReaderListener(sSharedImageReaderProxys), handler);
+        imageReaderProxy.addOnReaderCloseListener(
+                new QueuedImageReaderProxy.OnReaderCloseListener() {
+                    @Override
+                    public void onReaderClose(ImageReaderProxy reader) {
+                        sSharedImageReaderProxys.remove(reader);
+                        if (sSharedImageReaderProxys.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 (sSharedReaderWhitelist == null) {
+            sSharedReaderWhitelist = new HashSet<>();
+            for (int sdkVersion = 21; sdkVersion <= 27; ++sdkVersion) {
+                // TODO(b/128944206)
+                // The image reader sharing was for 4-use-case scenario (image capture, image
+                // analysis, video capture, viewfinder). Since 4-use-case scenario is currently
+                // deprioritized and video capture is deprioritized. Just make this empty list.
+            }
+        }
+        return sSharedReaderWhitelist.contains(device);
+    }
+
+    static void clearSharedReaders() {
+        sSharedImageReaderProxys.clear();
+        sSharedImageReader.setOnImageAvailableListener(null, null);
+        sSharedImageReader.close();
+        sSharedImageReader = 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..80db4a4
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageSaver.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.location.Location;
+import android.os.Handler;
+
+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";
+    @Nullable
+    private final Location mLocation;
+    // The image that was captured
+    private final ImageProxy mImage;
+    // The orientation of the image
+    private final int mOrientation;
+    // If true, the picture taken is reversed horizontally and needs to be flipped.
+    // Typical with front facing cameras.
+    private final boolean mIsReversedHorizontal;
+    // If true, the picture taken is reversed vertically and needs to be flipped.
+    private final boolean mIsReversedVertical;
+    // The file to save the image to
+    final File mFile;
+    // The callback to call on completion
+    final OnImageSavedListener mListener;
+    // The handler to call back on
+    private final Handler mHandler;
+
+    ImageSaver(
+            ImageProxy image,
+            File file,
+            int orientation,
+            boolean reversedHorizontal,
+            boolean reversedVertical,
+            @Nullable Location location,
+            OnImageSavedListener listener,
+            Handler handler) {
+        mImage = image;
+        mFile = file;
+        mOrientation = orientation;
+        mIsReversedHorizontal = reversedHorizontal;
+        mIsReversedVertical = reversedVertical;
+        mListener = listener;
+        mHandler = handler;
+        mLocation = location;
+    }
+
+    @Override
+    public void run() {
+        // Finally, we save the file to disk
+        SaveError saveError = null;
+        String errorMessage = null;
+        Exception exception = null;
+        try (ImageProxy imageToClose = mImage;
+             FileOutputStream output = new FileOutputStream(mFile)) {
+            byte[] bytes = ImageUtil.imageToJpegByteArray(mImage);
+            output.write(bytes);
+
+            Exif exif = Exif.createFromFile(mFile);
+            exif.attachTimestamp();
+            exif.rotate(mOrientation);
+            if (mIsReversedHorizontal) {
+                exif.flipHorizontally();
+            }
+            if (mIsReversedVertical) {
+                exif.flipVertically();
+            }
+            if (mLocation != null) {
+                exif.attachLocation(mLocation);
+            }
+            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 mImage";
+            exception = e;
+        }
+
+        if (saveError != null) {
+            postError(saveError, errorMessage, exception);
+        } else {
+            postSuccess();
+        }
+    }
+
+    private void postSuccess() {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mListener.onImageSaved(mFile);
+            }
+        });
+    }
+
+    private void postError(final SaveError saveError, final String message,
+            @Nullable final Throwable cause) {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mListener.onError(saveError, message, cause);
+            }
+        });
+    }
+
+    /** 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..64c3d12
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ImageUtil.java
@@ -0,0 +1,309 @@
+/*
+ * 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 androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.ImageOutputConfiguration.RotationValue;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility class for image related operations.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+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);
+        } else {
+            Log.w(TAG, "Unrecognized image format: " + image.getFormat());
+        }
+        return data;
+    }
+
+    /** 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);
+    }
+
+    /**
+     * Rotate rational by rotation value, which inverse it if the degree is 90 or 270.
+     *
+     * @param rational Rational to be rotated.
+     * @param rotation Rotation value being applied.
+     * */
+    public static Rational rotate(
+            Rational rational, @RotationValue int rotation) {
+        if (rotation == 90 || rotation == 270) {
+            return inverseRational(rational);
+        }
+
+        return rational;
+    }
+
+    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);
+    }
+
+    private static Rational inverseRational(Rational rational) {
+        if (rational == null) {
+            return rational;
+        }
+        return new Rational(
+                /*numerator=*/ rational.getDenominator(),
+                /*denominator=*/ rational.getNumerator());
+    }
+
+    private static boolean shouldCropImage(ImageProxy image) {
+        Size sourceSize = new Size(image.getWidth(), image.getHeight());
+        Size targetSize = new Size(image.getCropRect().width(), image.getCropRect().height());
+
+        return !targetSize.equals(sourceSize);
+    }
+
+    private static byte[] jpegImageToJpegByteArray(ImageProxy image) throws EncodeFailedException {
+        ImageProxy.PlaneProxy[] planes = image.getPlanes();
+        ByteBuffer buffer = planes[0].getBuffer();
+        byte[] data = new byte[buffer.capacity()];
+        buffer.get(data);
+        if (shouldCropImage(image)) {
+            data = cropByteArray(data, image.getCropRect());
+        }
+        return data;
+    }
+
+    private static byte[] yuvImageToJpegByteArray(ImageProxy image)
+            throws EncodeFailedException {
+        return ImageUtil.nv21ToJpeg(
+                ImageUtil.yuv_420_888toNv21(image),
+                image.getWidth(),
+                image.getHeight(),
+                shouldCropImage(image) ? image.getCropRect() : null);
+    }
+
+    /** 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..57d2c2a
--- /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 extends DeferrableSurface {
+    private final Surface mSurface;
+
+    public ImmediateSurface(Surface surface) {
+        mSurface = surface;
+    }
+
+    @Override
+    public ListenableFuture<Surface> getSurface() {
+        return Futures.immediateFuture(mSurface);
+    }
+}
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..bd775b8
--- /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 sExecutor;
+
+    private final ExecutorService mIoService =
+            Executors.newFixedThreadPool(
+                    2,
+                    new ThreadFactory() {
+                        private static final String THREAD_NAME_STEM =
+                                CameraXThreads.TAG + "camerax_io_%d";
+
+                        private final AtomicInteger mThreadId = new AtomicInteger(0);
+
+                        @Override
+                        public Thread newThread(Runnable r) {
+                            Thread t = new Thread(r);
+                            t.setName(
+                                    String.format(
+                                            Locale.US,
+                                            THREAD_NAME_STEM,
+                                            mThreadId.getAndIncrement()));
+                            return t;
+                        }
+                    });
+
+    static Executor getInstance() {
+        if (sExecutor != null) {
+            return sExecutor;
+        }
+        synchronized (IoExecutor.class) {
+            if (sExecutor == null) {
+                sExecutor = new IoExecutor();
+            }
+        }
+
+        return sExecutor;
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        mIoService.execute(command);
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/MainThreadExecutor.java b/camera/core/src/main/java/androidx/camera/core/MainThreadExecutor.java
new file mode 100644
index 0000000..6c93d62d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MainThreadExecutor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+final class MainThreadExecutor implements Executor {
+    private static volatile Executor sExecutor;
+    private final Handler mMainThreadHandler;
+
+    private MainThreadExecutor() {
+        mMainThreadHandler = new Handler(Looper.getMainLooper());
+    }
+
+    static Executor getInstance() {
+        if (sExecutor != null) {
+            return sExecutor;
+        }
+        synchronized (MainThreadExecutor.class) {
+            if (sExecutor == null) {
+                sExecutor = new MainThreadExecutor();
+            }
+        }
+
+        return sExecutor;
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        if (!mMainThreadHandler.post(command)) {
+            throw new RejectedExecutionException(mMainThreadHandler + " is shutting down");
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/MetadataImageReader.java b/camera/core/src/main/java/androidx/camera/core/MetadataImageReader.java
new file mode 100644
index 0000000..c5fa343
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MetadataImageReader.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.ImageReader;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An {@link ImageReaderProxy} which matches the incoming {@link android.media.Image} with its
+ * {@link ImageInfo}.
+ *
+ * <p>MetadataImageReader holds an ImageReaderProxy and listens to
+ * {@link CameraCaptureCallback}. Then compose them into an {@link ImageProxy} with same
+ * timestamp and output it to
+ * {@link androidx.camera.core.ImageReaderProxy.OnImageAvailableListener}. User who acquires the
+ * ImageProxy is responsible for closing it after use. A limited number of ImageProxy may be
+ * acquired at one time as defined by <code>maxImages</code> in the constructor. Any ImageProxy
+ * produced after that will be dropped unless one of the ImageProxy currently acquired is closed.
+ */
+class MetadataImageReader implements ImageReaderProxy, ForwardingImageProxy.OnImageCloseListener {
+    private static final String TAG = "MetadataImageReader";
+    private final Object mLock = new Object();
+
+    // Callback when camera capture is completed.
+    private CameraCaptureCallback mCameraCaptureCallback = new CameraCaptureCallback() {
+        @Override
+        public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
+            super.onCaptureCompleted(cameraCaptureResult);
+            resultIncoming(cameraCaptureResult);
+        }
+    };
+
+    // Callback when Image is ready from the underlying ImageReader.
+    private ImageReaderProxy.OnImageAvailableListener mTransformedListener =
+            new ImageReaderProxy.OnImageAvailableListener() {
+                @Override
+                public void onImageAvailable(ImageReaderProxy reader) {
+                    imageIncoming(reader);
+                }
+            };
+
+    @GuardedBy("mLock")
+    private boolean mClosed = false;
+
+    @GuardedBy("mLock")
+    private final ImageReaderProxy mImageReaderProxy;
+
+    @GuardedBy("mLock")
+    @Nullable
+    ImageReaderProxy.OnImageAvailableListener mListener;
+
+    @GuardedBy("mLock")
+    @Nullable
+    private Handler mHandler;
+
+    /** ImageInfos haven't been matched with Image. */
+    @GuardedBy("mLock")
+    private final Map<Long, ImageInfo> mPendingImageInfos = new HashMap<>();
+
+    /** Images haven't been matched with ImageInfo. */
+    @GuardedBy("mLock")
+    private final Map<Long, ImageProxy> mPendingImages = new HashMap<>();
+
+    @GuardedBy("mLock")
+    private int mImageProxiesIndex;
+
+    /** ImageProxies with matched Image and ImageInfo and are ready to be acquired. */
+    @GuardedBy("mLock")
+    private List<ImageProxy> mMatchedImageProxies;
+
+    /** ImageProxies which are already acquired. */
+    @GuardedBy("mLock")
+    private final List<ImageProxy> mAcquiredImageProxies = new ArrayList<>();
+
+    /**
+     * Create a {@link MetadataImageReader} with specific configurations.
+     *
+     * @param width     Width of the ImageReader
+     * @param height    Height of the ImageReader
+     * @param format    Image format
+     * @param maxImages Maximum Image number the ImageReader can hold
+     * @param handler   Handler for executing {@link ImageReaderProxy.OnImageAvailableListener}
+     */
+    MetadataImageReader(int width, int height, int format, int maxImages,
+            @Nullable Handler handler) {
+        mImageReaderProxy = new AndroidImageReaderProxy(
+                ImageReader.newInstance(width, height, format, maxImages));
+
+        init(handler);
+    }
+
+    /**
+     * Create a {@link MetadataImageReader} with a already created {@link ImageReaderProxy}.
+     *
+     * @param imageReaderProxy The existed ImageReaderProxy to be set underlying this
+     *                         MetadataImageReader.
+     * @param handler          Handler for executing
+     * {@link ImageReaderProxy.OnImageAvailableListener}
+     */
+    MetadataImageReader(ImageReaderProxy imageReaderProxy, @Nullable Handler handler) {
+        mImageReaderProxy = imageReaderProxy;
+
+        init(handler);
+    }
+
+    private void init(Handler handler) {
+        mHandler = handler;
+        mImageReaderProxy.setOnImageAvailableListener(mTransformedListener, handler);
+        mImageProxiesIndex = 0;
+        mMatchedImageProxies = new ArrayList<>(getMaxImages());
+    }
+
+    @Override
+    @Nullable
+    public ImageProxy acquireLatestImage() {
+        synchronized (mLock) {
+            if (mMatchedImageProxies.isEmpty()) {
+                return null;
+            }
+            if (mImageProxiesIndex >= mMatchedImageProxies.size()) {
+                throw new IllegalStateException("Maximum image number reached.");
+            }
+
+            // Release those older ImageProxies which haven't been acquired.
+            List<ImageProxy> toClose = new ArrayList<>();
+            for (int i = 0; i < mMatchedImageProxies.size() - 1; i++) {
+                if (!mAcquiredImageProxies.contains(mMatchedImageProxies.get(i))) {
+                    toClose.add(mMatchedImageProxies.get(i));
+                }
+            }
+            for (ImageProxy image : toClose) {
+                image.close();
+            }
+
+            // Pop the latest ImageProxy and set the index to the end of list.
+            mImageProxiesIndex = mMatchedImageProxies.size() - 1;
+            ImageProxy acquiredImage = mMatchedImageProxies.get(mImageProxiesIndex++);
+            mAcquiredImageProxies.add(acquiredImage);
+
+            return acquiredImage;
+        }
+    }
+
+    @Override
+    @Nullable
+    public ImageProxy acquireNextImage() {
+        synchronized (mLock) {
+            if (mMatchedImageProxies.isEmpty()) {
+                return null;
+            }
+
+            if (mImageProxiesIndex >= mMatchedImageProxies.size()) {
+                throw new IllegalStateException("Maximum image number reached.");
+            }
+
+            // Pop the next matched ImageProxy.
+            ImageProxy acquiredImage = mMatchedImageProxies.get(mImageProxiesIndex++);
+            mAcquiredImageProxies.add(acquiredImage);
+
+            return acquiredImage;
+        }
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+
+            List<ImageProxy> imagesToClose = new ArrayList<>(mMatchedImageProxies);
+            for (ImageProxy image : imagesToClose) {
+                image.close();
+            }
+            mMatchedImageProxies.clear();
+
+            mImageReaderProxy.close();
+            mClosed = true;
+        }
+    }
+
+    @Override
+    public int getHeight() {
+        synchronized (mLock) {
+            return mImageReaderProxy.getHeight();
+        }
+    }
+
+    @Override
+    public int getWidth() {
+        synchronized (mLock) {
+            return mImageReaderProxy.getWidth();
+        }
+    }
+
+    @Override
+    public int getImageFormat() {
+        synchronized (mLock) {
+            return mImageReaderProxy.getImageFormat();
+        }
+    }
+
+    @Override
+    public int getMaxImages() {
+        synchronized (mLock) {
+            return mImageReaderProxy.getMaxImages();
+        }
+    }
+
+    @Override
+    public Surface getSurface() {
+        synchronized (mLock) {
+            return mImageReaderProxy.getSurface();
+        }
+    }
+
+    @Override
+    public void setOnImageAvailableListener(
+            @Nullable final ImageReaderProxy.OnImageAvailableListener listener,
+            @Nullable Handler handler) {
+        synchronized (mLock) {
+            mListener = listener;
+            mHandler = handler;
+            mImageReaderProxy.setOnImageAvailableListener(mTransformedListener, handler);
+        }
+    }
+
+    @Override
+    public void onImageClose(ImageProxy image) {
+        synchronized (mLock) {
+            dequeImageProxy(image);
+        }
+    }
+
+    private void enqueueImageProxy(SettableImageProxy image) {
+        synchronized (mLock) {
+            if (mMatchedImageProxies.size() < getMaxImages()) {
+                image.addOnImageCloseListener(this);
+                mMatchedImageProxies.add(image);
+                if (mListener != null) {
+                    if (mHandler != null) {
+                        mHandler.post(
+                                new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        mListener.onImageAvailable(MetadataImageReader.this);
+                                    }
+                                });
+                    } else {
+                        mListener.onImageAvailable(MetadataImageReader.this);
+                    }
+                }
+            } else {
+                Log.d("TAG", "Maximum image number reached.");
+                image.close();
+            }
+        }
+    }
+
+    private void dequeImageProxy(ImageProxy image) {
+        synchronized (mLock) {
+            int index = mMatchedImageProxies.indexOf(image);
+            if (index >= 0) {
+                mMatchedImageProxies.remove(index);
+                if (index <= mImageProxiesIndex) {
+                    mImageProxiesIndex--;
+                }
+            }
+            mAcquiredImageProxies.remove(image);
+        }
+    }
+
+    @Nullable
+    Handler getHandler() {
+        return mHandler;
+    }
+
+    // Return the necessary CameraCaptureCallback, which needs to register to capture session.
+    CameraCaptureCallback getCameraCaptureCallback() {
+        return mCameraCaptureCallback;
+    }
+
+    // Incoming Image from underlying ImageReader. Matches it with pending ImageInfo.
+    void imageIncoming(ImageReaderProxy imageReader) {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+
+            ImageProxy image = null;
+            try {
+                image = imageReader.acquireNextImage();
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Failed to acquire latest image.", e);
+            } finally {
+                if (image != null) {
+                    // Add the incoming Image to pending list and do the matching logic.
+                    mPendingImages.put(image.getTimestamp(), image);
+
+                    matchImages();
+                }
+            }
+        }
+    }
+
+    // Incoming result from camera callback. Creates corresponding ImageInfo and matches it with
+    // pending Image.
+    void resultIncoming(CameraCaptureResult cameraCaptureResult) {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+
+            // Add the incoming CameraCaptureResult to pending list and do the matching logic.
+            mPendingImageInfos.put(cameraCaptureResult.getTimestamp(),
+                    new CameraCaptureResultImageInfo(cameraCaptureResult));
+
+            matchImages();
+        }
+    }
+
+    // Match incoming Image from the ImageReader with the corresponding ImageInfo.
+    private void matchImages() {
+        synchronized (mLock) {
+            List<Long> toRemove = new ArrayList<>();
+            for (Map.Entry<Long, ImageInfo> entry : mPendingImageInfos.entrySet()) {
+                ImageInfo imageInfo = entry.getValue();
+                long timestamp = imageInfo.getTimestamp();
+
+                if (mPendingImages.containsKey(timestamp)) {
+                    ImageProxy image = mPendingImages.get(timestamp);
+                    mPendingImages.remove(timestamp);
+                    Long key = entry.getKey();
+                    toRemove.add(key);
+                    // Got a match. Add the ImageProxy to matched list and invoke
+                    // onImageAvailableListener.
+                    enqueueImageProxy(new SettableImageProxy(image, imageInfo));
+                }
+            }
+
+            for (Long key : toRemove) {
+                mPendingImageInfos.remove(key);
+            }
+        }
+    }
+}
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..6d2709d
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/MutableOptionsBundle.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.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 =
+            new Comparator<Option<?>>() {
+                @Override
+                public int compare(Option<?> o1, Option<?> o2) {
+                    return 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) mOptions.remove(opt);
+
+        return value;
+    }
+
+    @Override
+    public <ValueT> void insertOption(Option<ValueT> opt, ValueT value) {
+        mOptions.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..b068b29
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/OnFocusCompletedListener.java
@@ -0,0 +1,31 @@
+/*
+ * 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. */
+    void onFocusLocked(Rect afRect);
+
+    /** Callback when unable to acquire focus. */
+    void onFocusUnableToLock(Rect afRect);
+
+    /** Callback when timeout is reached and af state haven't settled. */
+    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..87975b5
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/OptionsBundle.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 androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Collections;
+import java.util.Comparator;
+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<>(new Comparator<Option<?>>() {
+                @Override
+                public int compare(Option<?> o1, Option<?> o2) {
+                    return o1.getId().compareTo(o2.getId());
+                }
+            }));
+    // TODO: Make these options parcelable
+    protected final TreeMap<Option<?>, Object> mOptions;
+
+    OptionsBundle(TreeMap<Option<?>, Object> options) {
+        mOptions = 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<>(new Comparator<Option<?>>() {
+                    @Override
+                    public int compare(Option<?> o1, Option<?> o2) {
+                        return 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(mOptions.keySet());
+    }
+
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return mOptions.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) mOptions.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 : mOptions.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/ProcessingImageReader.java b/camera/core/src/main/java/androidx/camera/core/ProcessingImageReader.java
new file mode 100644
index 0000000..c511ad0
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ProcessingImageReader.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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.MoreExecutors;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An {@link ImageReaderProxy} which takes one or more {@link android.media.Image}, processes it,
+ * then output the final result {@link ImageProxy} to
+ * {@link androidx.camera.core.ImageReaderProxy.OnImageAvailableListener}.
+ *
+ * <p>ProcessingImageReader takes {@link CaptureBundle} as the expected set of
+ * {@link CaptureStage}. Once all the ImageProxy from the captures are ready. It invokes
+ * the {@link CaptureProcessor} set, then returns a single output ImageProxy to
+ * OnImageAvailableListener.
+ */
+class ProcessingImageReader implements ImageReaderProxy {
+    private static final String TAG = "ProcessingImageReader";
+    private final Object mLock = new Object();
+
+    // Callback when Image is ready from InputImageReader.
+    private ImageReaderProxy.OnImageAvailableListener mTransformedListener =
+            new ImageReaderProxy.OnImageAvailableListener() {
+                @Override
+                public void onImageAvailable(ImageReaderProxy reader) {
+                    imageIncoming(reader);
+                }
+            };
+
+    // Callback when Image is ready from OutputImageReader.
+    private ImageReader.OnImageAvailableListener mImageProcessedListener =
+            new ImageReader.OnImageAvailableListener() {
+                @Override
+                public void onImageAvailable(ImageReader reader) {
+                    // Callback the output OnImageAvailableListener.
+                    if (mHandler != null) {
+                        mHandler.post(
+                                new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        mListener.onImageAvailable(ProcessingImageReader.this);
+                                    }
+                                });
+                    } else {
+                        mListener.onImageAvailable(ProcessingImageReader.this);
+                    }
+                    // Resets SettableImageProxyBundle after the processor finishes processing.
+                    mSettableImageProxyBundle.reset();
+                    setupSettableImageProxyBundleCallbacks();
+                }
+            };
+
+    // Callback when all the ImageProxies in SettableImageProxyBundle are ready.
+    private FutureCallback<List<ImageProxy>> mCaptureStageReadyCallback =
+            new FutureCallback<List<ImageProxy>>() {
+                @Override
+                public void onSuccess(@Nullable List<ImageProxy> imageProxyList) {
+                    mCaptureProcessor.process(mSettableImageProxyBundle);
+                }
+
+                @Override
+                public void onFailure(Throwable throwable) {
+
+                }
+            };
+
+    @GuardedBy("mLock")
+    private boolean mClosed = false;
+
+    @GuardedBy("mLock")
+    private final ImageReaderProxy mInputImageReader;
+
+    @GuardedBy("mLock")
+    private final ImageReader mOutputImageReader;
+
+    @GuardedBy("mLock")
+    @Nullable
+    ImageReaderProxy.OnImageAvailableListener mListener;
+
+    @GuardedBy("mLock")
+    @Nullable
+    Handler mHandler;
+
+    @NonNull
+    CaptureProcessor mCaptureProcessor;
+
+    @GuardedBy("mLock")
+    SettableImageProxyBundle mSettableImageProxyBundle = null;
+
+    private final List<Integer> mCaptureIdList = new ArrayList<>();
+
+    /**
+     * Create a {@link ProcessingImageReader} with specific configurations.
+     *
+     * @param width            Width of the ImageReader
+     * @param height           Height of the ImageReader
+     * @param format           Image format
+     * @param maxImages        Maximum Image number the ImageReader can hold. The capacity should
+     *                        be greater than the captureBundle size in order to hold all the
+     *                         Images needed with this processing.
+     * @param handler          Handler for executing
+     * {@link ImageReaderProxy.OnImageAvailableListener}
+     * @param captureBundle    The {@link CaptureBundle} includes the processing information
+     * @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are ready
+     */
+    ProcessingImageReader(int width, int height, int format, int maxImages,
+            @Nullable Handler handler,
+            @NonNull CaptureBundle captureBundle, @NonNull CaptureProcessor captureProcessor) {
+        int captureBundleSize = captureBundle.getCaptureStages().size();
+        mInputImageReader = new MetadataImageReader(
+                width,
+                height,
+                format,
+                maxImages >= captureBundleSize ? maxImages : captureBundleSize,
+                handler);
+        mOutputImageReader = ImageReader.newInstance(width, height, format, maxImages);
+
+        init(handler, captureBundle, captureProcessor);
+    }
+
+    ProcessingImageReader(ImageReaderProxy imageReader, @Nullable Handler handler,
+            @NonNull CaptureBundle captureBundle,
+            @NonNull CaptureProcessor captureProcessor) {
+        if (imageReader.getMaxImages() < captureBundle.getCaptureStages().size()) {
+            throw new IllegalArgumentException(
+                    "MetadataImageReader is smaller than CaptureBundle.");
+        }
+        mInputImageReader = imageReader;
+        mOutputImageReader = ImageReader.newInstance(imageReader.getWidth(),
+                imageReader.getHeight(), imageReader.getImageFormat(), imageReader.getMaxImages());
+
+        init(handler, captureBundle, captureProcessor);
+    }
+
+    private void init(@Nullable Handler handler, @NonNull CaptureBundle captureBundle,
+            @NonNull CaptureProcessor captureProcessor) {
+        mHandler = handler;
+        mInputImageReader.setOnImageAvailableListener(mTransformedListener, handler);
+        mOutputImageReader.setOnImageAvailableListener(mImageProcessedListener, handler);
+        mCaptureProcessor = captureProcessor;
+        mCaptureProcessor.onOutputSurface(mOutputImageReader.getSurface(), getImageFormat());
+
+        setupSettableImageProxyBundle(captureBundle);
+    }
+
+    @Override
+    @Nullable
+    public ImageProxy acquireLatestImage() {
+        synchronized (mLock) {
+            Image image = mOutputImageReader.acquireLatestImage();
+            if (image == null) {
+                return null;
+            }
+
+            return new AndroidImageProxy(image);
+        }
+    }
+
+    @Override
+    @Nullable
+    public ImageProxy acquireNextImage() {
+        synchronized (mLock) {
+            Image image = mOutputImageReader.acquireNextImage();
+            if (image == null) {
+                return null;
+            }
+
+            return new AndroidImageProxy(image);
+        }
+    }
+
+    @Override
+    public void close() {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+
+            mInputImageReader.close();
+            mOutputImageReader.close();
+            mSettableImageProxyBundle.close();
+            mClosed = true;
+        }
+    }
+
+    @Override
+    public int getHeight() {
+        synchronized (mLock) {
+            return mInputImageReader.getHeight();
+        }
+    }
+
+    @Override
+    public int getWidth() {
+        synchronized (mLock) {
+            return mInputImageReader.getWidth();
+        }
+    }
+
+    @Override
+    public int getImageFormat() {
+        synchronized (mLock) {
+            return mInputImageReader.getImageFormat();
+        }
+    }
+
+    @Override
+    public int getMaxImages() {
+        synchronized (mLock) {
+            return mInputImageReader.getMaxImages();
+        }
+    }
+
+    @Override
+    public Surface getSurface() {
+        synchronized (mLock) {
+            return mInputImageReader.getSurface();
+        }
+    }
+
+    @Override
+    public void setOnImageAvailableListener(
+            @Nullable final ImageReaderProxy.OnImageAvailableListener listener,
+            @Nullable Handler handler) {
+        synchronized (mLock) {
+            mListener = listener;
+            mHandler = handler;
+            mInputImageReader.setOnImageAvailableListener(mTransformedListener, handler);
+            mOutputImageReader.setOnImageAvailableListener(mImageProcessedListener, handler);
+        }
+    }
+
+    /** Returns necessary camera callbacks to retrieve metadata from camera result. */
+    @Nullable
+    CameraCaptureCallback getCameraCaptureCallback() {
+        if (mInputImageReader instanceof MetadataImageReader) {
+            return ((MetadataImageReader) mInputImageReader).getCameraCaptureCallback();
+        } else {
+            return null;
+        }
+    }
+
+    private void setupSettableImageProxyBundle(CaptureBundle captureBundle) {
+        if (captureBundle != null) {
+            for (CaptureStage captureStage : captureBundle.getCaptureStages()) {
+                if (captureStage != null) {
+                    mCaptureIdList.add(captureStage.getId());
+                }
+            }
+        }
+
+        mSettableImageProxyBundle = new SettableImageProxyBundle(mCaptureIdList);
+        setupSettableImageProxyBundleCallbacks();
+    }
+
+    void setupSettableImageProxyBundleCallbacks() {
+        List<ListenableFuture<ImageProxy>> futureList = new ArrayList<>();
+        for (Integer id : mCaptureIdList) {
+            futureList.add(mSettableImageProxyBundle.getImageProxy((id)));
+        }
+        Futures.addCallback(Futures.successfulAsList(futureList), mCaptureStageReadyCallback,
+                MoreExecutors.directExecutor());
+    }
+
+    // Incoming Image from InputImageReader. Acquires it and add to SettableImageProxyBundle.
+    void imageIncoming(ImageReaderProxy imageReader) {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+
+            ImageProxy image = null;
+            try {
+                image = imageReader.acquireNextImage();
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Failed to acquire latest image.", e);
+            } finally {
+                if (image != null) {
+                    Integer tag = (Integer) image.getImageInfo().getTag();
+                    if (!mCaptureIdList.contains(tag)) {
+                        Log.w(TAG, "ImageProxyBundle does not contain this id: " + tag);
+                        image.close();
+                        return;
+                    }
+
+                    mSettableImageProxyBundle.addImageProxy(image);
+                }
+            }
+        }
+    }
+}
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..ac42f16
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/QueuedImageReaderProxy.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 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 mWidth;
+    private final int mHeight;
+    private final int mFormat;
+    private final int mMaxImages;
+
+    @GuardedBy("this")
+    private final Surface mSurface;
+
+    // mMaxImages 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> mImages;
+
+    @GuardedBy("this")
+    private final Set<ImageProxy> mAcquiredImages = new HashSet<>();
+    @GuardedBy("this")
+    private final Set<OnReaderCloseListener> mOnReaderCloseListeners = new HashSet<>();
+    // Current access position in the queue.
+    @GuardedBy("this")
+    private int mCurrentPosition;
+    @GuardedBy("this")
+    @Nullable
+    private ImageReaderProxy.OnImageAvailableListener mOnImageAvailableListener;
+    @GuardedBy("this")
+    @Nullable
+    private Handler mOnImageAvailableHandler;
+    @GuardedBy("this")
+    private boolean mClosed;
+
+    /**
+     * 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) {
+        mWidth = width;
+        mHeight = height;
+        mFormat = format;
+        mMaxImages = maxImages;
+        mSurface = surface;
+        mImages = new ArrayList<>(maxImages);
+        mCurrentPosition = 0;
+        mClosed = false;
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireLatestImage() {
+        throwExceptionIfClosed();
+        if (mImages.isEmpty()) {
+            return null;
+        }
+        if (mCurrentPosition >= mImages.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 < mImages.size() - 1; ++i) {
+            if (!mAcquiredImages.contains(mImages.get(i))) {
+                imagesToClose.add(mImages.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.
+        mCurrentPosition = mImages.size() - 1;
+        ImageProxy acquiredImage = mImages.get(mCurrentPosition++);
+        mAcquiredImages.add(acquiredImage);
+        return acquiredImage;
+    }
+
+    @Override
+    @Nullable
+    public synchronized ImageProxy acquireNextImage() {
+        throwExceptionIfClosed();
+        if (mImages.isEmpty()) {
+            return null;
+        }
+        if (mCurrentPosition >= mImages.size()) {
+            throw new IllegalStateException("Max images have already been acquired without close.");
+        }
+        ImageProxy acquiredImage = mImages.get(mCurrentPosition++);
+        mAcquiredImages.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 (mImages.size() < mMaxImages) {
+            mImages.add(image);
+            image.addOnImageCloseListener(this);
+            if (mOnImageAvailableListener != null && mOnImageAvailableHandler != null) {
+                final OnImageAvailableListener listener = mOnImageAvailableListener;
+                mOnImageAvailableHandler.post(
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                if (!QueuedImageReaderProxy.this.isClosed()) {
+                                    listener.onImageAvailable(QueuedImageReaderProxy.this);
+                                }
+                            }
+                        });
+            }
+        } else {
+            image.close();
+        }
+    }
+
+    @Override
+    public synchronized void close() {
+        if (!mClosed) {
+            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<>(mImages);
+            for (ImageProxy image : imagesToClose) {
+                image.close();
+            }
+            mImages.clear();
+            mClosed = true;
+            notifyOnReaderCloseListeners();
+        }
+    }
+
+    @Override
+    public int getHeight() {
+        throwExceptionIfClosed();
+        return mHeight;
+    }
+
+    @Override
+    public int getWidth() {
+        throwExceptionIfClosed();
+        return mWidth;
+    }
+
+    @Override
+    public int getImageFormat() {
+        throwExceptionIfClosed();
+        return mFormat;
+    }
+
+    @Override
+    public int getMaxImages() {
+        throwExceptionIfClosed();
+        return mMaxImages;
+    }
+
+    @Override
+    public synchronized Surface getSurface() {
+        throwExceptionIfClosed();
+        return mSurface;
+    }
+
+    @Override
+    public synchronized void setOnImageAvailableListener(
+            @Nullable OnImageAvailableListener onImageAvailableListener,
+            @Nullable Handler onImageAvailableHandler) {
+        throwExceptionIfClosed();
+        mOnImageAvailableListener = onImageAvailableListener;
+        mOnImageAvailableHandler = onImageAvailableHandler;
+    }
+
+    @Override
+    public synchronized void onImageClose(ImageProxy image) {
+        int index = mImages.indexOf(image);
+        if (index >= 0) {
+            mImages.remove(index);
+            if (index <= mCurrentPosition) {
+                mCurrentPosition--;
+            }
+        }
+        mAcquiredImages.remove(image);
+    }
+
+    /** Returns the current number of images in the queue. */
+    synchronized int getCurrentImages() {
+        throwExceptionIfClosed();
+        return mImages.size();
+    }
+
+    /** Returns true if the reader is already closed. */
+    synchronized boolean isClosed() {
+        return mClosed;
+    }
+
+    /**
+     * Adds a listener for close calls on this reader.
+     *
+     * @param listener to add
+     */
+    synchronized void addOnReaderCloseListener(OnReaderCloseListener listener) {
+        mOnReaderCloseListeners.add(listener);
+    }
+
+    private synchronized void throwExceptionIfClosed() {
+        if (mClosed) {
+            throw new IllegalStateException("This reader is already closed.");
+        }
+    }
+
+    private synchronized void notifyOnReaderCloseListeners() {
+        for (OnReaderCloseListener listener : mOnReaderCloseListeners) {
+            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..9388942
--- /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 mReferenceCount = 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 (mReferenceCount <= 0) {
+            return null;
+        } else {
+            mReferenceCount++;
+            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 (mReferenceCount > 0) {
+            mReferenceCount--;
+            if (mReferenceCount <= 0) {
+                super.close();
+            }
+        }
+    }
+
+    /** Returns the current reference count. */
+    synchronized int getReferenceCount() {
+        return mReferenceCount;
+    }
+}
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..7e758c3
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SessionConfiguration.java
@@ -0,0 +1,380 @@
+/*
+ * 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
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class SessionConfiguration {
+
+    /** The set of {@link Surface} that data from the camera will be put into. */
+    private final List<DeferrableSurface> mSurfaces;
+    /** The state callback for a {@link CameraDevice}. */
+    private final CameraDevice.StateCallback mDeviceStateCallback;
+    /** The state callback for a {@link CameraCaptureSession}. */
+    private final CameraCaptureSession.StateCallback mSessionStateCallback;
+    /** The configuration for building the {@link CaptureRequest}. */
+    private final CaptureRequestConfiguration mCaptureRequestConfiguration;
+
+    /**
+     * 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) {
+        mSurfaces = surfaces;
+        mDeviceStateCallback = deviceStateCallback;
+        mSessionStateCallback = sessionStateCallback;
+        mCaptureRequestConfiguration = captureRequestConfiguration;
+    }
+
+    /** Returns an instance of a session configuration with minimal configurations. */
+    public static SessionConfiguration defaultEmptySessionConfiguration() {
+        return new SessionConfiguration(
+                new ArrayList<DeferrableSurface>(),
+                CameraDeviceStateCallbacks.createNoOpCallback(),
+                CameraCaptureSessionStateCallbacks.createNoOpCallback(),
+                new CaptureRequestConfiguration.Builder().build());
+    }
+
+    public List<DeferrableSurface> getSurfaces() {
+        return Collections.unmodifiableList(mSurfaces);
+    }
+
+    public Map<Key<?>, CaptureRequestParameter<?>> getCameraCharacteristics() {
+        return mCaptureRequestConfiguration.getCameraCharacteristics();
+    }
+
+    public Configuration getImplementationOptions() {
+        return mCaptureRequestConfiguration.getImplementationOptions();
+    }
+
+    public int getTemplateType() {
+        return mCaptureRequestConfiguration.getTemplateType();
+    }
+
+    public CameraDevice.StateCallback getDeviceStateCallback() {
+        return mDeviceStateCallback;
+    }
+
+    public CameraCaptureSession.StateCallback getSessionStateCallback() {
+        return mSessionStateCallback;
+    }
+
+    public CameraCaptureCallback getCameraCaptureCallback() {
+        return mCaptureRequestConfiguration.getCameraCaptureCallback();
+    }
+
+    public CaptureRequestConfiguration getCaptureRequestConfiguration() {
+        return mCaptureRequestConfiguration;
+    }
+
+    /**
+     * 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.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public interface OptionUnpacker {
+
+        /**
+         * Apply the options from the config onto the builder
+         * @param config the set of options to apply
+         * @param builder the builder on which to apply the options
+         */
+        void unpack(UseCaseConfiguration<?> config, SessionConfiguration.Builder builder);
+    }
+
+    /**
+     * Base builder for easy modification/rebuilding of a {@link SessionConfiguration}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    static class BaseBuilder {
+        protected final Set<DeferrableSurface> mSurfaces = new HashSet<>();
+        protected final CaptureRequestConfiguration.Builder mCaptureRequestConfigBuilder =
+                new CaptureRequestConfiguration.Builder();
+        protected CameraDevice.StateCallback mDeviceStateCallback =
+                CameraDeviceStateCallbacks.createNoOpCallback();
+        protected CameraCaptureSession.StateCallback mSessionStateCallback =
+                CameraCaptureSessionStateCallbacks.createNoOpCallback();
+    }
+
+    /**
+     * Builder for easy modification/rebuilding of a {@link SessionConfiguration}.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    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) {
+            mCaptureRequestConfigBuilder.setTemplateType(templateType);
+        }
+
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        public void setDeviceStateCallback(CameraDevice.StateCallback deviceStateCallback) {
+            mDeviceStateCallback = deviceStateCallback;
+        }
+
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        public void setSessionStateCallback(
+                CameraCaptureSession.StateCallback sessionStateCallback) {
+            mSessionStateCallback = sessionStateCallback;
+        }
+
+        /** Set the {@link CameraCaptureCallback}. */
+        public void setCameraCaptureCallback(CameraCaptureCallback cameraCaptureCallback) {
+            mCaptureRequestConfigBuilder.setCameraCaptureCallback(cameraCaptureCallback);
+        }
+
+        /** Add a surface to the set that the session repeatedly writes data to. */
+        public void addSurface(DeferrableSurface surface) {
+            mSurfaces.add(surface);
+            mCaptureRequestConfigBuilder.addSurface(surface);
+        }
+
+        /** Add a surface for the session which only used for single captures. */
+        public void addNonRepeatingSurface(DeferrableSurface surface) {
+            mSurfaces.add(surface);
+        }
+
+        /** Remove a surface from the set which the session repeatedly writes to. */
+        public void removeSurface(DeferrableSurface surface) {
+            mSurfaces.remove(surface);
+            mCaptureRequestConfigBuilder.removeSurface(surface);
+        }
+
+        /** Clears all surfaces from the set which the session writes to. */
+        public void clearSurfaces() {
+            mSurfaces.clear();
+            mCaptureRequestConfigBuilder.clearSurfaces();
+        }
+
+        /** Add the {@link CaptureRequest.Key} value pair that will be applied. */
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        public <T> void addCharacteristic(Key<T> key, T value) {
+            mCaptureRequestConfigBuilder.addCharacteristic(key, value);
+        }
+
+        /** Add the {@link CaptureRequestParameter} that will be applied. */
+        // TODO(b/120949879): This is camera2 implementation detail that should be moved
+        public void addCharacteristics(Map<Key<?>, CaptureRequestParameter<?>> characteristics) {
+            mCaptureRequestConfigBuilder.addCharacteristics(characteristics);
+        }
+
+        /** Set the {@link Configuration} for options that are implementation specific. */
+        public void setImplementationOptions(Configuration config) {
+            mCaptureRequestConfigBuilder.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<>(mSurfaces),
+                    mDeviceStateCallback,
+                    mSessionStateCallback,
+                    mCaptureRequestConfigBuilder.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
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public static final class ValidatingBuilder extends BaseBuilder {
+        private static final String TAG = "ValidatingBuilder";
+        private final List<CameraDevice.StateCallback> mDeviceStateCallbacks = new ArrayList<>();
+        private final List<CameraCaptureSession.StateCallback> mSessionStateCallbacks =
+                new ArrayList<>();
+        private final List<CameraCaptureCallback> mCameraCaptureCallbacks = new ArrayList<>();
+        private boolean mValid = true;
+        private boolean mTemplateSet = 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 (!mTemplateSet) {
+                mCaptureRequestConfigBuilder.setTemplateType(
+                        captureRequestConfiguration.getTemplateType());
+                mTemplateSet = true;
+            } else if (mCaptureRequestConfigBuilder.getTemplateType()
+                    != captureRequestConfiguration.getTemplateType()) {
+                String errorMessage =
+                        "Invalid configuration due to template type: "
+                                + mCaptureRequestConfigBuilder.getTemplateType()
+                                + " != "
+                                + captureRequestConfiguration.getTemplateType();
+                Log.d(TAG, errorMessage);
+                mValid = false;
+            }
+
+            // Check device state callback
+            mDeviceStateCallbacks.add(sessionConfiguration.getDeviceStateCallback());
+
+            // Check session state callback
+            mSessionStateCallbacks.add(sessionConfiguration.getSessionStateCallback());
+
+            // Check camera capture callback
+            mCameraCaptureCallbacks.add(captureRequestConfiguration.getCameraCaptureCallback());
+
+            // Check surfaces
+            mSurfaces.addAll(sessionConfiguration.getSurfaces());
+
+            // Check capture request surfaces
+            mCaptureRequestConfigBuilder
+                    .getSurfaces()
+                    .addAll(captureRequestConfiguration.getSurfaces());
+
+            mCaptureRequestConfigBuilder.addImplementationOptions(
+                    captureRequestConfiguration.getImplementationOptions());
+
+            if (!mSurfaces.containsAll(mCaptureRequestConfigBuilder.getSurfaces())) {
+                String errorMessage =
+                        "Invalid configuration due to capture request surfaces are not a subset "
+                                + "of surfaces";
+                Log.d(TAG, errorMessage);
+                mValid = false;
+            }
+
+            // Check characteristics
+            for (Map.Entry<Key<?>, CaptureRequestParameter<?>> entry :
+                    captureRequestConfiguration.getCameraCharacteristics().entrySet()) {
+                Key<?> addedKey = entry.getKey();
+                if (mCaptureRequestConfigBuilder.getCharacteristic().containsKey(entry.getKey())) {
+                    // value is equal
+                    CaptureRequestParameter<?> addedValue = entry.getValue();
+                    CaptureRequestParameter<?> oldValue =
+                            mCaptureRequestConfigBuilder.getCharacteristic().get(addedKey);
+                    if (!addedValue.getValue().equals(oldValue.getValue())) {
+                        String errorMessage =
+                                "Invalid configuration due to conflicting CaptureRequest.Keys: "
+                                        + addedValue
+                                        + " != "
+                                        + oldValue;
+                        Log.d(TAG, errorMessage);
+                        mValid = false;
+                    }
+                } else {
+                    mCaptureRequestConfigBuilder
+                            .getCharacteristic()
+                            .put(entry.getKey(), entry.getValue());
+                }
+            }
+        }
+
+        /** Check if the set of SessionConfiguration that have been combined are valid */
+        public boolean isValid() {
+            return mTemplateSet && mValid;
+        }
+
+        /**
+         * 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 (!mValid) {
+                throw new IllegalArgumentException("Unsupported session configuration combination");
+            }
+            mCaptureRequestConfigBuilder.setCameraCaptureCallback(
+                    CameraCaptureCallbacks.createComboCallback(mCameraCaptureCallbacks));
+            return new SessionConfiguration(
+                    new ArrayList<>(mSurfaces),
+                    CameraDeviceStateCallbacks.createComboCallback(mDeviceStateCallbacks),
+                    CameraCaptureSessionStateCallbacks.createComboCallback(mSessionStateCallbacks),
+                    mCaptureRequestConfigBuilder.build());
+        }
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SettableImageProxy.java b/camera/core/src/main/java/androidx/camera/core/SettableImageProxy.java
new file mode 100644
index 0000000..4e7d564
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SettableImageProxy.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+/**
+ * An {@link ImageProxy} which overwrites the {@link ImageInfo}.
+ */
+final class SettableImageProxy extends ForwardingImageProxy{
+    private final ImageInfo mImageInfo;
+
+    /**
+     * Constructor for a {@link SettableImageProxy}.
+     *
+     * @param imageProxy The {@link ImageProxy} to forward.
+     * @param imageInfo The {@link ImageInfo} to overwrite with.
+     */
+    SettableImageProxy(ImageProxy imageProxy, ImageInfo imageInfo) {
+        super(imageProxy);
+        mImageInfo = imageInfo;
+    }
+
+    @Override
+    public ImageInfo getImageInfo() {
+        return mImageInfo;
+    }
+}
diff --git a/camera/core/src/main/java/androidx/camera/core/SettableImageProxyBundle.java b/camera/core/src/main/java/androidx/camera/core/SettableImageProxyBundle.java
new file mode 100644
index 0000000..13b831a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SettableImageProxyBundle.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import androidx.annotation.GuardedBy;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@link ImageProxyBundle} with a predefined set of captured ids. The {@link ListenableFuture}
+ * for the capture id becomes valid when the corresponding {@link ImageProxy} has been set.
+ */
+final class SettableImageProxyBundle implements ImageProxyBundle {
+    private final Object mLock = new Object();
+
+    // Whether or not the bundle has been closed or not
+    @GuardedBy("mLock")
+    private boolean mClosed = false;
+
+    /** Map of id to {@link ImageProxy} Future. */
+    @GuardedBy("mLock")
+    private final Map<Integer, SettableFuture<ImageProxy>> mFutureResults = new HashMap<>();
+
+    private final List<Integer> mCaptureIdList;
+
+    @Override
+    public ListenableFuture<ImageProxy> getImageProxy(int captureId) {
+        synchronized (mLock) {
+            if (mClosed) {
+                throw new IllegalStateException("ImageProxyBundle already closed.");
+            }
+
+            // Returns the future that has been set if it exists
+            if (!mFutureResults.containsKey(captureId)) {
+                throw new IllegalArgumentException(
+                        "ImageProxyBundle does not contain this id: " + captureId);
+            }
+
+            return mFutureResults.get(captureId);
+        }
+    }
+
+    @Override
+    public List<Integer> getCaptureIds() {
+        return Collections.unmodifiableList(new ArrayList<>(mFutureResults.keySet()));
+    }
+
+    /**
+     * Create a {@link ImageProxyBundle} for captures with the given ids.
+     *
+     * @param captureIds The set of captureIds contained by the ImageProxyBundle
+     */
+    SettableImageProxyBundle(List<Integer> captureIds) {
+        mCaptureIdList = captureIds;
+        setup();
+    }
+
+    /**
+     * Add an {@link ImageProxy} to synchronize.
+     */
+    void addImageProxy(ImageProxy imageProxy) {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+
+            Integer captureId = (Integer) imageProxy.getImageInfo().getTag();
+            if (captureId == null) {
+                throw new IllegalArgumentException("CaptureId is null.");
+            }
+
+            // If the CaptureId is associated with this SettableImageProxyBundle, set the
+            // corresponding Future. Otherwise, throws exception.
+            if (mFutureResults.containsKey(captureId)) {
+                SettableFuture<ImageProxy> futureResult = mFutureResults.get(captureId);
+                futureResult.set(imageProxy);
+            } else {
+                throw new IllegalArgumentException(
+                        "ImageProxyBundle does not contain this id: " + captureId);
+            }
+        }
+    }
+
+    /**
+     * Flush all {@link ImageProxy} that have been added.
+     */
+    void close() {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+            mFutureResults.clear();
+            mClosed = true;
+        }
+    }
+
+    /**
+     * Clear all {@link ImageProxy} that have been added and recreate the entries from the bundle.
+     */
+    void reset() {
+        synchronized (mLock) {
+            if (mClosed) {
+                return;
+            }
+
+            mFutureResults.clear();
+            setup();
+        }
+    }
+
+    private void setup() {
+        for (Integer captureId : mCaptureIdList) {
+            SettableFuture<ImageProxy> futureResult = SettableFuture.create();
+            mFutureResults.put(captureId, futureResult);
+        }
+    }
+}
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..1f0ca63
--- /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 mClosed = 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 (!mClosed) {
+            mClosed = 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..c64cb4f
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceCombination.java
@@ -0,0 +1,148 @@
+/*
+ * 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> mSurfaceConfigurationList = 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);
+            }
+        }
+    }
+
+    /** Adds a {@link SurfaceConfiguration} to the combination. */
+    public boolean addSurfaceConfiguration(SurfaceConfiguration surfaceConfiguration) {
+        if (surfaceConfiguration == null) {
+            return false;
+        }
+
+        return mSurfaceConfigurationList.add(surfaceConfiguration);
+    }
+
+    /** Removes a {@link SurfaceConfiguration} from the combination. */
+    public boolean removeSurfaceConfiguration(SurfaceConfiguration surfaceConfiguration) {
+        if (surfaceConfiguration == null) {
+            return false;
+        }
+
+        return mSurfaceConfigurationList.remove(surfaceConfiguration);
+    }
+
+    public List<SurfaceConfiguration> getSurfaceConfigurationList() {
+        return mSurfaceConfigurationList;
+    }
+
+    /**
+     * 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() > mSurfaceConfigurationList.size()) {
+            return false;
+        }
+
+        List<int[]> elementsArrangements = getElementsArrangements(
+                mSurfaceConfigurationList.size());
+
+        for (int[] elementsArrangement : elementsArrangements) {
+            boolean checkResult = true;
+
+            for (int index = 0; index < mSurfaceConfigurationList.size(); index++) {
+                if (elementsArrangement[index] < configurationList.size()) {
+                    checkResult &=
+                            mSurfaceConfigurationList
+                                    .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..edb7809
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceConfiguration.java
@@ -0,0 +1,129 @@
+/*
+ * 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 subclassing */
+    SurfaceConfiguration() {
+    }
+
+    /**
+     * Creates a new instance of SurfaceConfiguration with the given parameters.
+     */
+    public static SurfaceConfiguration create(ConfigurationType type, ConfigurationSize size) {
+        return new AutoValue_SurfaceConfiguration(type, size);
+    }
+
+    /** Returns the configuration type. */
+    public abstract ConfigurationType getConfigurationType();
+
+    /** Returns the configuration size. */
+    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.getId() <= getConfigurationSize().getId()
+                && 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)}.
+     */
+    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)}.
+     */
+    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 mId;
+
+        ConfigurationSize(int id) {
+            mId = id;
+        }
+
+        int getId() {
+            return mId;
+        }
+    }
+}
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..1f982f4
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/SurfaceSizeDefinition.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.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 SurfaceSizeDefinition object with input analysis, preview, record and maximum sizes
+     *
+     * @param analysisSize   Default ANALYSIS 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);
+    }
+
+    /** Returns the size of an ANALYSIS stream. */
+    public abstract Size getAnalysisSize();
+
+    /** Returns the size of a PREVIEW stream. */
+    public abstract Size getPreviewSize();
+
+    /** Returns the size of a RECORD stream*/
+    public abstract Size getRecordSize();
+
+    /**
+     * Returns a map of image format to resolution.
+     * @return a map with the image format as the key and resolution as the value.
+     */
+    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..eed17f6
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/TargetConfiguration.java
@@ -0,0 +1,131 @@
+/*
+ * 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 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
+    Class<T> getTargetClass(@Nullable Class<T> valueIfMissing);
+
+    /**
+     * 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.
+     */
+    Class<T> getTargetClass();
+
+    /**
+     * 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
+    String getTargetName(@Nullable String 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.
+     */
+    String getTargetName();
+
+    /**
+     * 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
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        B setTargetClass(Class<T> targetClass);
+
+        /**
+         * 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.
+         */
+        B setTargetName(String targetName);
+    }
+}
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..d9ba5a2
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ThreadConfiguration.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.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
+    Handler getCallbackHandler(@Nullable 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.
+     */
+    Handler getCallbackHandler();
+
+    // 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.
+         */
+        B setCallbackHandler(Handler handler);
+    }
+}
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..36cc2bf
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/TypeReference.java
@@ -0,0 +1,377 @@
+/*
+ * 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.
+ *
+ * @param <T> the type to capture
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public abstract class TypeReference<T> {
+    private final Type mType;
+    private final int mHash;
+
+    /**
+     * 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>
+        mType = 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(mType)) {
+            throw new IllegalArgumentException(
+                    "Including a type variable in a type reference is not allowed");
+        }
+        mHash = mType.hashCode();
+    }
+
+    TypeReference(Type type) {
+        mType = type;
+        if (containsTypeVariable(mType)) {
+            throw new IllegalArgumentException(
+                    "Including a type variable in a type reference is not allowed");
+        }
+        mHash = mType.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 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 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 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 mType;
+    }
+
+    /**
+     * 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(mType);
+    }
+
+    /**
+     * 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<?> && mType.equals(((TypeReference<?>) o).mType);
+    }
+
+    @Override
+    public int hashCode() {
+        return mHash;
+    }
+
+    @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> {
+        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..3ed507b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseAttachState.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.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 mCameraId;
+    /** A map of the use cases to the corresponding state information. */
+    private final Map<BaseUseCase, UseCaseAttachInfo> mAttachedUseCasesToInfoMap = new HashMap<>();
+
+    /** Constructs an instance of the attach state which corresponds to the named camera. */
+    public UseCaseAttachState(String cameraId) {
+        mCameraId = 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.setActive(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 (!mAttachedUseCasesToInfoMap.containsKey(useCase)) {
+            return;
+        }
+
+        UseCaseAttachInfo useCaseAttachInfo = mAttachedUseCasesToInfoMap.get(useCase);
+        useCaseAttachInfo.setActive(false);
+        if (!useCaseAttachInfo.getOnline()) {
+            mAttachedUseCasesToInfoMap.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.setOnline(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 (!mAttachedUseCasesToInfoMap.containsKey(useCase)) {
+            return;
+        }
+        UseCaseAttachInfo useCaseAttachInfo = mAttachedUseCasesToInfoMap.get(useCase);
+        useCaseAttachInfo.setOnline(false);
+        if (!useCaseAttachInfo.getActive()) {
+            mAttachedUseCasesToInfoMap.remove(useCase);
+        }
+    }
+
+    public Collection<BaseUseCase> getOnlineUseCases() {
+        return Collections.unmodifiableCollection(
+                getUseCases(new AttachStateFilter() {
+                    @Override
+                    public boolean filter(UseCaseAttachInfo useCaseAttachInfo) {
+                        return useCaseAttachInfo.getOnline();
+                    }
+                }));
+    }
+
+    public Collection<BaseUseCase> getActiveAndOnlineUseCases() {
+        return Collections.unmodifiableCollection(
+                getUseCases(
+                        new AttachStateFilter() {
+                            @Override
+                            public boolean filter(UseCaseAttachInfo useCaseAttachInfo) {
+                                return useCaseAttachInfo.getActive()
+                                        && useCaseAttachInfo.getOnline();
+                            }
+                        }));
+    }
+
+    /**
+     * 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 (!mAttachedUseCasesToInfoMap.containsKey(useCase)) {
+            return;
+        }
+
+        // Rebuild the attach info from scratch to get the updated SessionConfiguration.
+        UseCaseAttachInfo newUseCaseAttachInfo =
+                new UseCaseAttachInfo(useCase.getSessionConfiguration(mCameraId));
+
+        // Retain the online and active flags.
+        UseCaseAttachInfo oldUseCaseAttachInfo = mAttachedUseCasesToInfoMap.get(useCase);
+        newUseCaseAttachInfo.setOnline(oldUseCaseAttachInfo.getOnline());
+        newUseCaseAttachInfo.setActive(oldUseCaseAttachInfo.getActive());
+        mAttachedUseCasesToInfoMap.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 :
+                mAttachedUseCasesToInfoMap.entrySet()) {
+            UseCaseAttachInfo useCaseAttachInfo = attachedUseCase.getValue();
+            if (useCaseAttachInfo.getActive() && useCaseAttachInfo.getOnline()) {
+                BaseUseCase baseUseCase = attachedUseCase.getKey();
+                validatingBuilder.add(useCaseAttachInfo.getSessionConfiguration());
+                list.add(baseUseCase.getName());
+            }
+        }
+        Log.d(TAG, "Active and online use case: " + list + " for camera: " + mCameraId);
+        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 :
+                mAttachedUseCasesToInfoMap.entrySet()) {
+            UseCaseAttachInfo useCaseAttachInfo = attachedUseCase.getValue();
+            if (useCaseAttachInfo.getOnline()) {
+                validatingBuilder.add(useCaseAttachInfo.getSessionConfiguration());
+                BaseUseCase baseUseCase = attachedUseCase.getKey();
+                list.add(baseUseCase.getName());
+            }
+        }
+        Log.d(TAG, "All use case: " + list + " for camera: " + mCameraId);
+        return validatingBuilder;
+    }
+
+    private UseCaseAttachInfo getOrCreateUseCaseAttachInfo(BaseUseCase useCase) {
+        UseCaseAttachInfo useCaseAttachInfo = mAttachedUseCasesToInfoMap.get(useCase);
+        if (useCaseAttachInfo == null) {
+            useCaseAttachInfo = new UseCaseAttachInfo(useCase.getSessionConfiguration(mCameraId));
+            mAttachedUseCasesToInfoMap.put(useCase, useCaseAttachInfo);
+        }
+        return useCaseAttachInfo;
+    }
+
+    private Collection<BaseUseCase> getUseCases(AttachStateFilter attachStateFilter) {
+        List<BaseUseCase> useCases = new ArrayList<>();
+        for (Entry<BaseUseCase, UseCaseAttachInfo> attachedUseCase :
+                mAttachedUseCasesToInfoMap.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. */
+        private final SessionConfiguration mSessionConfiguration;
+        /**
+         * True if the use case is currently online (i.e. camera should have a capture session
+         * configured for it).
+         */
+        private boolean mOnline = false;
+
+        /**
+         * True if the use case is currently active (i.e. camera should be issuing capture requests
+         * for it).
+         */
+        private boolean mActive = false;
+
+        UseCaseAttachInfo(SessionConfiguration sessionConfiguration) {
+            mSessionConfiguration = sessionConfiguration;
+        }
+
+        SessionConfiguration getSessionConfiguration() {
+            return mSessionConfiguration;
+        }
+
+        boolean getOnline() {
+            return mOnline;
+        }
+
+        void setOnline(boolean online) {
+            mOnline = online;
+        }
+
+        boolean getActive() {
+            return mActive;
+        }
+
+        void setActive(boolean active) {
+            mActive = active;
+        }
+    }
+}
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..996f3aa8
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseConfiguration.java
@@ -0,0 +1,199 @@
+/*
+ * 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
+    SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration 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)
+    SessionConfiguration getDefaultSessionConfiguration();
+
+    /**
+     * 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
+    SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker 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)
+    SessionConfiguration.OptionUnpacker getOptionUnpacker();
+
+    // 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)
+    int getSurfaceOccupancyPriority(int 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)
+    int getSurfaceOccupancyPriority();
+
+    /**
+     * 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)
+        B setDefaultSessionConfiguration(SessionConfiguration sessionConfig);
+
+        /**
+         * 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)
+        B setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker);
+
+        /**
+         * 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)
+        B setSurfaceOccupancyPriority(int priority);
+    }
+}
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..5435393
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseConfigurationFactory.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+     *
+     * @param lensFacing The {@link CameraX.LensFacing} that the configuration will target to.
+     */
+    @Nullable
+    <C extends UseCaseConfiguration<?>> C getConfiguration(Class<C> configType,
+            CameraX.LensFacing lensFacing);
+}
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..576b175
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -0,0 +1,176 @@
+/*
+ * 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 mUseCasesLock so that there is no
+     * lock-ordering deadlock.
+     */
+    private final Object mListenerLock = 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 mListenerLock so that there is no
+     * lock-ordering deadlock.
+     */
+    private final Object mUseCasesLock = new Object();
+    @GuardedBy("mUseCasesLock")
+    private final Set<BaseUseCase> mUseCases = new HashSet<>();
+    @GuardedBy("mListenerLock")
+    private StateChangeListener mListener;
+
+    /** Starts all the use cases so that they are brought into an online state. */
+    void start() {
+        synchronized (mListenerLock) {
+            if (mListener != null) {
+                mListener.onGroupActive(this);
+            }
+        }
+    }
+
+    /** Stops all the use cases so that they are brought into an offline state. */
+    void stop() {
+        synchronized (mListenerLock) {
+            if (mListener != null) {
+                mListener.onGroupInactive(this);
+            }
+        }
+    }
+
+    void setListener(StateChangeListener listener) {
+        synchronized (mListenerLock) {
+            this.mListener = 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 (mUseCasesLock) {
+            return mUseCases.add(useCase);
+        }
+    }
+
+    /** Returns true if the {@link BaseUseCase} is contained in the group. */
+    boolean contains(BaseUseCase useCase) {
+        synchronized (mUseCasesLock) {
+            return mUseCases.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 (mUseCasesLock) {
+            return mUseCases.remove(useCase);
+        }
+    }
+
+    /** Clears all use cases from this group. */
+    public void clear() {
+        List<BaseUseCase> useCasesToClear = new ArrayList<>();
+        synchronized (mUseCasesLock) {
+            useCasesToClear.addAll(mUseCases);
+            mUseCases.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 (mUseCasesLock) {
+            return Collections.unmodifiableCollection(mUseCases);
+        }
+    }
+
+    Map<String, Set<BaseUseCase>> getCameraIdToUseCaseMap() {
+        Map<String, Set<BaseUseCase>> cameraIdToUseCases = new HashMap<>();
+        synchronized (mUseCasesLock) {
+            for (BaseUseCase useCase : mUseCases) {
+                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..4c9fcbe
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupLifecycleController.java
@@ -0,0 +1,106 @@
+/*
+ * 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.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+
+/** A {@link UseCaseGroup} whose starting and stopping is controlled by a {@link Lifecycle}. */
+final class UseCaseGroupLifecycleController implements LifecycleObserver {
+    private final Object mUseCaseGroupLock = new Object();
+
+    @GuardedBy("mUseCaseGroupLock")
+    private final UseCaseGroup mUseCaseGroup;
+
+    /** The lifecycle that controls the {@link UseCaseGroup}. */
+    private final Lifecycle mLifecycle;
+
+    /** 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.mUseCaseGroup = useCaseGroup;
+        this.mLifecycle = lifecycle;
+        lifecycle.addObserver(this);
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_START)
+    public void onStart(LifecycleOwner lifecycleOwner) {
+        synchronized (mUseCaseGroupLock) {
+            mUseCaseGroup.start();
+        }
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+    public void onStop(LifecycleOwner lifecycleOwner) {
+        synchronized (mUseCaseGroupLock) {
+            mUseCaseGroup.stop();
+        }
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+    public void onDestroy(LifecycleOwner lifecycleOwner) {
+        synchronized (mUseCaseGroupLock) {
+            mUseCaseGroup.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 (mUseCaseGroupLock) {
+            if (mLifecycle.getCurrentState().isAtLeast(State.STARTED)) {
+                mUseCaseGroup.start();
+            }
+            for (BaseUseCase useCase : mUseCaseGroup.getUseCases()) {
+                useCase.notifyState();
+            }
+        }
+    }
+
+    UseCaseGroup getUseCaseGroup() {
+        synchronized (mUseCaseGroupLock) {
+            return mUseCaseGroup;
+        }
+    }
+
+    /**
+     * 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() {
+        mLifecycle.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..f18df0b
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/UseCaseGroupRepository.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.GuardedBy;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+
+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 mUseCasesLock = new Object();
+
+    @GuardedBy("mUseCasesLock")
+    final Map<LifecycleOwner, UseCaseGroupLifecycleController>
+            mLifecycleToUseCaseGroupControllerMap =
+            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, new UseCaseGroupSetup() {
+            @Override
+            public void setup(UseCaseGroup 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 (mUseCasesLock) {
+            useCaseGroupLifecycleController = mLifecycleToUseCaseGroupControllerMap.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 androidx.lifecycle.Lifecycle} of
+     *                                  lifecycleOwner is already
+     *                                  {@link androidx.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 (mUseCasesLock) {
+            mLifecycleToUseCaseGroupControllerMap.put(lifecycleOwner,
+                    useCaseGroupLifecycleController);
+        }
+        return useCaseGroupLifecycleController;
+    }
+
+    /**
+     * Creates a {@link LifecycleObserver} which removes any {@link
+     * UseCaseGroupLifecycleController} associated with a {@link LifecycleOwner} from this
+     * repository when that lifecycle is destroyed.
+     *
+     * @return a new {@link LifecycleObserver}
+     */
+    private LifecycleObserver createRemoveOnDestroyObserver() {
+        return new LifecycleObserver() {
+            @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+            public void onDestroy(LifecycleOwner lifecycleOwner) {
+                synchronized (mUseCasesLock) {
+                    mLifecycleToUseCaseGroupControllerMap.remove(lifecycleOwner);
+                }
+                lifecycleOwner.getLifecycle().removeObserver(this);
+            }
+        };
+    }
+
+    Collection<UseCaseGroupLifecycleController> getUseCaseGroups() {
+        synchronized (mUseCasesLock) {
+            return Collections.unmodifiableCollection(
+                    mLifecycleToUseCaseGroupControllerMap.values());
+        }
+    }
+
+    @VisibleForTesting
+    Map<LifecycleOwner, UseCaseGroupLifecycleController> getUseCasesMap() {
+        synchronized (mUseCasesLock) {
+            return mLifecycleToUseCaseGroupControllerMap;
+        }
+    }
+
+    /**
+     * 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..cd12669
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCase.java
@@ -0,0 +1,934 @@
+/*
+ * 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.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.
+ *
+ * @hide In the earlier stage, the VideoCaptureUseCase is deprioritized.
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+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[] sAudioEncoding = {
+            AudioFormat.ENCODING_PCM_16BIT,
+            AudioFormat.ENCODING_PCM_8BIT,
+            AudioFormat.ENCODING_PCM_FLOAT
+    };
+    private final BufferInfo mVideoBufferInfo = new BufferInfo();
+    private final Object mMuxerLock = new Object();
+    /** Thread on which all encoding occurs. */
+    private final HandlerThread mVideoHandlerThread =
+            new HandlerThread(CameraXThreads.TAG + "video encoding thread");
+    private final Handler mVideoHandler;
+    /** Thread on which audio encoding occurs. */
+    private final HandlerThread mAudioHandlerThread =
+            new HandlerThread(CameraXThreads.TAG + "audio encoding thread");
+    private final Handler mAudioHandler;
+    private final AtomicBoolean mEndOfVideoStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean mEndOfAudioStreamSignal = new AtomicBoolean(true);
+    private final AtomicBoolean mEndOfAudioVideoSignal = new AtomicBoolean(true);
+    private final BufferInfo mAudioBufferInfo = new BufferInfo();
+    /** For record the first sample written time. */
+    private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
+    private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
+    private final VideoCaptureUseCaseConfiguration.Builder mUseCaseConfigBuilder;
+    @NonNull
+    MediaCodec mVideoEncoder;
+    @NonNull
+    private MediaCodec mAudioEncoder;
+    /** The muxer that writes the encoding data to file. */
+    @GuardedBy("mMuxerLock")
+    private MediaMuxer mMuxer;
+    private boolean mMuxerStarted = false;
+    /** The index of the video track used by the muxer. */
+    private int mVideoTrackIndex;
+    /** The index of the audio track used by the muxer. */
+    private int mAudioTrackIndex;
+    /** Surface the camera writes to, which the videoEncoder uses as input. */
+    Surface mCameraSurface;
+    /** audio raw data */
+    @NonNull
+    private AudioRecord mAudioRecorder;
+    private int mAudioBufferSize;
+    private boolean mIsRecording = false;
+    private int mAudioChannelCount;
+    private int mAudioSampleRate;
+    private int mAudioBitRate;
+    private DeferrableSurface mDeferrableSurface;
+
+    /**
+     * Creates a new video capture use case from the given configuration.
+     *
+     * @param configuration for this use case instance
+     */
+    public VideoCaptureUseCase(VideoCaptureUseCaseConfiguration configuration) {
+        super(configuration);
+        mUseCaseConfigBuilder = VideoCaptureUseCaseConfiguration.Builder.fromConfig(configuration);
+
+        // video thread start
+        mVideoHandlerThread.start();
+        mVideoHandler = new Handler(mVideoHandlerThread.getLooper());
+
+        // audio thread start
+        mAudioHandlerThread.start();
+        mAudioHandler = new Handler(mAudioHandlerThread.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 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
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
+        VideoCaptureUseCaseConfiguration defaults = CameraX.getDefaultUseCaseConfiguration(
+                VideoCaptureUseCaseConfiguration.class, lensFacing);
+        if (defaults != null) {
+            return VideoCaptureUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected Map<String, Size> onSuggestedResolutionUpdated(
+            Map<String, Size> suggestedResolutionMap) {
+        VideoCaptureUseCaseConfiguration configuration =
+                (VideoCaptureUseCaseConfiguration) getUseCaseConfiguration();
+        if (mCameraSurface != null) {
+            mVideoEncoder.stop();
+            mVideoEncoder.release();
+            mAudioEncoder.stop();
+            mAudioEncoder.release();
+            releaseCameraSurface();
+        }
+
+        try {
+            mVideoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
+            mAudioEncoder = 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) {
+        mIsFirstVideoSampleWrite.set(false);
+        mIsFirstAudioSampleWrite.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(
+            final File saveLocation, final OnVideoSavedListener listener, Metadata metadata) {
+        Log.i(TAG, "startRecording");
+
+        if (!mEndOfAudioVideoSignal.get()) {
+            listener.onError(
+                    UseCaseError.RECORDING_IN_PROGRESS, "It is still in video recording!", null);
+            return;
+        }
+
+        try {
+            // audioRecord start
+            mAudioRecorder.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");
+            mVideoEncoder.start();
+            // audio encoder start
+            Log.i(TAG, "audioEncoder start");
+            mAudioEncoder.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 (mMuxerLock) {
+                mMuxer =
+                        new MediaMuxer(
+                                saveLocation.getAbsolutePath(),
+                                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+                mMuxer.setOrientationHint(relativeRotation);
+                if (metadata.location != null) {
+                    mMuxer.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;
+        }
+
+        mEndOfVideoStreamSignal.set(false);
+        mEndOfAudioStreamSignal.set(false);
+        mEndOfAudioVideoSignal.set(false);
+        mIsRecording = true;
+
+        notifyActive();
+        mAudioHandler.post(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        VideoCaptureUseCase.this.audioEncode(listener);
+                    }
+                });
+
+        mVideoHandler.post(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        boolean errorOccurred = VideoCaptureUseCase.this.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 (!mEndOfAudioVideoSignal.get() && mIsRecording) {
+            // stop audio encoder thread, and wait video encoder and muxer stop.
+            mEndOfAudioStreamSignal.set(true);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void clear() {
+        mVideoHandlerThread.quitSafely();
+
+        if (mVideoEncoder != null) {
+            mVideoEncoder.release();
+            mVideoEncoder = null;
+        }
+
+        // audio encoder release
+        mAudioHandlerThread.quitSafely();
+        if (mAudioEncoder != null) {
+            mAudioEncoder.release();
+            mAudioEncoder = null;
+        }
+
+        if (mAudioRecorder != null) {
+            mAudioRecorder.release();
+            mAudioRecorder = null;
+        }
+
+        if (mCameraSurface != null) {
+            releaseCameraSurface();
+        }
+
+        super.clear();
+    }
+
+    private void releaseCameraSurface() {
+        if (mDeferrableSurface == null) {
+            return;
+        }
+
+        final Surface surface = mCameraSurface;
+        mDeferrableSurface.setOnSurfaceDetachedListener(
+                MainThreadExecutor.getInstance(),
+                new DeferrableSurface.OnSurfaceDetachedListener() {
+                    @Override
+                    public void onSurfaceDetached() {
+                        if (surface != null) {
+                            surface.release();
+                        }
+                    }
+                });
+
+        mCameraSurface = null;
+        mDeferrableSurface = null;
+    }
+
+
+    /**
+     * 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) {
+            mUseCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(mUseCaseConfigBuilder.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
+        mVideoEncoder.reset();
+        mVideoEncoder.configure(
+                createMediaFormat(configuration, resolution), /*surface*/
+                null, /*crypto*/
+                null,
+                MediaCodec.CONFIGURE_FLAG_ENCODE);
+        if (mCameraSurface != null) {
+            releaseCameraSurface();
+        }
+        mCameraSurface = mVideoEncoder.createInputSurface();
+
+        SessionConfiguration.Builder builder =
+                SessionConfiguration.Builder.createFrom(configuration);
+
+        mDeferrableSurface = new ImmediateSurface(mCameraSurface);
+
+        builder.addSurface(mDeferrableSurface);
+
+        String cameraId = getCameraIdUnchecked(configuration.getLensFacing());
+        attachToCamera(cameraId, builder.build());
+
+        // audio encoder setup
+        setAudioParametersByCamcorderProfile(resolution, cameraId);
+        mAudioEncoder.reset();
+        mAudioEncoder.configure(
+                createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
+
+        if (mAudioRecorder != null) {
+            mAudioRecorder.release();
+        }
+        mAudioRecorder = autoConfigAudioRecordSource(configuration);
+        // check mAudioRecorder
+        if (mAudioRecorder == null) {
+            Log.e(TAG, "AudioRecord object cannot initialized correctly!");
+        }
+
+        mVideoTrackIndex = -1;
+        mAudioTrackIndex = -1;
+        mIsRecording = 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 = mVideoEncoder.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 mMuxer if available
+        if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0 && mVideoBufferInfo.size > 0) {
+            outputBuffer.position(mVideoBufferInfo.offset);
+            outputBuffer.limit(mVideoBufferInfo.offset + mVideoBufferInfo.size);
+            mVideoBufferInfo.presentationTimeUs = (System.nanoTime() / 1000);
+
+            synchronized (mMuxerLock) {
+                if (!mIsFirstVideoSampleWrite.get()) {
+                    Log.i(TAG, "First video sample written.");
+                    mIsFirstVideoSampleWrite.set(true);
+                }
+                mMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, mVideoBufferInfo);
+            }
+        }
+
+        // Release data
+        mVideoEncoder.releaseOutputBuffer(bufferIndex, false);
+
+        // Return true if EOS is set
+        return (mVideoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
+    }
+
+    private boolean writeAudioEncodedBuffer(int bufferIndex) {
+        ByteBuffer buffer = getOutputBuffer(mAudioEncoder, bufferIndex);
+        buffer.position(mAudioBufferInfo.offset);
+        if (mAudioTrackIndex >= 0
+                && mVideoTrackIndex >= 0
+                && mAudioBufferInfo.size > 0
+                && mAudioBufferInfo.presentationTimeUs > 0) {
+            try {
+                synchronized (mMuxerLock) {
+                    if (!mIsFirstAudioSampleWrite.get()) {
+                        Log.i(TAG, "First audio sample written.");
+                        mIsFirstAudioSampleWrite.set(true);
+                    }
+                    mMuxer.writeSampleData(mAudioTrackIndex, buffer, mAudioBufferInfo);
+                }
+            } catch (Exception e) {
+                Log.e(
+                        TAG,
+                        "audio error:size="
+                                + mAudioBufferInfo.size
+                                + "/offset="
+                                + mAudioBufferInfo.offset
+                                + "/timeUs="
+                                + mAudioBufferInfo.presentationTimeUs);
+                e.printStackTrace();
+            }
+        }
+        mAudioEncoder.releaseOutputBuffer(bufferIndex, false);
+        return (mAudioBufferInfo.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}
+     */
+    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 (mEndOfVideoStreamSignal.get()) {
+                mVideoEncoder.signalEndOfInputStream();
+                mEndOfVideoStreamSignal.set(false);
+            }
+
+            // Deque buffer to check for processing step
+            int outputBufferId =
+                    mVideoEncoder.dequeueOutputBuffer(mVideoBufferInfo, DEQUE_TIMEOUT_USEC);
+            switch (outputBufferId) {
+                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                    if (mMuxerStarted) {
+                        videoSavedListener.onError(
+                                UseCaseError.ENCODER_ERROR,
+                                "Unexpected change in video encoding format.",
+                                null);
+                        errorOccurred = true;
+                    }
+
+                    synchronized (mMuxerLock) {
+                        mVideoTrackIndex = mMuxer.addTrack(mVideoEncoder.getOutputFormat());
+                        if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
+                            mMuxerStarted = true;
+                            Log.i(TAG, "media mMuxer start");
+                            mMuxer.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");
+            mVideoEncoder.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 (mMuxerLock) {
+                if (mMuxer != null) {
+                    if (mMuxerStarted) {
+                        mMuxer.stop();
+                    }
+                    mMuxer.release();
+                    mMuxer = null;
+                }
+            }
+        } catch (IllegalStateException e) {
+            videoSavedListener.onError(UseCaseError.MUXER_ERROR, "Muxer stop failed!", e);
+            errorOccurred = true;
+        }
+
+        mMuxerStarted = 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
+        mEndOfAudioVideoSignal.set(true);
+
+        Log.i(TAG, "Video encode thread end.");
+        return errorOccurred;
+    }
+
+    boolean audioEncode(OnVideoSavedListener videoSavedListener) {
+        // Audio encoding loop. Exits on end of stream.
+        boolean audioEos = false;
+        int outIndex;
+        while (!audioEos && mIsRecording) {
+            // Check for end of stream from main thread
+            if (mEndOfAudioStreamSignal.get()) {
+                mEndOfAudioStreamSignal.set(false);
+                mIsRecording = false;
+            }
+
+            // get audio deque input buffer
+            if (mAudioEncoder != null && mAudioRecorder != null) {
+                int index = mAudioEncoder.dequeueInputBuffer(-1);
+                if (index >= 0) {
+                    final ByteBuffer buffer = getInputBuffer(mAudioEncoder, index);
+                    buffer.clear();
+                    int length = mAudioRecorder.read(buffer, mAudioBufferSize);
+                    if (length > 0) {
+                        mAudioEncoder.queueInputBuffer(
+                                index,
+                                0,
+                                length,
+                                (System.nanoTime() / 1000),
+                                mIsRecording ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+                    }
+                }
+
+                // start to dequeue audio output buffer
+                do {
+                    outIndex = mAudioEncoder.dequeueOutputBuffer(mAudioBufferInfo, 0);
+                    switch (outIndex) {
+                        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
+                            synchronized (mMuxerLock) {
+                                mAudioTrackIndex = mMuxer.addTrack(mAudioEncoder.getOutputFormat());
+                                if (mAudioTrackIndex >= 0 && mVideoTrackIndex >= 0) {
+                                    mMuxerStarted = true;
+                                    mMuxer.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");
+            mAudioRecorder.stop();
+        } catch (IllegalStateException e) {
+            videoSavedListener.onError(
+                    UseCaseError.ENCODER_ERROR, "Audio recorder stop failed!", e);
+        }
+
+        try {
+            mAudioEncoder.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
+        mEndOfVideoStreamSignal.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, mAudioSampleRate,
+                        mAudioChannelCount);
+        format.setInteger(
+                MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
+        format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitRate);
+
+        return format;
+    }
+
+    /** Create a AudioRecord object to get raw data */
+    private AudioRecord autoConfigAudioRecordSource(
+            VideoCaptureUseCaseConfiguration configuration) {
+        for (short audioFormat : sAudioEncoding) {
+
+            // Use channel count to determine stereo vs mono
+            int channelConfig =
+                    mAudioChannelCount == 1
+                            ? AudioFormat.CHANNEL_IN_MONO
+                            : AudioFormat.CHANNEL_IN_STEREO;
+            int source = configuration.getAudioRecordSource();
+
+            try {
+                int bufferSize =
+                        AudioRecord.getMinBufferSize(mAudioSampleRate, channelConfig, audioFormat);
+
+                if (bufferSize <= 0) {
+                    bufferSize = configuration.getAudioMinBufferSize();
+                }
+
+                AudioRecord recorder =
+                        new AudioRecord(
+                                source,
+                                mAudioSampleRate,
+                                channelConfig,
+                                audioFormat,
+                                bufferSize * 2);
+
+                if (recorder.getState() == AudioRecord.STATE_INITIALIZED) {
+                    mAudioBufferSize = bufferSize;
+                    Log.i(
+                            TAG,
+                            "source: "
+                                    + source
+                                    + " audioSampleRate: "
+                                    + mAudioSampleRate
+                                    + " 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) {
+                    mAudioChannelCount = profile.audioChannels;
+                    mAudioSampleRate = profile.audioSampleRate;
+                    mAudioBitRate = 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();
+            mAudioChannelCount = config.getAudioChannelCount();
+            mAudioSampleRate = config.getAudioSampleRate();
+            mAudioBitRate = 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(UseCaseError, String, Throwable)}.
+     *
+     * <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 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)
+                            .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(LensFacing lensFacing) {
+            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..71c1f45
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/VideoCaptureUseCaseConfiguration.java
@@ -0,0 +1,750 @@
+/*
+ * 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.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Set;
+import java.util.UUID;
+
+/** Configuration for a video capture use case.
+ *
+ * @hide In the earlier stage, the VideoCaptureUseCase is deprioritized.
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+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 mConfig;
+
+    VideoCaptureUseCaseConfiguration(OptionsBundle config) {
+        mConfig = config;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Configuration getConfiguration() {
+        return mConfig;
+    }
+
+    /**
+     * 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 mMutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            mMutableConfig = 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 mMutableConfig;
+        }
+
+        /** The solution for the unchecked cast warning. */
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public VideoCaptureUseCaseConfiguration build() {
+            return new VideoCaptureUseCaseConfiguration(OptionsBundle.from(mMutableConfig));
+        }
+
+        /**
+         * 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();
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // Implementations of TargetConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetClass(Class<VideoCaptureUseCase> 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();
+        }
+
+        @Override
+        public Builder setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+
+        // Implementations of CameraDeviceConfiguration.Builder default methods
+
+        @Override
+        public Builder setLensFacing(CameraX.LensFacing lensFacing) {
+            getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+            return builder();
+        }
+
+        // Implementations of ImageOutputConfiguration.Builder default methods
+
+        @Override
+        public Builder setTargetAspectRatio(Rational aspectRatio) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return builder();
+        }
+
+        @Override
+        public Builder setTargetRotation(@RotationValue int rotation) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_RESOLUTION, resolution);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setMaxResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return builder();
+        }
+
+        // Implementations of ThreadConfiguration.Builder default methods
+
+        @Override
+        public Builder setCallbackHandler(Handler handler) {
+            getMutableConfiguration().insertOption(OPTION_CALLBACK_HANDLER, handler);
+            return builder();
+        }
+
+        // Implementations of UseCaseConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+            getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+            getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // Implementations of TargetConfiguration default methods
+
+    @Override
+    @Nullable
+    public Class<VideoCaptureUseCase> getTargetClass(
+            @Nullable Class<VideoCaptureUseCase> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<VideoCaptureUseCase> storedClass =
+                (Class<VideoCaptureUseCase>) retrieveOption(
+                        OPTION_TARGET_CLASS,
+                        valueIfMissing);
+        return storedClass;
+    }
+
+    @Override
+    public Class<VideoCaptureUseCase> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<VideoCaptureUseCase> storedClass =
+                (Class<VideoCaptureUseCase>) retrieveOption(
+                        OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    @Override
+    @Nullable
+    public String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    @Override
+    public String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    // Implementations of CameraDeviceConfiguration default methods
+
+    @Override
+    @Nullable
+    public CameraX.LensFacing getLensFacing(@Nullable CameraX.LensFacing valueIfMissing) {
+        return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+    }
+
+    @Override
+    public CameraX.LensFacing getLensFacing() {
+        return retrieveOption(OPTION_LENS_FACING);
+    }
+
+    // Implementations of ImageOutputConfiguration default methods
+
+    @Override
+    @Nullable
+    public Rational getTargetAspectRatio(@Nullable Rational valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO, valueIfMissing);
+    }
+
+    @Override
+    public Rational getTargetAspectRatio() {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation(int valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ROTATION, valueIfMissing);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation() {
+        return retrieveOption(OPTION_TARGET_ROTATION);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getTargetResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_RESOLUTION, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getTargetResolution() {
+        return retrieveOption(OPTION_TARGET_RESOLUTION);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_MAX_RESOLUTION, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution() {
+        return retrieveOption(OPTION_MAX_RESOLUTION);
+    }
+
+    // Implementations of ThreadConfiguration default methods
+
+    @Override
+    @Nullable
+    public Handler getCallbackHandler(@Nullable Handler valueIfMissing) {
+        return retrieveOption(OPTION_CALLBACK_HANDLER, valueIfMissing);
+    }
+
+    @Override
+    public Handler getCallbackHandler() {
+        return retrieveOption(OPTION_CALLBACK_HANDLER);
+    }
+
+    // Implementations of UseCaseConfiguration default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration valueIfMissing) {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration getDefaultSessionConfiguration() {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+        return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+        return retrieveOption(OPTION_CONFIG_UNPACKER);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority(int valueIfMissing) {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority() {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+
+}
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..f973894
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCase.java
@@ -0,0 +1,465 @@
+/*
+ * 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.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 mMainHandler = new Handler(Looper.getMainLooper());
+    private final CheckedSurfaceTexture.OnTextureChangedListener mSurfaceTextureListener =
+            new CheckedSurfaceTexture.OnTextureChangedListener() {
+                @Override
+                public void onTextureChanged(SurfaceTexture newSurfaceTexture, Size newResolution) {
+                    ViewFinderUseCase.this.updateOutput(newSurfaceTexture, newResolution);
+                }
+            };
+    final CheckedSurfaceTexture mCheckedSurfaceTexture =
+            new CheckedSurfaceTexture(mSurfaceTextureListener, mMainHandler);
+    private final ViewFinderUseCaseConfiguration.Builder mUseCaseConfigBuilder;
+    @Nullable
+    private OnViewFinderOutputUpdateListener mSubscribedViewFinderOutputListener;
+    @Nullable
+    private ViewFinderOutput mLatestViewFinderOutput;
+    private boolean mSurfaceDispatched = 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);
+        mUseCaseConfigBuilder = 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 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 mSubscribedViewFinderOutputListener;
+    }
+
+    /**
+     * 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 newListener The listener which will receive {@link ViewFinderOutput} updates.
+     */
+    @UiThread
+    public void setOnViewFinderOutputUpdateListener(
+            @Nullable OnViewFinderOutputUpdateListener newListener) {
+        OnViewFinderOutputUpdateListener oldListener = mSubscribedViewFinderOutputListener;
+        mSubscribedViewFinderOutputListener = newListener;
+        if (oldListener == null && newListener != null) {
+            notifyActive();
+            if (mLatestViewFinderOutput != null) {
+                mSurfaceDispatched = true;
+                newListener.onUpdated(mLatestViewFinderOutput);
+            }
+        } else if (oldListener != null && newListener == null) {
+            notifyInactive();
+        } else if (oldListener != null && oldListener != newListener) {
+            if (mLatestViewFinderOutput != null) {
+                mCheckedSurfaceTexture.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, mMainHandler);
+    }
+
+    /**
+     * 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) {
+            mUseCaseConfigBuilder.setTargetRotation(rotation);
+            updateUseCaseConfiguration(mUseCaseConfigBuilder.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
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
+        ViewFinderUseCaseConfiguration defaults = CameraX.getDefaultUseCaseConfiguration(
+                ViewFinderUseCaseConfiguration.class, lensFacing);
+        if (defaults != null) {
+            return ViewFinderUseCaseConfiguration.Builder.fromConfig(defaults);
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void clear() {
+        mCheckedSurfaceTexture.setOnSurfaceDetachedListener(
+                MainThreadExecutor.getInstance(),
+                new DeferrableSurface.OnSurfaceDetachedListener() {
+                    @Override
+                    public void onSurfaceDetached() {
+                        mCheckedSurfaceTexture.release();
+                    }
+                });
+        removeViewFinderOutputListener();
+        notifyInactive();
+
+        SurfaceTexture oldTexture =
+                (mLatestViewFinderOutput == null)
+                        ? null
+                        : mLatestViewFinderOutput.getSurfaceTexture();
+        if (oldTexture != null && !mSurfaceDispatched) {
+            oldTexture.release();
+        }
+
+        super.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @hide
+     */
+    @Override
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    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);
+        }
+
+        mCheckedSurfaceTexture.setResolution(resolution);
+        mCheckedSurfaceTexture.resetSurfaceTexture();
+
+        SessionConfiguration.Builder sessionConfigBuilder =
+                createFrom(configuration, mCheckedSurfaceTexture);
+        attachToCamera(cameraId, sessionConfigBuilder.build());
+
+        return suggestedResolutionMap;
+    }
+
+    @UiThread
+    private void invalidateMetadata() {
+        if (mLatestViewFinderOutput != null) {
+            // Only update the output if we have a SurfaceTexture. Otherwise we'll wait until a
+            // SurfaceTexture is ready.
+            updateOutput(
+                    mLatestViewFinderOutput.getSurfaceTexture(),
+                    mLatestViewFinderOutput.getTextureSize());
+        }
+    }
+
+    @UiThread
+    void updateOutput(SurfaceTexture surfaceTexture, Size resolution) {
+        ViewFinderUseCaseConfiguration useCaseConfig =
+                (ViewFinderUseCaseConfiguration) getUseCaseConfiguration();
+
+        int relativeRotation =
+                (mLatestViewFinderOutput == null) ? 0
+                        : mLatestViewFinderOutput.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(mLatestViewFinderOutput, newOutput)) {
+            SurfaceTexture oldTexture =
+                    (mLatestViewFinderOutput == null)
+                            ? null
+                            : mLatestViewFinderOutput.getSurfaceTexture();
+            OnViewFinderOutputUpdateListener outputListener = getOnViewFinderOutputUpdateListener();
+
+            mLatestViewFinderOutput = newOutput;
+
+            boolean textureChanged = oldTexture != surfaceTexture;
+            if (textureChanged) {
+                // If the old surface was never dispatched, we can safely release the old
+                // SurfaceTexture.
+                if (oldTexture != null && !mSurfaceDispatched) {
+                    oldTexture.release();
+                }
+
+                // Keep track of whether this SurfaceTexture is dispatched
+                mSurfaceDispatched = 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();
+                }
+
+                mSurfaceDispatched = 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 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)
+                            .setMaxResolution(DEFAULT_MAX_RESOLUTION)
+                            .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+            DEFAULT_CONFIG = builder.build();
+        }
+
+        @Override
+        public ViewFinderUseCaseConfiguration getConfiguration(LensFacing lensFacing) {
+            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..a42065a
--- /dev/null
+++ b/camera/core/src/main/java/androidx/camera/core/ViewFinderUseCaseConfiguration.java
@@ -0,0 +1,468 @@
+/*
+ * 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.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+
+import java.util.Set;
+import java.util.UUID;
+
+/** Configuration for an image capture use case. */
+public final class ViewFinderUseCaseConfiguration
+        implements UseCaseConfiguration<ViewFinderUseCase>,
+        ImageOutputConfiguration,
+        CameraDeviceConfiguration,
+        ThreadConfiguration {
+
+    private final OptionsBundle mConfig;
+
+    /** Creates a new configuration instance. */
+    ViewFinderUseCaseConfiguration(OptionsBundle config) {
+        mConfig = 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 mConfig;
+    }
+
+    /** 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 mMutableConfig;
+
+        /** Creates a new Builder object. */
+        public Builder() {
+            this(MutableOptionsBundle.create());
+        }
+
+        private Builder(MutableOptionsBundle mutableConfig) {
+            mMutableConfig = 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 mMutableConfig;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public ViewFinderUseCaseConfiguration build() {
+            return new ViewFinderUseCaseConfiguration(OptionsBundle.from(mMutableConfig));
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // Implementations of TargetConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setTargetClass(Class<ViewFinderUseCase> 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();
+        }
+
+        @Override
+        public Builder setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+
+        // Implementations of CameraDeviceConfiguration.Builder default methods
+
+        @Override
+        public Builder setLensFacing(CameraX.LensFacing lensFacing) {
+            getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+            return builder();
+        }
+
+        // Implementations of ImageOutputConfiguration.Builder default methods
+
+        @Override
+        public Builder setTargetAspectRatio(Rational aspectRatio) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ASPECT_RATIO, aspectRatio);
+            return builder();
+        }
+
+        @Override
+        public Builder setTargetRotation(@RotationValue int rotation) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_ROTATION, rotation);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setMaxResolution(Size resolution) {
+            getMutableConfiguration().insertOption(OPTION_MAX_RESOLUTION, resolution);
+            return builder();
+        }
+
+        // Implementations of ThreadConfiguration.Builder default methods
+
+        @Override
+        public Builder setCallbackHandler(Handler handler) {
+            getMutableConfiguration().insertOption(OPTION_CALLBACK_HANDLER, handler);
+            return builder();
+        }
+
+        // Implementations of UseCaseConfiguration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+            getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+            getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // Implementations of TargetConfiguration default methods
+
+    @Override
+    @Nullable
+    public Class<ViewFinderUseCase> getTargetClass(
+            @Nullable Class<ViewFinderUseCase> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<ViewFinderUseCase> storedClass = (Class<ViewFinderUseCase>) retrieveOption(
+                OPTION_TARGET_CLASS,
+                valueIfMissing);
+        return storedClass;
+    }
+
+    @Override
+    public Class<ViewFinderUseCase> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<ViewFinderUseCase> storedClass = (Class<ViewFinderUseCase>) retrieveOption(
+                OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    @Override
+    @Nullable
+    public String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    @Override
+    public String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    // Implementations of CameraDeviceConfiguration default methods
+
+    @Override
+    @Nullable
+    public CameraX.LensFacing getLensFacing(@Nullable CameraX.LensFacing valueIfMissing) {
+        return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+    }
+
+    @Override
+    public CameraX.LensFacing getLensFacing() {
+        return retrieveOption(OPTION_LENS_FACING);
+    }
+
+    // Implementations of ImageOutputConfiguration default methods
+
+    @Override
+    @Nullable
+    public Rational getTargetAspectRatio(@Nullable Rational valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO, valueIfMissing);
+    }
+
+    @Override
+    public Rational getTargetAspectRatio() {
+        return retrieveOption(OPTION_TARGET_ASPECT_RATIO);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation(int valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_ROTATION, valueIfMissing);
+    }
+
+    @Override
+    @RotationValue
+    public int getTargetRotation() {
+        return retrieveOption(OPTION_TARGET_ROTATION);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution(Size valueIfMissing) {
+        return retrieveOption(OPTION_MAX_RESOLUTION, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Size getMaxResolution() {
+        return retrieveOption(OPTION_MAX_RESOLUTION);
+    }
+
+    // Implementations of ThreadConfiguration default methods
+
+    @Override
+    @Nullable
+    public Handler getCallbackHandler(@Nullable Handler valueIfMissing) {
+        return retrieveOption(OPTION_CALLBACK_HANDLER, valueIfMissing);
+    }
+
+    @Override
+    public Handler getCallbackHandler() {
+        return retrieveOption(OPTION_CALLBACK_HANDLER);
+    }
+
+    // Implementations of UseCaseConfiguration default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration valueIfMissing) {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration getDefaultSessionConfiguration() {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+        return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+        return retrieveOption(OPTION_CONFIG_UNPACKER);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority(int valueIfMissing) {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public int getSurfaceOccupancyPriority() {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/AppConfigurationTest.java b/camera/core/src/test/java/androidx/camera/core/AppConfigurationTest.java
new file mode 100644
index 0000000..b838bdf
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/AppConfigurationTest.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 static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import androidx.camera.testing.fakes.FakeAppConfiguration;
+import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager;
+import androidx.camera.testing.fakes.FakeCameraFactory;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class AppConfigurationTest {
+
+    private AppConfiguration mAppConfiguration;
+
+    @Before
+    public void setUp() {
+        mAppConfiguration = FakeAppConfiguration.create();
+    }
+
+    @Test
+    public void canGetConfigTarget() {
+        Class<CameraX> configTarget = mAppConfiguration.getTargetClass(/*valueIfMissing=*/ null);
+        assertThat(configTarget).isEqualTo(CameraX.class);
+    }
+
+    @Test
+    public void canGetCameraFactory() {
+        CameraFactory cameraFactory = mAppConfiguration.getCameraFactory(/*valueIfMissing=*/ null);
+        assertThat(cameraFactory).isInstanceOf(FakeCameraFactory.class);
+    }
+
+    @Test
+    public void canGetDeviceSurfaceManager() {
+        CameraDeviceSurfaceManager surfaceManager =
+                mAppConfiguration.getDeviceSurfaceManager(/*valueIfMissing=*/ null);
+        assertThat(surfaceManager).isInstanceOf(FakeCameraDeviceSurfaceManager.class);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/CameraCaptureResultImageInfoTest.java b/camera/core/src/test/java/androidx/camera/core/CameraCaptureResultImageInfoTest.java
new file mode 100644
index 0000000..b94d80e
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/CameraCaptureResultImageInfoTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import androidx.camera.testing.fakes.FakeCameraCaptureResult;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class CameraCaptureResultImageInfoTest {
+    @Test
+    public void creationSuccess() {
+        long timestamp = 10L;
+        FakeCameraCaptureResult cameraCaptureResult = new FakeCameraCaptureResult();
+        cameraCaptureResult.setTimestamp(timestamp);
+        ImageInfo imageInfo = new CameraCaptureResultImageInfo(cameraCaptureResult);
+
+        assertThat(imageInfo.getTimestamp()).isEqualTo(timestamp);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/CaptureBundleTest.java b/camera/core/src/test/java/androidx/camera/core/CaptureBundleTest.java
new file mode 100644
index 0000000..810572c
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/CaptureBundleTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import androidx.camera.testing.fakes.FakeCaptureStage;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class CaptureBundleTest {
+    @Test
+    public void bundleRetainsOrder() {
+        List<Integer> captureOrder = new ArrayList<>();
+        captureOrder.add(0);
+        captureOrder.add(2);
+        captureOrder.add(1);
+
+        CaptureBundle captureBundle = new CaptureBundle();
+        for (Integer captureId : captureOrder) {
+            captureBundle.addCaptureStage(new FakeCaptureStage(captureId, null));
+        }
+
+        List<CaptureStage> captureStages = captureBundle.getCaptureStages();
+
+        assertThat(captureOrder.size()).isEqualTo(captureStages.size());
+
+        Iterator<Integer> captureOrderIterator = captureOrder.iterator();
+        Iterator<CaptureStage> captureStageIterator = captureStages.iterator();
+
+        while (captureOrderIterator.hasNext() && captureStageIterator.hasNext()) {
+            assertThat(captureOrderIterator.next()).isEqualTo(captureStageIterator.next().getId());
+        }
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/CaptureBundlesTest.java b/camera/core/src/test/java/androidx/camera/core/CaptureBundlesTest.java
new file mode 100644
index 0000000..b6bcc13
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/CaptureBundlesTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class CaptureBundlesTest {
+    @Test
+    public void singleDefaultBundleSizeCheck() {
+        CaptureBundle singleBundle = CaptureBundles.singleDefaultCaptureBundle();
+
+        assertThat(singleBundle.getCaptureStages().size()).isEqualTo(1);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/CaptureStagesTest.java b/camera/core/src/test/java/androidx/camera/core/CaptureStagesTest.java
new file mode 100644
index 0000000..f87f0c5
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/CaptureStagesTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class CaptureStagesTest {
+    @Test
+    public void defaultCaptureStageHasNoOptions() {
+        CaptureStage captureStage = new CaptureStage.DefaultCaptureStage();
+        Configuration options =
+                captureStage.getCaptureRequestConfiguration().getImplementationOptions();
+
+        assertThat(options.listOptions().size()).isEqualTo(0);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureTest.java b/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureTest.java
new file mode 100644
index 0000000..70f716d
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/CheckedSurfaceTextureTest.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 android.graphics.SurfaceTexture;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Size;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.CheckedSurfaceTexture.OnTextureChangedListener;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class CheckedSurfaceTextureTest {
+
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+    private Size mDefaultResolution;
+    private CheckedSurfaceTexture mCheckedSurfaceTexture;
+    private SurfaceTexture mLatestSurfaceTexture;
+    private final CheckedSurfaceTexture.OnTextureChangedListener mTextureChangedListener =
+            new OnTextureChangedListener() {
+                @Override
+                public void onTextureChanged(
+                        @Nullable SurfaceTexture newOutput, @Nullable Size newResolution) {
+                    mLatestSurfaceTexture = newOutput;
+                }
+            };
+
+    @Before
+    public void setup() {
+        mDefaultResolution = new Size(640, 480);
+        mCheckedSurfaceTexture =
+                new CheckedSurfaceTexture(mTextureChangedListener, mMainThreadHandler);
+        mCheckedSurfaceTexture.setResolution(mDefaultResolution);
+    }
+
+    @Test
+    public void viewFinderOutputUpdatesWhenReset() {
+        // Create the initial surface texture
+        mCheckedSurfaceTexture.resetSurfaceTexture();
+
+        // Surface texture should have been set
+        SurfaceTexture initialOutput = mLatestSurfaceTexture;
+
+        // Create a new surface texture
+        mCheckedSurfaceTexture.resetSurfaceTexture();
+
+        assertThat(initialOutput).isNotNull();
+        assertThat(mLatestSurfaceTexture).isNotNull();
+        assertThat(mLatestSurfaceTexture).isNotEqualTo(initialOutput);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/DeferrableSurfaceTest.java b/camera/core/src/test/java/androidx/camera/core/DeferrableSurfaceTest.java
new file mode 100644
index 0000000..5b4b4ee
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/DeferrableSurfaceTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+
+import android.os.Build;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.concurrent.Executor;
+
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class DeferrableSurfaceTest {
+    DeferrableSurface mDeferrableSurface;
+
+    @Before
+    public void setup() {
+        mDeferrableSurface = new DeferrableSurface() {
+            @Nullable
+            @Override
+            public ListenableFuture<Surface> getSurface() {
+                return null;
+            }
+        };
+    }
+
+    @Test
+    public void attachCountIsCorrect() {
+
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+
+        assertThat(mDeferrableSurface.getAttachedCount()).isEqualTo(0);
+
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+
+        assertThat(mDeferrableSurface.getAttachedCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void onSurfaceDetachListenerIsCalledWhenDetachedLater() {
+        DeferrableSurface.OnSurfaceDetachedListener listener =
+                Mockito.mock(DeferrableSurface.OnSurfaceDetachedListener.class);
+
+        Executor executor = new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        };
+
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.setOnSurfaceDetachedListener(executor, listener);
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+        mDeferrableSurface.notifySurfaceDetached();
+
+        Mockito.verify(listener, times(1)).onSurfaceDetached();
+    }
+
+    @Test
+    public void onSurfaceDetachListenerIsCalledWhenDetachedAlready() {
+        DeferrableSurface.OnSurfaceDetachedListener listener =
+                Mockito.mock(DeferrableSurface.OnSurfaceDetachedListener.class);
+
+        Executor executor = new Executor() {
+            @Override
+            public void execute(Runnable command) {
+                command.run();
+            }
+        };
+
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+
+        mDeferrableSurface.setOnSurfaceDetachedListener(executor, listener);
+
+        Mockito.verify(listener, times(1)).onSurfaceDetached();
+    }
+
+    @Test
+    public void onSurfaceDetachListenerRunInCorrectExecutor() {
+        Executor executor = Mockito.mock(Executor.class);
+        DeferrableSurface.OnSurfaceDetachedListener listener =
+                Mockito.mock(DeferrableSurface.OnSurfaceDetachedListener.class);
+
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.setOnSurfaceDetachedListener(executor, listener);
+        mDeferrableSurface.notifySurfaceDetached();
+
+        Mockito.verify(executor, times(1)).execute(any(Runnable.class));
+
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void detachInWrongState_throwException() {
+        mDeferrableSurface.notifySurfaceAttached();
+        mDeferrableSurface.notifySurfaceDetached();
+
+        mDeferrableSurface.notifySurfaceDetached();
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/ExifTest.java b/camera/core/src/test/java/androidx/camera/core/ExifTest.java
new file mode 100644
index 0000000..b2b01bc
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/ExifTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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 android.os.Build;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowSystemClock;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class ExifTest {
+    private static final InputStream FAKE_INPUT_STREAM =
+            new InputStream() {
+                @Override
+                public int read() {
+                    return 0;
+                }
+            };
+    private Exif mExif;
+
+    @Before
+    public void setup() throws Exception {
+        ShadowLog.stream = System.out;
+        mExif = Exif.createFromInputStream(FAKE_INPUT_STREAM);
+    }
+
+    @Test
+    public void defaultsAreExpectedValues() {
+        assertThat(mExif.getRotation()).isEqualTo(0);
+        assertThat(mExif.isFlippedHorizontally()).isFalse();
+        assertThat(mExif.isFlippedVertically()).isFalse();
+        assertThat(mExif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+        assertThat(mExif.getLocation()).isNull();
+        assertThat(mExif.getDescription()).isNull();
+    }
+
+    @Test
+    public void rotateProducesCorrectRotation() {
+        assertThat(mExif.getRotation()).isEqualTo(0);
+        mExif.rotate(90);
+        assertThat(mExif.getRotation()).isEqualTo(90);
+        mExif.rotate(90);
+        assertThat(mExif.getRotation()).isEqualTo(180);
+        mExif.rotate(90);
+        assertThat(mExif.getRotation()).isEqualTo(270);
+        mExif.rotate(90);
+        assertThat(mExif.getRotation()).isEqualTo(0);
+        mExif.rotate(-90);
+        assertThat(mExif.getRotation()).isEqualTo(270);
+        mExif.rotate(360);
+        assertThat(mExif.getRotation()).isEqualTo(270);
+        mExif.rotate(500 * 360 - 90);
+        assertThat(mExif.getRotation()).isEqualTo(180);
+    }
+
+    @Test
+    public void flipHorizontallyWillToggle() {
+        assertThat(mExif.isFlippedHorizontally()).isFalse();
+        mExif.flipHorizontally();
+        assertThat(mExif.isFlippedHorizontally()).isTrue();
+        mExif.flipHorizontally();
+        assertThat(mExif.isFlippedHorizontally()).isFalse();
+    }
+
+    @Test
+    public void flipVerticallyWillToggle() {
+        assertThat(mExif.isFlippedVertically()).isFalse();
+        mExif.flipVertically();
+        assertThat(mExif.isFlippedVertically()).isTrue();
+        mExif.flipVertically();
+        assertThat(mExif.isFlippedVertically()).isFalse();
+    }
+
+    @Test
+    public void flipAndRotateUpdatesHorizontalAndVerticalFlippedState() {
+        assertThat(mExif.getRotation()).isEqualTo(0);
+        assertThat(mExif.isFlippedHorizontally()).isFalse();
+        assertThat(mExif.isFlippedVertically()).isFalse();
+
+        mExif.rotate(-90);
+        assertThat(mExif.getRotation()).isEqualTo(270);
+
+        mExif.flipHorizontally();
+        assertThat(mExif.getRotation()).isEqualTo(90);
+        assertThat(mExif.isFlippedVertically()).isTrue();
+
+        mExif.flipVertically();
+        assertThat(mExif.getRotation()).isEqualTo(90);
+        assertThat(mExif.isFlippedHorizontally()).isFalse();
+        assertThat(mExif.isFlippedVertically()).isFalse();
+
+        mExif.rotate(90);
+        assertThat(mExif.getRotation()).isEqualTo(180);
+
+        mExif.flipVertically();
+        assertThat(mExif.getRotation()).isEqualTo(0);
+        assertThat(mExif.isFlippedHorizontally()).isTrue();
+        assertThat(mExif.isFlippedVertically()).isFalse();
+
+        mExif.flipHorizontally();
+        assertThat(mExif.getRotation()).isEqualTo(0);
+        assertThat(mExif.isFlippedHorizontally()).isFalse();
+        assertThat(mExif.isFlippedVertically()).isFalse();
+    }
+
+    @Test
+    public void timestampCanBeAttachedAndRemoved() {
+        assertThat(mExif.getTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+        mExif.attachTimestamp();
+        assertThat(mExif.getTimestamp()).isNotEqualTo(Exif.INVALID_TIMESTAMP);
+
+        mExif.removeTimestamp();
+        assertThat(mExif.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);
+        mExif.attachTimestamp();
+        long afterTimestamp = System.currentTimeMillis();
+
+        // Check that the attached timestamp is in the closed range [beforeTimestamp,
+        // afterTimestamp].
+        long attachedTimestamp = mExif.getTimestamp();
+        assertThat(attachedTimestamp).isAtLeast(beforeTimestamp);
+        assertThat(attachedTimestamp).isAtMost(afterTimestamp);
+    }
+
+    @Test
+    public void locationCanBeAttachedAndRemoved() {
+        assertThat(mExif.getLocation()).isNull();
+
+        Location location = new Location("TEST");
+        location.setLatitude(22.3);
+        location.setLongitude(114);
+        location.setTime(System.currentTimeMillis() / 1000 * 1000);
+        mExif.attachLocation(location);
+        assertThat(location.toString()).isEqualTo(mExif.getLocation().toString());
+
+        mExif.removeLocation();
+        assertThat(mExif.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);
+        mExif.attachLocation(location);
+        assertThat(location.toString()).isEqualTo(mExif.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);
+        mExif.attachLocation(location);
+        // Location loses precision when set through attachLocation(), so check the locations are
+        // roughly equal first
+        Location exifLocation = mExif.getLocation();
+        assertThat(location.getSpeed()).isWithin(0.01f).of(exifLocation.getSpeed());
+    }
+
+    @Test
+    public void descriptionCanBeAttachedAndRemoved() {
+        assertThat(mExif.getDescription()).isNull();
+
+        mExif.setDescription("Hello World");
+        assertThat(mExif.getDescription()).isEqualTo("Hello World");
+
+        mExif.setDescription(null);
+        assertThat(mExif.getDescription()).isNull();
+    }
+
+    @Test
+    public void saveUpdatesLastModifiedTimestampUnlessRemoved() {
+        assertThat(mExif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+        try {
+            mExif.save();
+        } catch (IOException e) {
+            // expected
+        }
+
+        assertThat(mExif.getLastModifiedTimestamp()).isNotEqualTo(Exif.INVALID_TIMESTAMP);
+
+        // removeTimestamp should also be clearing the last modified timestamp
+        mExif.removeTimestamp();
+        assertThat(mExif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+
+        // Even when saving again
+        try {
+            mExif.save();
+        } catch (IOException e) {
+            // expected
+        }
+
+        assertThat(mExif.getLastModifiedTimestamp()).isEqualTo(Exif.INVALID_TIMESTAMP);
+    }
+
+    @Test
+    public void toStringProducesNonNullString() {
+        assertThat(mExif.toString()).isNotNull();
+        mExif.setDescription("Hello World");
+        mExif.attachTimestamp();
+        Location location = new Location("TEST");
+        location.setLatitude(22.3);
+        location.setLongitude(114);
+        location.setTime(System.currentTimeMillis() / 1000 * 1000);
+        location.setAltitude(5.0);
+        mExif.attachLocation(location);
+        assertThat(mExif.toString()).isNotNull();
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryTest.java b/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryTest.java
new file mode 100644
index 0000000..8e4e2da
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/ExtendableUseCaseConfigFactoryTest.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 static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import androidx.camera.testing.fakes.FakeUseCase;
+import androidx.camera.testing.fakes.FakeUseCaseConfiguration;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class ExtendableUseCaseConfigFactoryTest {
+
+    private ExtendableUseCaseConfigFactory mFactory;
+
+    @Before
+    public void setUp() {
+        mFactory = new ExtendableUseCaseConfigFactory();
+    }
+
+    @Test
+    public void canInstallProvider_andRetrieveConfig() {
+        mFactory.installDefaultProvider(
+                FakeUseCaseConfiguration.class, new FakeUseCaseConfigurationProvider());
+
+        FakeUseCaseConfiguration config = mFactory.getConfiguration(FakeUseCaseConfiguration.class,
+                null);
+        assertThat(config).isNotNull();
+        assertThat(config.getTargetClass(null)).isEqualTo(FakeUseCase.class);
+    }
+
+    private static class FakeUseCaseConfigurationProvider
+            implements ConfigurationProvider<FakeUseCaseConfiguration> {
+
+        @Override
+        public FakeUseCaseConfiguration getConfiguration(CameraX.LensFacing lensFacing) {
+            return new FakeUseCaseConfiguration.Builder().build();
+        }
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleTest.java b/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleTest.java
new file mode 100644
index 0000000..a0cf05c
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/MutableOptionsBundleTest.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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import androidx.camera.core.Configuration.Option;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class MutableOptionsBundleTest {
+
+    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/OptionTest.java b/camera/core/src/test/java/androidx/camera/core/OptionTest.java
new file mode 100644
index 0000000..d16c8c0
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/OptionTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.Build;
+
+import androidx.camera.core.Configuration.Option;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class OptionTest {
+
+    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/OptionsBundleTest.java b/camera/core/src/test/java/androidx/camera/core/OptionsBundleTest.java
new file mode 100644
index 0000000..27d330e
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/OptionsBundleTest.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.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import androidx.camera.core.Configuration.Option;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class OptionsBundleTest {
+
+    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 mAllOpts;
+
+    @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);
+
+        mAllOpts = OptionsBundle.from(mutOpts);
+    }
+
+    @Test
+    public void canRetrieveValue() {
+        assertThat(mAllOpts.retrieveOption(OPTION_1)).isSameAs(VALUE_1);
+        assertThat(mAllOpts.retrieveOption(OPTION_1_A)).isSameAs(VALUE_1_A);
+        assertThat(mAllOpts.retrieveOption(OPTION_2)).isSameAs(VALUE_2);
+    }
+
+    @Test
+    public void willReturnDefault_ifOptionIsMissing() {
+        Object value = mAllOpts.retrieveOption(OPTION_MISSING, VALUE_MISSING);
+        assertThat(value).isSameAs(VALUE_MISSING);
+    }
+
+    @Test
+    public void willReturnStoredValue_whenGivenDefault() {
+        Object value = mAllOpts.retrieveOption(OPTION_1, VALUE_MISSING);
+        assertThat(value).isSameAs(VALUE_1);
+    }
+
+    @Test
+    public void canListOptions() {
+        Set<Option<?>> list = mAllOpts.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(mAllOpts);
+
+        assertThat(copyBundle.containsOption(OPTION_1)).isTrue();
+        assertThat(copyBundle.containsOption(OPTION_1_A)).isTrue();
+        assertThat(copyBundle.containsOption(OPTION_2)).isTrue();
+    }
+
+    @Test
+    public void canFindPartialIds() {
+        mAllOpts.findOptions(
+                "option.1",
+                new Configuration.OptionMatcher() {
+                    @Override
+                    public boolean onOptionMatched(Option<?> option) {
+                        assertThat(option).isAnyOf(OPTION_1, OPTION_1_A);
+                        return true;
+                    }
+                });
+    }
+
+    @Test
+    public void canStopSearchingAfterFirstMatch() {
+        final AtomicInteger count = new AtomicInteger();
+        mAllOpts.findOptions(
+                "option",
+                new Configuration.OptionMatcher() {
+                    @Override
+                    public boolean onOptionMatched(Option<?> option) {
+                        count.getAndIncrement();
+                        return false;
+                    }
+                });
+
+        assertThat(count.get()).isEqualTo(1);
+    }
+
+    @Test
+    public void canGetZeroResults_fromFind() {
+        final AtomicInteger count = new AtomicInteger();
+        mAllOpts.findOptions(
+                "invalid_find_string",
+                new Configuration.OptionMatcher() {
+                    @Override
+                    public boolean onOptionMatched(Option<?> option) {
+                        count.getAndIncrement();
+                        return false;
+                    }
+                });
+
+        assertThat(count.get()).isEqualTo(0);
+    }
+
+    @Test
+    public void canRetrieveValue_fromFindLambda() {
+        final AtomicReference<Object> value = new AtomicReference<>(VALUE_MISSING);
+        mAllOpts.findOptions(
+                "option.2",
+                new Configuration.OptionMatcher() {
+                    @Override
+                    public boolean onOptionMatched(Option<?> option) {
+                        value.set(mAllOpts.retrieveOption(option));
+                        return true;
+                    }
+                });
+
+        assertThat(value.get()).isSameAs(VALUE_2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void retrieveMissingOption_willThrow() {
+        // Should throw IllegalArgumentException
+        mAllOpts.retrieveOption(OPTION_MISSING);
+    }
+}
diff --git a/camera/core/src/test/java/androidx/camera/core/SettableImageProxyBundleTest.java b/camera/core/src/test/java/androidx/camera/core/SettableImageProxyBundleTest.java
new file mode 100644
index 0000000..e50dea3
--- /dev/null
+++ b/camera/core/src/test/java/androidx/camera/core/SettableImageProxyBundleTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+
+import androidx.camera.testing.fakes.FakeImageInfo;
+import androidx.camera.testing.fakes.FakeImageProxy;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@SmallTest
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class SettableImageProxyBundleTest {
+    private static final int CAPTURE_ID_0 = 0;
+    private static final int CAPTURE_ID_1 = 1;
+    private static final int CAPTURE_ID_NONEXISTANT = 5;
+    private static final long TIMESTAMP_0 = 10L;
+    private static final long TIMESTAMP_1 = 20L;
+    private final ImageInfo mImageInfo0 = new FakeImageInfo();
+    private final ImageInfo mImageInfo1 = new FakeImageInfo();
+    private final ImageProxy mImageProxy0 = new FakeImageProxy();
+    private final ImageProxy mImageProxy1 = new FakeImageProxy();
+    private List<Integer> mCaptureIdList;
+    private SettableImageProxyBundle mImageProxyBundle;
+
+    @Before
+    public void setup() {
+        ((FakeImageInfo) mImageInfo0).setTimestamp(TIMESTAMP_0);
+        ((FakeImageInfo) mImageInfo1).setTimestamp(TIMESTAMP_1);
+        ((FakeImageInfo) mImageInfo0).setTag(CAPTURE_ID_0);
+        ((FakeImageInfo) mImageInfo1).setTag(CAPTURE_ID_1);
+        mImageProxy0.setTimestamp(TIMESTAMP_0);
+        mImageProxy1.setTimestamp(TIMESTAMP_1);
+        ((FakeImageProxy) mImageProxy0).setImageInfo(mImageInfo0);
+        ((FakeImageProxy) mImageProxy1).setImageInfo(mImageInfo1);
+
+        mCaptureIdList = new ArrayList<>();
+        mCaptureIdList.add(CAPTURE_ID_0);
+        mCaptureIdList.add(CAPTURE_ID_1);
+
+        mImageProxyBundle = new SettableImageProxyBundle(mCaptureIdList);
+    }
+
+    @Test
+    public void canInvokeMatchedImageProxyFuture() throws InterruptedException,
+            ExecutionException, TimeoutException {
+
+        // Inputs two ImageProxy to SettableImageProxyBundle.
+        mImageProxyBundle.addImageProxy(mImageProxy0);
+        mImageProxyBundle.addImageProxy(mImageProxy1);
+
+        // Tries to get the Images for the ListenableFutures got from SettableImageProxyBundle.
+        ImageProxy result0 = mImageProxyBundle.getImageProxy(CAPTURE_ID_0).get(0, TimeUnit.SECONDS);
+        ImageProxy result1 = mImageProxyBundle.getImageProxy(CAPTURE_ID_1).get(0, TimeUnit.SECONDS);
+
+        // Checks if the results match what was input.
+        assertThat(result0.getImageInfo()).isSameAs(mImageInfo0);
+        assertThat(result0.getTimestamp()).isSameAs(mImageProxy0.getTimestamp());
+        assertThat(result1.getImageInfo()).isSameAs(mImageInfo1);
+        assertThat(result1.getTimestamp()).isSameAs(mImageProxy1.getTimestamp());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void exceptionWhenAddingImageWithInvalidCaptureId() {
+        ImageInfo imageInfo = new FakeImageInfo();
+        ImageProxy imageProxy = new FakeImageProxy();
+
+        // Adds an ImageProxy with a capture id which doesn't exist in the initial list.
+        ((FakeImageInfo) imageInfo).setTag(CAPTURE_ID_NONEXISTANT);
+        ((FakeImageProxy) imageProxy).setImageInfo(imageInfo);
+
+        // Expects to throw exception while adding ImageProxy.
+        mImageProxyBundle.addImageProxy(imageProxy);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void exceptionWhenRetrievingImageWithInvalidCaptureId() throws InterruptedException,
+            ExecutionException, TimeoutException {
+        // Tries to get a ImageProxy with non-existed capture id. Expects to throw exception
+        // while getting ImageProxy.
+        mImageProxyBundle.getImageProxy(CAPTURE_ID_NONEXISTANT).get(0, TimeUnit.SECONDS);
+    }
+}
diff --git a/camera/extensions/build.gradle b/camera/extensions/build.gradle
new file mode 100644
index 0000000..1cd7f39
--- /dev/null
+++ b/camera/extensions/build.gradle
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+// TODO(b/124783972): Switch to androidx.build.LibraryVersions and androidx.build.LibraryGroups when ready
+import androidx.build.UnpublishedLibraryVersions
+import androidx.build.UnpublishedLibraryGroups
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    api(project(":camera:camera-core"))
+    api(project(":camera:camera-camera2"))
+    implementation("androidx.core:core:1.0.0")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+}
+
+supportLibrary {
+    name = "Jetpack Camera Library OEM Extensions"
+    publish = false
+    mavenVersion = UnpublishedLibraryVersions.CAMERA
+    mavenGroup = UnpublishedLibraryGroups.CAMERA
+    inceptionYear = "2019"
+    description = "OEM Extensions for the Jetpack Camera Library, a library providing interfaces" +
+            " to integrate with OEM specific camera features."
+}
diff --git a/camera/extensions/src/main/AndroidManifest.xml b/camera/extensions/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..bfcb9ff
--- /dev/null
+++ b/camera/extensions/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.extensions">
+
+    <application>
+        <uses-library
+            android:name="androidx.camera.extensions.impl"
+            android:required="false" />
+    </application>
+</manifest>
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/BokehImageCaptureExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/BokehImageCaptureExtender.java
new file mode 100644
index 0000000..cb2c812
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/BokehImageCaptureExtender.java
@@ -0,0 +1,29 @@
+/*
+ * 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.extensions;
+
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+
+/**
+ * Load the OEM extension implementation for bokeh effect type.
+ */
+public class BokehImageCaptureExtender extends ImageCaptureUseCaseExtender {
+    public BokehImageCaptureExtender(ImageCaptureUseCaseConfiguration.Builder builder) {
+        super(builder);
+        loadImplementation("androidx.camera.extensions.impl.BokehImageCaptureExtender");
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/BokehViewFinderExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/BokehViewFinderExtender.java
new file mode 100644
index 0000000..d93a189
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/BokehViewFinderExtender.java
@@ -0,0 +1,29 @@
+/*
+ * 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.extensions;
+
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/**
+ * Load the OEM extension ViewFinder implementation for bokeh effect type.
+ */
+public class BokehViewFinderExtender extends ViewFinderUseCaseExtender {
+    public BokehViewFinderExtender(ViewFinderUseCaseConfiguration.Builder builder) {
+        super(builder);
+        loadImplementation("androidx.camera.extensions.impl.BokehViewFinderExtender");
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/CaptureStage.java b/camera/extensions/src/main/java/androidx/camera/extensions/CaptureStage.java
new file mode 100644
index 0000000..9954336
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/CaptureStage.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.camera2.Camera2Configuration;
+import androidx.camera.core.CaptureRequestConfiguration;
+
+/**
+ * The set of parameters that defines a single capture that will be sent to the camera.
+ */
+public final class CaptureStage implements androidx.camera.core.CaptureStage {
+    private final int mId;
+    private final CaptureRequestConfiguration.Builder mCaptureRequestConfigurationBuilder =
+            new CaptureRequestConfiguration.Builder();
+    private final Camera2Configuration.Builder mCamera2ConfigurationBuilder =
+            new Camera2Configuration.Builder();
+
+    /**
+     * Constructor for a {@link CaptureStage} with specific identifier.
+     *
+     * <p>After this {@link CaptureStage} is applied to a single capture operation, developers can
+     * retrieve the {@link androidx.camera.core.ImageProxy} object with the identifier by
+     * {@link androidx.camera.core.ImageProxyBundle#getImageProxy(int)}.
+     *
+     * @param id The identifier for the {@link CaptureStage}.
+     * */
+    public CaptureStage(int id) {
+        mCaptureRequestConfigurationBuilder.setTag(id);
+        mId = id;
+    }
+
+    /** Returns the identifier for the {@link CaptureStage}. */
+    @Override
+    public int getId() {
+        return mId;
+    }
+
+    /**
+     * Returns the {@link CaptureRequestConfiguration} for the {@link CaptureStage} object.
+     *
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public CaptureRequestConfiguration getCaptureRequestConfiguration() {
+        mCaptureRequestConfigurationBuilder.addImplementationOptions(
+                mCamera2ConfigurationBuilder.build());
+        return mCaptureRequestConfigurationBuilder.build();
+    }
+
+    /**
+     * Adds necessary {@link CaptureRequest.Key} settings into the {@link CaptureStage} object.
+     */
+    public <T> void addCaptureRequestParameters(CaptureRequest.Key<T> key, T value) {
+        mCamera2ConfigurationBuilder.setCaptureRequestOption(key, value);
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/DefaultImageCaptureUseCaseExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/DefaultImageCaptureUseCaseExtender.java
new file mode 100644
index 0000000..ad9ed76
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/DefaultImageCaptureUseCaseExtender.java
@@ -0,0 +1,40 @@
+/*
+ * 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.extensions;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+
+/**
+ * Default implementation for ImageCaptureUseCaseExtender.
+ */
+public final class DefaultImageCaptureUseCaseExtender extends ImageCaptureUseCaseExtender {
+    DefaultImageCaptureUseCaseExtender(ImageCaptureUseCaseConfiguration.Builder builder) {
+        super(builder);
+    }
+
+    @Override
+    public void enableExtension() {
+    }
+
+    @Override
+    protected boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        return false;
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/DefaultViewFinderUseCaseExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/DefaultViewFinderUseCaseExtender.java
new file mode 100644
index 0000000..94c6db6
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/DefaultViewFinderUseCaseExtender.java
@@ -0,0 +1,40 @@
+/*
+ * 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.extensions;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/**
+ * Default implementation for ViewFinderUseCaseExtender.
+ */
+public final class DefaultViewFinderUseCaseExtender extends ViewFinderUseCaseExtender {
+    DefaultViewFinderUseCaseExtender(ViewFinderUseCaseConfiguration.Builder builder) {
+        super(builder);
+    }
+
+    @Override
+    public void enableExtension() {
+    }
+
+    @Override
+    protected boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        return false;
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/ExtensionsManager.java b/camera/extensions/src/main/java/androidx/camera/extensions/ExtensionsManager.java
new file mode 100644
index 0000000..8423d78
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/ExtensionsManager.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.extensions;
+
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/**
+ * Provides interfaces for third party app developers to get capabilities info of extension
+ * functions.
+ */
+public final class ExtensionsManager {
+    /** The effect mode options applied on the bound use cases */
+    public enum EffectMode {
+        /** Normal mode without any specific effect applied. */
+        NORMAL,
+        /** Bokeh mode that is often applied as portrait mode for people pictures. */
+        BOKEH,
+        /**
+         * HDR mode that may get source pictures with different AE settings to generate a best
+         * result.
+         */
+        HDR
+    }
+
+    /**
+     * Indicates whether the camera device with the {@link LensFacing} can support the specific
+     * extension function.
+     *
+     * @param effectMode The extension function to be checked.
+     * @param lensFacing The {@link LensFacing} of the camera device to be checked.
+     * @return True if the specific extension function is supported for the camera device.
+     */
+    public static boolean isExtensionAvailable(EffectMode effectMode, LensFacing lensFacing) {
+        return checkImageCaptureExtensionCapability(effectMode, lensFacing)
+                || checkViewFinderExtensionCapability(effectMode, lensFacing);
+    }
+
+    /**
+     * Indicates whether the camera device with the {@link LensFacing} can support the specific
+     * extension function for specific use case.
+     *
+     * @param klass      The {@link androidx.camera.core.ImageCaptureUseCase} or
+     *                   {@link androidx.camera.core.ViewFinderUseCase} class
+     *                   to be checked.
+     * @param effectMode The extension function to be checked.
+     * @param lensFacing The {@link LensFacing} of the camera device to be checked.
+     * @return True if the specific extension function is supported for the camera device.
+     */
+    public static boolean isExtensionAvailable(
+            Class<?> klass, EffectMode effectMode, LensFacing lensFacing) {
+        boolean isAvailable = false;
+
+        if (klass == ImageCaptureUseCase.class) {
+            isAvailable = checkImageCaptureExtensionCapability(effectMode, lensFacing);
+        } else if (klass.equals(ViewFinderUseCase.class)) {
+            isAvailable = checkViewFinderExtensionCapability(effectMode, lensFacing);
+        }
+
+        return isAvailable;
+    }
+
+    private static boolean checkImageCaptureExtensionCapability(EffectMode effectMode,
+            LensFacing lensFacing) {
+        ImageCaptureUseCaseConfiguration.Builder builder =
+                new ImageCaptureUseCaseConfiguration.Builder();
+        builder.setLensFacing(lensFacing);
+        ImageCaptureUseCaseExtender extender;
+
+        switch (effectMode) {
+            case BOKEH:
+                extender = new BokehImageCaptureExtender(builder);
+                break;
+            case HDR:
+                extender = new HdrImageCaptureExtender(builder);
+                break;
+            default:
+                extender = new DefaultImageCaptureUseCaseExtender(builder);
+                break;
+        }
+
+        return extender.isExtensionAvailable();
+    }
+
+    private static boolean checkViewFinderExtensionCapability(EffectMode effectMode,
+            LensFacing lensFacing) {
+        ViewFinderUseCaseConfiguration.Builder builder =
+                new ViewFinderUseCaseConfiguration.Builder();
+        builder.setLensFacing(lensFacing);
+        ViewFinderUseCaseExtender extender;
+
+        switch (effectMode) {
+            case BOKEH:
+                extender = new BokehViewFinderExtender(builder);
+                break;
+            case HDR:
+                extender = new HdrViewFinderExtender(builder);
+                break;
+            default:
+                extender = new DefaultViewFinderUseCaseExtender(builder);
+                break;
+        }
+
+        return extender.isExtensionAvailable();
+    }
+
+    private ExtensionsManager() {
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/HdrImageCaptureExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/HdrImageCaptureExtender.java
new file mode 100644
index 0000000..1778b93
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/HdrImageCaptureExtender.java
@@ -0,0 +1,29 @@
+/*
+ * 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.extensions;
+
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+
+/**
+ * Load the OEM extension implementation for HDR effect type.
+ */
+public class HdrImageCaptureExtender extends ImageCaptureUseCaseExtender {
+    public HdrImageCaptureExtender(ImageCaptureUseCaseConfiguration.Builder builder) {
+        super(builder);
+        loadImplementation("androidx.camera.extensions.impl.HdrImageCaptureExtender");
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/HdrViewFinderExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/HdrViewFinderExtender.java
new file mode 100644
index 0000000..2b7fea2
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/HdrViewFinderExtender.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions;
+
+
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+/**
+ * Load the OEM extension ViewFinder implementation for HDR effect type.
+ */
+public class HdrViewFinderExtender extends ViewFinderUseCaseExtender {
+    public HdrViewFinderExtender(ViewFinderUseCaseConfiguration.Builder builder) {
+        super(builder);
+        loadImplementation("androidx.camera.extensions.impl.HdrViewFinderExtender");
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/ImageCaptureUseCaseExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/ImageCaptureUseCaseExtender.java
new file mode 100644
index 0000000..a3641fb
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/ImageCaptureUseCaseExtender.java
@@ -0,0 +1,148 @@
+/*
+ * 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.extensions;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.CaptureBundle;
+import androidx.camera.core.CaptureProcessor;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+/**
+ * Provides interfaces that OEM need to implement to enable extension function.
+ */
+public abstract class ImageCaptureUseCaseExtender {
+    private static final String TAG = "ImageCaptureExtender";
+    private final ImageCaptureUseCaseConfiguration.Builder mBuilder;
+    protected ImageCaptureUseCaseExtender mImpl;
+
+    public ImageCaptureUseCaseExtender(ImageCaptureUseCaseConfiguration.Builder builder) {
+        mBuilder = builder;
+    }
+
+    boolean loadImplementation(String className) {
+        try {
+            final Class<?> imageCaptureClass = Class.forName(className);
+            Constructor<?> imageCaptureConstructor =
+                    imageCaptureClass.getDeclaredConstructor(
+                            ImageCaptureUseCaseConfiguration.Builder.class);
+            mImpl = (ImageCaptureUseCaseExtender) imageCaptureConstructor.newInstance(mBuilder);
+        } catch (ClassNotFoundException
+                | NoSuchMethodException
+                | InstantiationException
+                | InvocationTargetException
+                | IllegalAccessException e) {
+            Log.e(TAG, "Failed to load image capture extension with exception: " + e);
+        }
+
+        if (mImpl == null) {
+            mImpl = new DefaultImageCaptureUseCaseExtender(mBuilder);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Indicates whether extension function can support with
+     * {@link ImageCaptureUseCaseConfiguration.Builder}
+     *
+     * @return True if the specific extension function is supported for the camera device.
+     */
+    public boolean isExtensionAvailable() {
+        LensFacing lensFacing = mBuilder.build().getLensFacing();
+        String cameraId;
+        try {
+            cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (CameraInfoUnavailableException e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + lensFacing, e);
+        }
+
+        Context context = CameraX.getContext();
+        CameraManager cameraManager = (CameraManager) context.getSystemService(
+                Context.CAMERA_SERVICE);
+        CameraCharacteristics cameraCharacteristics = null;
+        try {
+            cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
+        } catch (CameraAccessException e) {
+            throw new IllegalArgumentException(
+                    "Unable to retrieve info for camera with id " + cameraId + ".", e);
+        }
+
+        return isExtensionAvailable(cameraId, cameraCharacteristics);
+    }
+
+    protected boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        return mImpl.isExtensionAvailable(cameraId, cameraCharacteristics);
+    }
+
+    /** Enable the extension if available. If not available then acts a no-op. */
+    public void enableExtension() {
+        mImpl.enableExtension();
+    }
+
+    /**
+     * Sets necessary {@link CaptureStage} lists for the extension effect mode.
+     *
+     * <p>Sets one or more {@link CaptureStage} objects that depends on the requirement for the
+     * feature. If more than one {@link CaptureStage} is set, then the processing step must
+     * be set to process the multiple results into one final result.
+     *
+     * @param captureStages The necessary {@link CaptureStage} lists.
+     */
+    protected void setCaptureStages(@NonNull List<CaptureStage> captureStages) {
+        if (captureStages.isEmpty()) {
+            throw new IllegalArgumentException("The CaptureStage list is empty.");
+        }
+
+        CaptureBundle captureBundle = new CaptureBundle();
+
+        for (CaptureStage captureStage : captureStages) {
+            captureBundle.addCaptureStage(captureStage);
+        }
+
+        mBuilder.setCaptureBundle(captureBundle);
+    }
+
+    /**
+     * Sets the post processing step needed for the extension effect mode.
+     *
+     * <p>If there is more than one {@link CaptureStage} set by {@link #setCaptureStages(List)},
+     * then this must be set. Otherwise, this will be optional that depends on post
+     * processing requirement. The post processing will receive YUV_420_888 formatted image data.
+     * The post processing should also write out YUV_420_888 image data.
+     *
+     * @param captureProcessor The post processing implementation
+     */
+    protected void setCaptureProcessor(@NonNull CaptureProcessor captureProcessor) {
+        mBuilder.setCaptureProcessor(captureProcessor);
+    }
+}
diff --git a/camera/extensions/src/main/java/androidx/camera/extensions/ViewFinderUseCaseExtender.java b/camera/extensions/src/main/java/androidx/camera/extensions/ViewFinderUseCaseExtender.java
new file mode 100644
index 0000000..fa3b6d6
--- /dev/null
+++ b/camera/extensions/src/main/java/androidx/camera/extensions/ViewFinderUseCaseExtender.java
@@ -0,0 +1,121 @@
+/*
+ * 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.extensions;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraInfoUnavailableException;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.Configuration.Option;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Provides interfaces that OEM need to implement to enable extension function in ViewFinder.
+ */
+public abstract class ViewFinderUseCaseExtender {
+    private static final String TAG = "ViewFinderExtender";
+    private final ViewFinderUseCaseConfiguration.Builder mBuilder;
+    protected ViewFinderUseCaseExtender mImpl;
+
+    public ViewFinderUseCaseExtender(ViewFinderUseCaseConfiguration.Builder builder) {
+        mBuilder = builder;
+    }
+
+    boolean loadImplementation(String className) {
+        try {
+            final Class<?> ViewFinderClass = Class.forName(className);
+            Constructor<?> ViewFinderConstructor =
+                    ViewFinderClass.getDeclaredConstructor(
+                            ViewFinderUseCaseConfiguration.Builder.class);
+            mImpl = (ViewFinderUseCaseExtender) ViewFinderConstructor.newInstance(mBuilder);
+        } catch (ClassNotFoundException
+                | NoSuchMethodException
+                | InstantiationException
+                | InvocationTargetException
+                | IllegalAccessException e) {
+            Log.e(TAG, "Failed to load view finder extension with exception: " + e);
+        }
+
+        if (mImpl == null) {
+            mImpl = new DefaultViewFinderUseCaseExtender(mBuilder);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Indicates whether extension function can support with
+     * {@link ViewFinderUseCaseConfiguration.Builder}
+     *
+     * @return True if the specific extension function is supported for the camera device.
+     */
+    public boolean isExtensionAvailable() {
+        LensFacing lensFacing = mBuilder.build().getLensFacing();
+        String cameraId;
+        try {
+            cameraId = CameraX.getCameraWithLensFacing(lensFacing);
+        } catch (CameraInfoUnavailableException e) {
+            throw new IllegalArgumentException(
+                    "Unable to attach to camera with LensFacing " + lensFacing, e);
+        }
+
+        Context context = CameraX.getContext();
+        CameraManager cameraManager = (CameraManager) context.getSystemService(
+                Context.CAMERA_SERVICE);
+        CameraCharacteristics cameraCharacteristics = null;
+        try {
+            cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
+        } catch (CameraAccessException e) {
+            throw new IllegalArgumentException(
+                    "Unable to retrieve info for camera with id " + cameraId + ".", e);
+        }
+
+        return isExtensionAvailable(cameraId, cameraCharacteristics);
+    }
+
+    protected boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        return mImpl.isExtensionAvailable(cameraId, cameraCharacteristics);
+    }
+
+    /** Enable the extension if available. If not available then acts a no-op. */
+    public void enableExtension() {
+        mImpl.enableExtension();
+    }
+
+    protected void setCaptureStage(@NonNull CaptureStage captureStage) {
+        Configuration configuration =
+                captureStage.getCaptureRequestConfiguration().getImplementationOptions();
+
+        for (Option<?> option : configuration.listOptions()) {
+            @SuppressWarnings("unchecked") // Options/values are being copied directly
+                    Option<Object> objectOpt = (Option<Object>) option;
+            mBuilder.insertOption(objectOpt, configuration.retrieveOption(objectOpt));
+        }
+    }
+}
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/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
new file mode 100644
index 0000000..7ef2b3e
--- /dev/null
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.camera.integration.core"
+        minSdkVersion 21
+        versionCode 1
+        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 {
+        }
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation(project(":camera:camera-camera2"))
+
+    // Android Support Library
+    api(CONSTRAINT_LAYOUT, { transitive = true })
+    implementation(project(":appcompat"))
+    implementation 'androidx.test.espresso:espresso-idling-resource:3.1.0'
+    androidTestImplementation(TEST_EXT_JUNIT)
+    androidTestImplementation(TEST_CORE)
+    androidTestImplementation(TEST_RUNNER)
+    androidTestImplementation(TEST_RULES)
+    androidTestImplementation(ESPRESSO_CORE)
+    androidTestImplementation(TEST_UIAUTOMATOR)
+}
+
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
new file mode 100644
index 0000000..00ebb3d
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.rule.GrantPermissionRule;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+// Tests basic UI operation when using CoreTest app.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class BasicUITest {
+    private static final int TEST_COUNT = 2;
+    private static final int LAUNCH_TIMEOUT_MS = 5000;
+    private static final int IDLE_TIMEOUT_MS = 1000;
+
+    private final UiDevice mDevice =
+            UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    private final String mLauncherPackageName = mDevice.getLauncherPackageName();
+
+    @Rule
+    public ActivityTestRule<CameraXActivity> mActivityRule =
+            new ActivityTestRule<>(CameraXActivity.class);
+
+    @Rule
+    public GrantPermissionRule mCameraPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.CAMERA);
+    @Rule
+    public GrantPermissionRule mStoragePermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    @Rule
+    public GrantPermissionRule mAudioPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO);
+
+    @Before
+    public void setup() {
+        checkViewReady();
+    }
+
+    @Test
+    public void testAnalysisButton() {
+
+        IdlingRegistry.getInstance().register(
+                mActivityRule.getActivity().mAnalysisIdlingResource);
+        for (int i = 0; i < TEST_COUNT; i++) {
+            // Click to disable the use case.
+            onView(withId(R.id.AnalysisToggle)).perform(click());
+            waitForIdlingRegistry();
+            // Click to enable use case and check use case if ready.
+            onView(withId(R.id.AnalysisToggle)).perform(click());
+            waitForIdlingRegistry();
+        }
+
+        IdlingRegistry.getInstance().unregister(
+                mActivityRule.getActivity().mAnalysisIdlingResource);
+
+        pressBackAndReturnHome();
+    }
+
+    @Test
+    public void testPreviewButton() {
+
+        IdlingRegistry.getInstance().register(mActivityRule.getActivity().mViewIdlingResource);
+        // Clicks once to disable, then once to re-enable and check view is ready.
+        for (int i = 0; i < TEST_COUNT; i++) {
+            // Disables Preview.
+            onView(withId(R.id.PreviewToggle)).perform(click());
+            waitForIdlingRegistry();
+            // Enables Preview.
+            onView(withId(R.id.PreviewToggle)).perform(click());
+            waitForIdlingRegistry();
+            onView(withId(R.id.textureView)).perform(click()).check(matches(isDisplayed()));
+        }
+
+        IdlingRegistry.getInstance().unregister(mActivityRule.getActivity().mViewIdlingResource);
+        pressBackAndReturnHome();
+    }
+
+    private void checkViewReady() {
+        IdlingRegistry.getInstance().register(mActivityRule.getActivity().mViewIdlingResource);
+        onView(withId(R.id.textureView)).perform(click()).check(matches(isDisplayed()));
+        IdlingRegistry.getInstance().unregister(mActivityRule.getActivity().mViewIdlingResource);
+    }
+
+    private void waitForIdlingRegistry() {
+        // Idles Espresso thread and make activity complete each action.
+        IdlingResource idlingResource = new ElapsedTimeIdlingResource(IDLE_TIMEOUT_MS);
+        IdlingRegistry.getInstance().register(idlingResource);
+        Espresso.onIdle();
+        IdlingRegistry.getInstance().unregister(idlingResource);
+    }
+
+    private void pressBackAndReturnHome() {
+        mDevice.pressBack();
+
+        // Returns to Home to restart next test.
+        mDevice.pressHome();
+        mDevice.wait(Until.hasObject(By.pkg(mLauncherPackageName).depth(0)), LAUNCH_TIMEOUT_MS);
+    }
+
+}
+
+
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.java
new file mode 100644
index 0000000..3d7c04f
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNull.notNullValue;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.GrantPermissionRule;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+// Test application lifecycle when using CameraX.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class ExistingActivityLifecycleTest {
+    private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
+    private static final int LAUNCH_TIMEOUT_MS = 5000;
+    private static final int IDLE_TIMEOUT_MS = 1000;
+
+
+    private final UiDevice mDevice =
+            UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    private final String mLauncherPackageName = mDevice.getLauncherPackageName();
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final Intent mIntent = mContext.getPackageManager()
+            .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
+
+    @Rule
+    public GrantPermissionRule mCameraPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.CAMERA);
+    @Rule
+    public GrantPermissionRule mStoragePermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    @Rule
+    public GrantPermissionRule mAudioPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO);
+
+    @Before
+    public void setup() {
+        assertThat(mLauncherPackageName, notNullValue());
+        returnHomeScreen();
+    }
+
+    @After
+    public void tearDown() {
+        returnHomeScreen();
+    }
+
+    // Starts the activity, returns to the home screen to pause the activity, starts the activity
+    // again. This simulates lifecycle changes caused by a user pressing the HOME button.
+    @Test
+    public void startCoreTestTwiceToSimulatePauseResume() {
+        mContext.startActivity(mIntent);
+        waitUntilTextureViewIsReady();
+
+        returnHomeScreen();
+
+        mContext.startActivity(mIntent);
+        waitUntilTextureViewIsReady();
+
+        waitForIdlingRegistryAndPressBackButton();
+    }
+
+    // Starts the activity, returns to the home screen to pause the activity, starts the activity
+    // with a flag which clears any previous instance of the activity. This simulates lifecycle
+    // changes caused by a user pressing the BACK button.
+    @Test
+    public void startCoreTestTwiceClearingPreviousInstance() {
+        mContext.startActivity(mIntent);
+        waitUntilTextureViewIsReady();
+
+        returnHomeScreen();
+
+        // Clears out any previous instances.
+        mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        mContext.startActivity(mIntent);
+        waitUntilTextureViewIsReady();
+
+        waitForIdlingRegistryAndPressBackButton();
+    }
+
+    private void waitUntilTextureViewIsReady() {
+        mDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT_MS);
+        onView(withId(R.id.textureView)).check(matches(isDisplayed()));
+    }
+
+    private void returnHomeScreen() {
+        mDevice.pressHome();
+        mDevice.wait(Until.hasObject(By.pkg(mLauncherPackageName).depth(0)), LAUNCH_TIMEOUT_MS);
+    }
+
+    private void waitForIdlingRegistryAndPressBackButton() {
+        IdlingResource idlingResource = new ElapsedTimeIdlingResource(IDLE_TIMEOUT_MS);
+        IdlingRegistry.getInstance().register(idlingResource);
+        Espresso.onIdle();
+        IdlingRegistry.getInstance().unregister(idlingResource);
+
+        // Finishs the activity finally.
+        mDevice.pressBack();
+    }
+
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
new file mode 100644
index 0000000..2aca890
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNull.notNullValue;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.GrantPermissionRule;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+// Test new activity lifecycle when using CameraX.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class NewActivityLifecycleTest {
+    private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
+    private static final int LAUNCH_TIMEOUT_MS = 5000;
+    private static final int IDLE_TIMEOUT_MS = 1000;
+
+    private final UiDevice mDevice =
+            UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    private final String mLauncherPackageName = mDevice.getLauncherPackageName();
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final Intent mIntent = mContext.getPackageManager()
+            .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
+
+    @Rule
+    public GrantPermissionRule mCameraPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.CAMERA);
+    @Rule
+    public GrantPermissionRule mStoragePermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    @Rule
+    public GrantPermissionRule mAudioPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO);
+
+    @Before
+    public void setup() {
+        assertThat(mLauncherPackageName, notNullValue());
+        returnHomeScreen();
+    }
+
+    @After
+    public void tearDown() {
+        returnHomeScreen();
+    }
+
+    // Starts the activity, returns to the home screen to pause the activity, starts the activity
+    // with a flag which generates the new instance of the activity.
+    @Test
+    public void startCoreTestTwiceAlwaysWithNewInstance() {
+        mContext.startActivity(mIntent);
+        waitUntilTextureViewIsReady();
+
+        returnHomeScreen();
+
+        Intent newIntent = new Intent(Intent.ACTION_MAIN);
+        newIntent.setClass(mContext, CameraXActivity.class);
+        newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(newIntent);
+        waitUntilTextureViewIsReady();
+
+        waitForIdlingRegistryAndPressBackButton();
+    }
+
+    private void waitUntilTextureViewIsReady() {
+        mDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT_MS);
+        onView(withId(R.id.textureView)).check(matches(isDisplayed()));
+    }
+
+    private void returnHomeScreen() {
+        mDevice.pressHome();
+        mDevice.wait(Until.hasObject(By.pkg(mLauncherPackageName).depth(0)), LAUNCH_TIMEOUT_MS);
+    }
+
+    private void waitForIdlingRegistryAndPressBackButton() {
+        IdlingResource idlingResource = new ElapsedTimeIdlingResource(IDLE_TIMEOUT_MS);
+        IdlingRegistry.getInstance().register(idlingResource);
+        Espresso.onIdle();
+        IdlingRegistry.getInstance().unregister(idlingResource);
+
+        // Finish the activity finally.
+        mDevice.pressBack();
+    }
+
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
new file mode 100644
index 0000000..a8a9465
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static junit.framework.TestCase.assertNotNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import androidx.camera.core.FlashMode;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
+import androidx.camera.integration.core.idlingresource.WaitForViewToShow;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.rule.GrantPermissionRule;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test toggle buttons in CoreTestApp. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class ToggleButtonUITest {
+
+    private static final int LAUNCH_TIMEOUT_MS = 5000;
+    private static final int IDLE_TIMEOUT_MS = 1000;
+
+    private final UiDevice mDevice =
+            UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    private final String mLauncherPackageName = mDevice.getLauncherPackageName();
+
+    @Rule
+    public ActivityTestRule<CameraXActivity> mActivityRule =
+            new ActivityTestRule<>(CameraXActivity.class);
+
+    @Rule
+    public GrantPermissionRule mCameraPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.CAMERA);
+    @Rule
+    public GrantPermissionRule mStoragePermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    @Rule
+    public GrantPermissionRule mAudioPermissionRule =
+            GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO);
+
+    public static void waitFor(IdlingResource idlingResource) {
+        IdlingRegistry.getInstance().register(idlingResource);
+        Espresso.onIdle();
+        IdlingRegistry.getInstance().unregister(idlingResource);
+    }
+
+    @Test
+    public void testFlashToggleButton() {
+        waitFor(new WaitForViewToShow(R.id.flash_toggle));
+
+        ImageCaptureUseCase useCase = mActivityRule.getActivity().getImageCaptureUseCase();
+        assertNotNull(useCase);
+
+        // There are 3 different states of flash mode: ON, OFF and AUTO.
+        // By pressing flash mode toggle button, the flash mode would switch to the next state.
+        // The flash mode would loop in following sequence: OFF -> AUTO -> ON -> OFF.
+        FlashMode mode1 = useCase.getFlashMode();
+
+        onView(withId(R.id.flash_toggle)).perform(click());
+        FlashMode mode2 = useCase.getFlashMode();
+        // After the switch, the mode2 should be different from mode1.
+        assertNotEquals(mode2, mode1);
+
+        onView(withId(R.id.flash_toggle)).perform(click());
+        FlashMode mode3 = useCase.getFlashMode();
+        // The mode3 should be different from first and second time.
+        assertNotEquals(mode3, mode2);
+        assertNotEquals(mode3, mode1);
+
+        waitForIdlingRegistryAndPressBackAndHomeButton();
+    }
+
+    @Test
+    public void testTorchToggleButton() {
+        waitFor(new WaitForViewToShow(R.id.torch_toggle));
+
+        ViewFinderUseCase useCase = mActivityRule.getActivity().getViewFinderUseCase();
+        assertNotNull(useCase);
+        boolean isTorchOn = useCase.isTorchOn();
+
+        onView(withId(R.id.torch_toggle)).perform(click());
+        assertNotEquals(useCase.isTorchOn(), isTorchOn);
+
+        // By pressing the torch toggle button two times, it should switch back to original state.
+        onView(withId(R.id.torch_toggle)).perform(click());
+        assertEquals(useCase.isTorchOn(), isTorchOn);
+
+        waitForIdlingRegistryAndPressBackAndHomeButton();
+    }
+
+    @Test
+    public void testSwitchCameraToggleButton() {
+        waitFor(new WaitForViewToShow(R.id.direction_toggle));
+
+        boolean isViewFinderExist = mActivityRule.getActivity().getViewFinderUseCase() != null;
+        boolean isImageCaptureExist = mActivityRule.getActivity().getImageCaptureUseCase() != null;
+        boolean isVideoCaptureExist = mActivityRule.getActivity().getVideoCaptureUseCase() != null;
+        boolean isImageAnalysisExist =
+                mActivityRule.getActivity().getImageAnalysisUseCase() != null;
+
+        for (int i = 0; i < 2; i++) {
+            onView(withId(R.id.direction_toggle)).perform(click());
+            waitFor(new ElapsedTimeIdlingResource(2000));
+            if (isImageCaptureExist) {
+                assertNotNull(mActivityRule.getActivity().getImageCaptureUseCase());
+            }
+            if (isImageAnalysisExist) {
+                assertNotNull(mActivityRule.getActivity().getImageAnalysisUseCase());
+            }
+            if (isVideoCaptureExist) {
+                assertNotNull(mActivityRule.getActivity().getVideoCaptureUseCase());
+            }
+            if (isViewFinderExist) {
+                assertNotNull(mActivityRule.getActivity().getViewFinderUseCase());
+            }
+        }
+
+        waitForIdlingRegistryAndPressBackAndHomeButton();
+    }
+
+    private void waitForIdlingRegistryAndPressBackAndHomeButton() {
+        // Idles Espresso thread and make activity complete each action.
+        waitFor(new ElapsedTimeIdlingResource(IDLE_TIMEOUT_MS));
+
+        mDevice.pressBack();
+
+        // Returns to Home to restart next test.
+        mDevice.pressHome();
+        mDevice.wait(Until.hasObject(By.pkg(mLauncherPackageName).depth(0)), LAUNCH_TIMEOUT_MS);
+    }
+
+}
+
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/ElapsedTimeIdlingResource.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/ElapsedTimeIdlingResource.java
new file mode 100644
index 0000000..fad24ea
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/ElapsedTimeIdlingResource.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core.idlingresource;
+
+import androidx.test.espresso.IdlingResource;
+
+/** Idling resource which will block until the timeout occurs. */
+public class ElapsedTimeIdlingResource implements IdlingResource {
+
+    private long mStartTime;
+    private long mWaitTime;
+    private IdlingResource.ResourceCallback mResourceCallback;
+
+    public ElapsedTimeIdlingResource(long waitTime) {
+        mStartTime = System.currentTimeMillis();
+        mWaitTime = waitTime;
+    }
+
+    @Override
+    public String getName() {
+        return "ElapsedTimeIdlingResource:" + mWaitTime;
+    }
+
+    @Override
+    public boolean isIdleNow() {
+        if ((System.currentTimeMillis() - mStartTime) >= mWaitTime) {
+            mResourceCallback.onTransitionToIdle();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
+        mResourceCallback = resourceCallback;
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/ViewIdlingResource.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/ViewIdlingResource.java
new file mode 100644
index 0000000..1cbb0a1
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/ViewIdlingResource.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core.idlingresource;
+
+import android.app.Activity;
+import android.view.View;
+
+import androidx.annotation.IdRes;
+import androidx.test.espresso.IdlingResource;
+import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
+import androidx.test.runner.lifecycle.Stage;
+
+import java.util.Collection;
+
+/** Idling resource which wait for view with given resource id. */
+public abstract class ViewIdlingResource implements IdlingResource {
+
+    @IdRes
+    private final int mViewId;
+
+    private ResourceCallback mResourceCallback;
+
+    protected ViewIdlingResource(@IdRes int viewId) {
+        mViewId = viewId;
+    }
+
+    protected abstract boolean isViewIdle(View view);
+
+    @Override
+    public String getName() {
+        return "ViewIdlingResource";
+    }
+
+    @Override
+    public boolean isIdleNow() {
+        Collection<Activity> resumedActivities =
+                ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
+        for (Activity activity : resumedActivities) {
+            View view = activity.findViewById(mViewId);
+            if (view != null && isViewIdle(view)) {
+                if (mResourceCallback != null) {
+                    mResourceCallback.onTransitionToIdle();
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void registerIdleTransitionCallback(ResourceCallback callback) {
+        mResourceCallback = callback;
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/WaitForViewToHide.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/WaitForViewToHide.java
new file mode 100644
index 0000000..8c586e9
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/WaitForViewToHide.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core.idlingresource;
+
+import android.view.View;
+
+/** Idling resource which waits for a view to be hidden. */
+public class WaitForViewToHide extends ViewIdlingResource {
+
+    public WaitForViewToHide(int viewId) {
+        super(viewId);
+    }
+
+    @Override
+    protected boolean isViewIdle(View view) {
+        return !view.isShown();
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/WaitForViewToShow.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/WaitForViewToShow.java
new file mode 100644
index 0000000..89b1d1f
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/idlingresource/WaitForViewToShow.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core.idlingresource;
+
+import android.view.View;
+
+/** Idling resource which waits for a view to be shown. */
+public class WaitForViewToShow extends ViewIdlingResource {
+
+    public WaitForViewToShow(int viewId) {
+        super(viewId);
+    }
+
+    @Override
+    protected boolean isViewIdle(View view) {
+        return view.isShown();
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..85f8c5a
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?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.integration.core">
+    <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=".CameraXActivity"
+            android:label="Camera Core Test App">
+            <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/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
new file mode 100644
index 0000000..c6eccef
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -0,0 +1,873 @@
+/*
+ * 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.integration.core;
+
+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.util.Log;
+import android.util.Size;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AppCompatActivity;
+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.FlashMode;
+import androidx.camera.core.ImageAnalysisUseCase;
+import androidx.camera.core.ImageAnalysisUseCaseConfiguration;
+import androidx.camera.core.ImageCaptureUseCase;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.VideoCaptureUseCase;
+import androidx.camera.core.VideoCaptureUseCaseConfiguration;
+import androidx.camera.core.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.test.espresso.idling.CountingIdlingResource;
+
+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, View.OnLayoutChangeListener {
+    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> mSettableResult = new SettableCallable<>();
+    private final FutureTask<Boolean> mCompletableFuture = new FutureTask<>(mSettableResult);
+    private final AtomicLong mImageAnalysisFrameCount = new AtomicLong(0);
+    private final MutableLiveData<String> mImageAnalysisResult = new MutableLiveData<>();
+    private VideoFileSaver mVideoFileSaver;
+    /** The cameraId to use. Assume that 0 is the typical back facing camera. */
+    private LensFacing mCurrentCameraLensFacing = LensFacing.BACK;
+
+    // TODO: Move the analysis processing, capture processing to separate threads, so
+    // there is smaller impact on the preview.
+    private String mCurrentCameraDirection = "BACKWARD";
+    private ViewFinderUseCase mViewFinderUseCase;
+    private ImageAnalysisUseCase mImageAnalysisUseCase;
+    private ImageCaptureUseCase mImageCaptureUseCase;
+    private VideoCaptureUseCase mVideoCaptureUseCase;
+
+    // Espresso testing variables
+    @VisibleForTesting
+    CountingIdlingResource mViewIdlingResource = new CountingIdlingResource("view");
+    private static final int FRAMES_UNTIL_VIEW_IS_READY = 5;
+    @VisibleForTesting
+    CountingIdlingResource mAnalysisIdlingResource =
+            new CountingIdlingResource("analysis");
+    @VisibleForTesting
+    CountingIdlingResource mImageSavedIdlingResource =
+            new CountingIdlingResource("imagesaved");
+
+
+    /**
+     * 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(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        Button buttonView = (Button) view;
+                        if (mViewFinderUseCase != null) {
+                            // Remove the use case
+                            buttonView.setBackgroundColor(Color.RED);
+                            CameraX.unbind(mViewFinderUseCase);
+                            mViewFinderUseCase = null;
+                        } else {
+                            // Add the use case
+                            buttonView.setBackgroundColor(Color.LTGRAY);
+
+                            CameraXActivity.this.enableViewFinderUseCase();
+                        }
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + mViewFinderUseCase);
+    }
+
+    void enableViewFinderUseCase() {
+        ViewFinderUseCaseConfiguration configuration =
+                new ViewFinderUseCaseConfiguration.Builder()
+                        .setLensFacing(mCurrentCameraLensFacing)
+                        .setTargetName("ViewFinder")
+                        .build();
+
+        mViewFinderUseCase = new ViewFinderUseCase(configuration);
+        TextureView textureView = findViewById(R.id.textureView);
+        mViewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                new ViewFinderUseCase.OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderUseCase.ViewFinderOutput 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());
+                    }
+                });
+
+        textureView.addOnLayoutChangeListener(this);
+
+        for (int i = 0; i < FRAMES_UNTIL_VIEW_IS_READY; i++) {
+            mViewIdlingResource.increment();
+        }
+
+        if (!bindToLifecycleSafely(mViewFinderUseCase, R.id.PreviewToggle)) {
+            mViewFinderUseCase = 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) {
+                    }
+
+                    @Override
+                    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+                        return true;
+                    }
+
+                    @Override
+                    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+                        // Wait until surface texture receives enough updates.
+                        if (!mViewIdlingResource.isIdleNow()) {
+                            mViewIdlingResource.decrement();
+                        }
+                    }
+                });
+    }
+
+    void transformPreview() {
+        String cameraId = null;
+        LensFacing viewFinderLensFacing =
+                ((CameraDeviceConfiguration) mViewFinderUseCase.getUseCaseConfiguration())
+                        .getLensFacing(/*valueIfMissing=*/ null);
+        if (viewFinderLensFacing != mCurrentCameraLensFacing) {
+            throw new IllegalStateException(
+                    "Invalid view finder lens facing: "
+                            + viewFinderLensFacing
+                            + " Should be: "
+                            + mCurrentCameraLensFacing);
+        }
+        try {
+            cameraId = CameraX.getCameraWithLensFacing(viewFinderLensFacing);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(
+                    "Unable to get camera id for lens facing " + viewFinderLensFacing, e);
+        }
+        Size srcResolution = mViewFinderUseCase.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(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        Button buttonView = (Button) view;
+                        if (mImageAnalysisUseCase != null) {
+                            // Remove the use case
+                            buttonView.setBackgroundColor(Color.RED);
+                            CameraX.unbind(mImageAnalysisUseCase);
+                            mImageAnalysisUseCase = null;
+                        } else {
+                            // Add the use case
+                            buttonView.setBackgroundColor(Color.LTGRAY);
+                            CameraXActivity.this.enableImageAnalysisUseCase();
+                        }
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + mImageAnalysisUseCase);
+    }
+
+    void enableImageAnalysisUseCase() {
+        ImageAnalysisUseCaseConfiguration configuration =
+                new ImageAnalysisUseCaseConfiguration.Builder()
+                        .setLensFacing(mCurrentCameraLensFacing)
+                        .setTargetName("ImageAnalysis")
+                        .setCallbackHandler(new Handler(Looper.getMainLooper()))
+                        .build();
+
+        mImageAnalysisUseCase = new ImageAnalysisUseCase(configuration);
+        TextView textView = this.findViewById(R.id.textView);
+        mAnalysisIdlingResource.increment();
+
+        if (!bindToLifecycleSafely(mImageAnalysisUseCase, R.id.AnalysisToggle)) {
+            mImageAnalysisUseCase = null;
+            return;
+        }
+
+        mImageAnalysisUseCase.setAnalyzer(
+                new ImageAnalysisUseCase.Analyzer() {
+                    @Override
+                    public void analyze(ImageProxy image, int 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.
+                        mImageAnalysisResult.setValue(Long.toString(image.getTimestamp()));
+
+                        if (!mAnalysisIdlingResource.isIdleNow()) {
+                            mAnalysisIdlingResource.decrement();
+                        }
+                    }
+                });
+        mImageAnalysisResult.observe(
+                this,
+                new Observer<String>() {
+                    @Override
+                    public void onChanged(String text) {
+                        if (mImageAnalysisFrameCount.getAndIncrement() % 30 == 0) {
+                            textView.setText(
+                                    "ImgCount: " + mImageAnalysisFrameCount.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(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        Button buttonView = (Button) view;
+                        if (mImageCaptureUseCase != null) {
+                            // Remove the use case
+                            buttonView.setBackgroundColor(Color.RED);
+                            CameraXActivity.this.disableImageCaptureUseCase();
+                        } else {
+                            // Add the use case
+                            buttonView.setBackgroundColor(Color.LTGRAY);
+                            CameraXActivity.this.enableImageCaptureUseCase();
+                        }
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + mImageCaptureUseCase);
+    }
+
+    void enableImageCaptureUseCase() {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder()
+                        .setLensFacing(mCurrentCameraLensFacing)
+                        .setTargetName("ImageCapture")
+                        .build();
+
+        mImageCaptureUseCase = new ImageCaptureUseCase(configuration);
+
+        if (!bindToLifecycleSafely(mImageCaptureUseCase, R.id.PhotoToggle)) {
+            Button button = this.findViewById(R.id.Picture);
+            button.setOnClickListener(null);
+            mImageCaptureUseCase = 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(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        mImageSavedIdlingResource.increment();
+
+                        mImageCaptureUseCase.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);
+                                        if (!mImageSavedIdlingResource.isIdleNow()) {
+                                            mImageSavedIdlingResource.decrement();
+                                        }
+                                    }
+
+                                    @Override
+                                    public void onError(
+                                            ImageCaptureUseCase.UseCaseError useCaseError,
+                                            String message,
+                                            Throwable cause) {
+                                        Log.e(TAG, "Failed to save image.", cause);
+                                        if (!mImageSavedIdlingResource.isIdleNow()) {
+                                            mImageSavedIdlingResource.decrement();
+                                        }
+                                    }
+                                });
+                    }
+                });
+
+        refreshFlashButtonIcon();
+    }
+
+    void disableImageCaptureUseCase() {
+        CameraX.unbind(mImageCaptureUseCase);
+
+        mImageCaptureUseCase = null;
+        Button button = this.findViewById(R.id.Picture);
+        button.setOnClickListener(null);
+
+        refreshFlashButtonIcon();
+    }
+
+    private void refreshFlashButtonIcon() {
+        ImageButton flashToggle = findViewById(R.id.flash_toggle);
+        if (mImageCaptureUseCase != null) {
+            flashToggle.setVisibility(View.VISIBLE);
+            flashToggle.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    FlashMode flashMode = mImageCaptureUseCase.getFlashMode();
+                    if (flashMode == FlashMode.ON) {
+                        mImageCaptureUseCase.setFlashMode(FlashMode.OFF);
+                    } else if (flashMode == FlashMode.OFF) {
+                        mImageCaptureUseCase.setFlashMode(FlashMode.AUTO);
+                    } else if (flashMode == FlashMode.AUTO) {
+                        mImageCaptureUseCase.setFlashMode(FlashMode.ON);
+                    }
+                    refreshFlashButtonIcon();
+                }
+            });
+            FlashMode flashMode = mImageCaptureUseCase.getFlashMode();
+            switch (flashMode) {
+                case ON:
+                    flashToggle.setImageResource(R.drawable.ic_flash_on);
+                    break;
+                case OFF:
+                    flashToggle.setImageResource(R.drawable.ic_flash_off);
+                    break;
+                case AUTO:
+                    flashToggle.setImageResource(R.drawable.ic_flash_auto);
+                    break;
+            }
+
+        } else {
+            flashToggle.setVisibility(View.GONE);
+            flashToggle.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();
+
+        mVideoFileSaver = new VideoFileSaver();
+        mVideoFileSaver.setRootDirectory(
+                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
+
+        button.setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        Button buttonView = (Button) view;
+                        if (mVideoCaptureUseCase != null) {
+                            // Remove the use case
+                            buttonView.setBackgroundColor(Color.RED);
+                            CameraXActivity.this.disableVideoCaptureUseCase();
+                        } else {
+                            // Add the use case
+                            buttonView.setBackgroundColor(Color.LTGRAY);
+                            CameraXActivity.this.enableVideoCaptureUseCase();
+                        }
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + mVideoCaptureUseCase);
+    }
+
+    void enableVideoCaptureUseCase() {
+        VideoCaptureUseCaseConfiguration configuration =
+                new VideoCaptureUseCaseConfiguration.Builder()
+                        .setLensFacing(mCurrentCameraLensFacing)
+                        .setTargetName("VideoCapture")
+                        .build();
+
+        mVideoCaptureUseCase = new VideoCaptureUseCase(configuration);
+
+        if (!bindToLifecycleSafely(mVideoCaptureUseCase, R.id.VideoToggle)) {
+            Button button = this.findViewById(R.id.Video);
+            button.setOnClickListener(null);
+            mVideoCaptureUseCase = null;
+            return;
+        }
+
+        Button button = this.findViewById(R.id.Video);
+        button.setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        Button buttonView = (Button) view;
+                        String text = button.getText().toString();
+                        if (text.equals("Record") && !mVideoFileSaver.isSaving()) {
+                            mVideoCaptureUseCase.startRecording(
+                                    mVideoFileSaver.getNewVideoFile(), mVideoFileSaver);
+                            mVideoFileSaver.setSaving();
+                            buttonView.setText("Stop");
+                        } else if (text.equals("Stop") && mVideoFileSaver.isSaving()) {
+                            buttonView.setText("Record");
+                            mVideoCaptureUseCase.stopRecording();
+                        } else if (text.equals("Record") && mVideoFileSaver.isSaving()) {
+                            buttonView.setText("Stop");
+                            mVideoFileSaver.setSaving();
+                        } else if (text.equals("Stop") && !mVideoFileSaver.isSaving()) {
+                            buttonView.setText("Record");
+                        }
+                    }
+                });
+    }
+
+    void disableVideoCaptureUseCase() {
+        Button button = this.findViewById(R.id.Video);
+        button.setOnClickListener(null);
+        CameraX.unbind(mVideoCaptureUseCase);
+
+        mVideoCaptureUseCase = 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) {
+                mCurrentCameraDirection = newCameraDirection;
+            }
+        }
+
+        new Thread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        CameraXActivity.this.setupCamera();
+                    }
+                })
+                .start();
+        setupPermissions();
+    }
+
+    private void setupCamera() {
+        try {
+            // Wait for permissions before proceeding.
+            if (!mCompletableFuture.get()) {
+                Log.d(TAG, "Permissions denied.");
+                return;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Exception occurred getting permission future: " + e);
+        }
+
+        Log.d(TAG, "Camera direction: " + mCurrentCameraDirection);
+        if (mCurrentCameraDirection.equalsIgnoreCase("BACKWARD")) {
+            mCurrentCameraLensFacing = LensFacing.BACK;
+        } else if (mCurrentCameraDirection.equalsIgnoreCase("FORWARD")) {
+            mCurrentCameraLensFacing = LensFacing.FRONT;
+        } else {
+            throw new RuntimeException("Invalid camera direction: " + mCurrentCameraDirection);
+        }
+        Log.d(TAG, "Using camera lens facing: " + mCurrentCameraLensFacing);
+
+        // Run this on the UI thread to manipulate the Textures & Views.
+        CameraXActivity.this.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        CameraXActivity.this.createUseCases();
+
+                        ImageButton directionToggle = findViewById(R.id.direction_toggle);
+                        directionToggle.setVisibility(View.VISIBLE);
+                        directionToggle.setOnClickListener(new View.OnClickListener() {
+                            @Override
+                            public void onClick(View v) {
+                                if (mCurrentCameraLensFacing == LensFacing.BACK) {
+                                    mCurrentCameraLensFacing = LensFacing.FRONT;
+                                } else if (mCurrentCameraLensFacing == LensFacing.FRONT) {
+                                    mCurrentCameraLensFacing = LensFacing.BACK;
+                                }
+
+                                Log.d(TAG, "Change camera direction: " + mCurrentCameraLensFacing);
+
+                                // Rebind all use cases.
+                                CameraX.unbindAll();
+                                if (mImageCaptureUseCase != null) {
+                                    enableImageCaptureUseCase();
+                                }
+                                if (mViewFinderUseCase != null) {
+                                    enableViewFinderUseCase();
+                                }
+                                if (mImageAnalysisUseCase != null) {
+                                    enableImageAnalysisUseCase();
+                                }
+                                if (mVideoCaptureUseCase != null) {
+                                    enableVideoCaptureUseCase();
+                                }
+                            }
+                        });
+
+                        ImageButton torchToggle = findViewById(R.id.torch_toggle);
+                        torchToggle.setVisibility(View.VISIBLE);
+                        torchToggle.setOnClickListener(new View.OnClickListener() {
+                            @Override
+                            public void onClick(View v) {
+                                if (mViewFinderUseCase != null) {
+                                    boolean toggledState = !mViewFinderUseCase.isTorchOn();
+                                    Log.d(TAG, "Set camera torch: " + toggledState);
+                                    mViewFinderUseCase.enableTorch(toggledState);
+                                }
+                            }
+                        });
+                    }
+                });
+    }
+
+    private void setupPermissions() {
+        if (!allPermissionsGranted()) {
+            makePermissionRequest();
+        } else {
+            mSettableResult.set(true);
+            mCompletableFuture.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.");
+                    mSettableResult.set(true);
+                    mCompletableFuture.run();
+                } else {
+                    Log.d(TAG, "Permissions Denied.");
+                    mSettableResult.set(false);
+                    mCompletableFuture.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;
+    }
+
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+            int oldTop, int oldRight, int oldBottom) {
+        transformPreview();
+    }
+
+    /** A {@link Callable} whose return value can be set. */
+    private static final class SettableCallable<V> implements Callable<V> {
+        private final AtomicReference<V> mValue = new AtomicReference<>();
+
+        public void set(V value) {
+            mValue.set(value);
+        }
+
+        @Override
+        public V call() {
+            return mValue.get();
+        }
+    }
+
+    ViewFinderUseCase getViewFinderUseCase() {
+        return mViewFinderUseCase;
+    }
+
+    ImageAnalysisUseCase getImageAnalysisUseCase() {
+        return mImageAnalysisUseCase;
+    }
+
+    ImageCaptureUseCase getImageCaptureUseCase() {
+        return mImageCaptureUseCase;
+    }
+
+    VideoCaptureUseCase getVideoCaptureUseCase() {
+        return mVideoCaptureUseCase;
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoFileSaver.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoFileSaver.java
new file mode 100644
index 0000000..57161b0
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/VideoFileSaver.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.integration.core;
+
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.Nullable;
+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 mFormatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.ENGLISH);
+    private final Object mLock = new Object();
+    private File mRootDirectory;
+    @GuardedBy("mLock")
+    private boolean mIsSaving = false;
+
+    @Override
+    public void onVideoSaved(File file) {
+
+        Log.d(TAG, "Saved file: " + file.getPath());
+        synchronized (mLock) {
+            mIsSaving = 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 (mLock) {
+            mIsSaving = false;
+        }
+    }
+
+    /** Returns a new {@link File} where to save a video. */
+    public File getNewVideoFile() {
+        Date date = Calendar.getInstance().getTime();
+        File file = new File(mRootDirectory + "/" + mFormatter.format(date) + ".mp4");
+        return file;
+    }
+
+    /** Sets the directory for saving files. */
+    public void setRootDirectory(File rootDirectory) {
+
+        mRootDirectory = rootDirectory;
+    }
+
+    boolean isSaving() {
+        synchronized (mLock) {
+            return mIsSaving;
+        }
+    }
+
+    /** Sets saving state after video startRecording */
+    void setSaving() {
+        synchronized (mLock) {
+            mIsSaving = true;
+        }
+    }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/res/drawable/ic_camera_switch.xml b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_camera_switch.xml
new file mode 100644
index 0000000..8a0c372
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_camera_switch.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2015 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:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M20,4h-3.17L15,2H9L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2zm-5,11.5V13H9v2.5L5.5,12 9,8.5V11h6V8.5l3.5,3.5 -3.5,3.5z"/>
+</vector>
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_auto.xml b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_auto.xml
new file mode 100644
index 0000000..8d29812
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_auto.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016 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:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M3,2v12h3v9l7,-12L9,11l4,-9L3,2zM19,2h-2l-3.2,9h1.9l0.7,-2h3.2l0.7,2h1.9L19,2zM16.85,7.65L18,4l1.15,3.65h-2.3z"/>
+</vector>
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_off.xml b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_off.xml
new file mode 100644
index 0000000..4e338f8
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_off.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016 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:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M3.27,3L2,4.27l5,5V13h3v9l3.58,-6.14L17.73,20 19,18.73 3.27,3zM17,10h-4l4,-8H7v2.18l8.46,8.46L17,10z"/>
+</vector>
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_on.xml b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_on.xml
new file mode 100644
index 0000000..b70eee0
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flash_on.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016 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:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M7,2v11h3v9l7,-12h-4l4,-8z"/>
+</vector>
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flashlight.xml b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flashlight.xml
new file mode 100644
index 0000000..f3de137
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/drawable/ic_flashlight.xml
@@ -0,0 +1,33 @@
+<!--
+/**
+ * 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:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:pathData="M 0 0 H 24 V 24 H 0 V 0 Z" />
+    <path
+        android:pathData="M 0 0 H 24 V 24 H 0 V 0 Z" />
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M11,22h2a2,2,0,0,0,2-2V10a6.84,6.84,0,0,0,3-6V3H6V4a6.84,6.84,0,0,0,3,6V20A2,2,0,0,0,11,22ZM17,4a8.26,8.26,0,0,1-0.07,1H7.07A8.26,8.26,0,0,1,7,4ZM7.28,6h9.45a5.24,5.24,0,0,1-2.24,3.14L14,9.43V20a1,1,0,0,1-1,1H11a1,1,0,0,1-1-1V9.43l-0.49-0.29A5.25,5.25,0,0,1,7.28,6Z" />
+    <path
+        android:fillColor="@android:color/black"
+        android:pathData="M 12 13 C 12.5522847498 13 13 13.4477152502 13 14 C 13 14.5522847498 12.5522847498 15 12 15 C 11.4477152502 15 11 14.5522847498 11 14 C 11 13.4477152502 11.4477152502 13 12 13 Z" />
+</vector>
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
new file mode 100644
index 0000000..f6f5184
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/main/res/layout/activity_camera_xmain.xml
@@ -0,0 +1,204 @@
+<?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.
+-->
+<androidx.constraintlayout.widget.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.integration.core.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" />
+
+    <androidx.constraintlayout.widget.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" />
+
+    <androidx.constraintlayout.widget.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" />
+
+    <androidx.constraintlayout.widget.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"
+        />
+
+    <ImageButton
+        android:id="@+id/torch_toggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:src="@drawable/ic_flashlight"
+        android:translationZ="1dp"
+        android:background="@android:drawable/btn_default"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.05"
+        app:layout_constraintStart_toStartOf="@id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.7" />
+
+    <ImageButton
+        android:id="@+id/flash_toggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:src="@drawable/ic_flash_off"
+        android:translationZ="1dp"
+        android:background="@android:drawable/btn_default"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.05"
+        app:layout_constraintStart_toStartOf="@id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.5" />
+
+    <ImageButton
+        android:id="@+id/direction_toggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:src="@drawable/ic_camera_switch"
+        android:translationZ="1dp"
+        android:background="@android:drawable/btn_default"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.05"
+        app:layout_constraintStart_toStartOf="@id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.3" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/camera/integration-tests/coretestapp/src/main/res/values/style.xml b/camera/integration-tests/coretestapp/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/coretestapp/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/extensionstestapp/build.gradle b/camera/integration-tests/extensionstestapp/build.gradle
new file mode 100644
index 0000000..87840b07
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.camera.integration.extensions"
+        minSdkVersion 21
+        versionCode 1
+        multiDexEnabled true
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation(project(":camera:camera-camera2"))
+    implementation(project(":camera:camera-extensions"))
+
+    // Android Support Library
+    api(CONSTRAINT_LAYOUT, { transitive = true })
+    implementation(project(":appcompat"))
+
+    // Guava
+    implementation(GUAVA_ANDROID)
+}
+
diff --git a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f7ceba1
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?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 xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.camera.integration.extensions">
+
+    <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-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+    <uses-permission android:name="android.permission.WRITE_MEDIA_IMAGES" />
+    <uses-permission android:name="android.permission.WRITE_MEDIA_VIDEO" />
+
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application
+        android:theme="@style/AppTheme">
+        <activity
+            android:name="androidx.camera.integration.extensions.CameraExtensionsActivity"
+            android:label="Camera Extensions">
+            <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/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
new file mode 100644
index 0000000..3c4b853
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.extensions;
+
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.StrictMode;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+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.ViewFinderUseCase;
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.camera.extensions.BokehImageCaptureExtender;
+import androidx.camera.extensions.BokehViewFinderExtender;
+import androidx.camera.extensions.HdrImageCaptureExtender;
+import androidx.camera.extensions.HdrViewFinderExtender;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import java.io.File;
+import java.text.Format;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** An activity that shows off how extensions can be applied */
+public class CameraExtensionsActivity extends AppCompatActivity
+        implements ActivityCompat.OnRequestPermissionsResultCallback {
+    private static final String TAG = "CameraExtensionActivity";
+    private static final int PERMISSIONS_REQUEST_CODE = 42;
+
+    private final SettableCallable<Boolean> mSettableResult = new SettableCallable<>();
+    private final FutureTask<Boolean> mCompletableFuture = new FutureTask<>(mSettableResult);
+
+    /** The cameraId to use. Assume that 0 is the typical back facing camera. */
+    private String mCurrentCameraId = "0";
+
+    private String mCurrentCameraFacing = "BACK";
+
+    private ViewFinderUseCase mViewFinderUseCase;
+    private ImageCaptureUseCase mImageCaptureUseCase;
+    private ImageCaptureType mCurrentImageCaptureType = ImageCaptureType.IMAGE_CAPTURE_TYPE_HDR;
+
+    /**
+     * 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() {
+        enableViewFinderUseCase();
+        Log.i(TAG, "Got UseCase: " + mViewFinderUseCase);
+    }
+
+    void enableViewFinderUseCase() {
+        if (mViewFinderUseCase != null) {
+            CameraX.unbind(mViewFinderUseCase);
+        }
+
+        ViewFinderUseCaseConfiguration.Builder builder =
+                new ViewFinderUseCaseConfiguration.Builder()
+                        .setLensFacing(LensFacing.BACK)
+                        .setTargetName("ViewFinder");
+
+        Log.d(TAG, "Enabling the extended view finder");
+        if (mCurrentImageCaptureType == ImageCaptureType.IMAGE_CAPTURE_TYPE_BOKEH) {
+            Log.d(TAG, "Enabling the extended view finder in bokeh mode.");
+
+            BokehViewFinderExtender extender = new BokehViewFinderExtender(builder);
+            if (extender.isExtensionAvailable()) {
+                extender.enableExtension();
+            }
+        } else if (mCurrentImageCaptureType == ImageCaptureType.IMAGE_CAPTURE_TYPE_HDR) {
+            Log.d(TAG, "Enabling the extended view finder in HDR mode.");
+
+            HdrViewFinderExtender extender = new HdrViewFinderExtender(builder);
+            if (extender.isExtensionAvailable()) {
+                extender.enableExtension();
+            }
+        }
+
+        mViewFinderUseCase = new ViewFinderUseCase(builder.build());
+
+        TextureView textureView = findViewById(R.id.textureView);
+
+        mViewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                new ViewFinderUseCase.OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderUseCase.ViewFinderOutput output) {
+                        // 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(output.getSurfaceTexture());
+                    }
+                });
+    }
+
+    enum ImageCaptureType {
+        IMAGE_CAPTURE_TYPE_HDR,
+        IMAGE_CAPTURE_TYPE_BOKEH,
+        IMAGE_CAPTURE_TYPE_DEFAULT,
+        IMAGE_CAPTURE_TYPE_NONE,
+    }
+
+    /**
+     * 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 = findViewById(R.id.PhotoToggle);
+        enableImageCaptureUseCase(mCurrentImageCaptureType);
+        button.setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        disableImageCaptureUseCase();
+                        // Toggle to next capture type and enable it and set it as current
+                        switch (mCurrentImageCaptureType) {
+                            case IMAGE_CAPTURE_TYPE_HDR:
+                                enableImageCaptureUseCase(
+                                        ImageCaptureType.IMAGE_CAPTURE_TYPE_BOKEH);
+                                enableViewFinderUseCase();
+                                break;
+                            case IMAGE_CAPTURE_TYPE_BOKEH:
+                                enableImageCaptureUseCase(
+                                        ImageCaptureType.IMAGE_CAPTURE_TYPE_DEFAULT);
+                                enableViewFinderUseCase();
+                                break;
+                            case IMAGE_CAPTURE_TYPE_DEFAULT:
+                                enableImageCaptureUseCase(ImageCaptureType.IMAGE_CAPTURE_TYPE_NONE);
+                                enableViewFinderUseCase();
+                                break;
+                            case IMAGE_CAPTURE_TYPE_NONE:
+                                enableImageCaptureUseCase(ImageCaptureType.IMAGE_CAPTURE_TYPE_HDR);
+                                enableViewFinderUseCase();
+                                break;
+                        }
+                        bindUseCases();
+                    }
+                });
+
+        Log.i(TAG, "Got UseCase: " + mImageCaptureUseCase);
+    }
+
+    void enableImageCaptureUseCase(ImageCaptureType imageCaptureType) {
+        mCurrentImageCaptureType = imageCaptureType;
+        ImageCaptureUseCaseConfiguration.Builder builder =
+                new ImageCaptureUseCaseConfiguration.Builder()
+                        .setLensFacing(LensFacing.BACK)
+                        .setTargetName("ImageCapture");
+        Button toggleButton = findViewById(R.id.PhotoToggle);
+        toggleButton.setText(mCurrentImageCaptureType.toString());
+
+        switch (imageCaptureType) {
+            case IMAGE_CAPTURE_TYPE_HDR:
+                HdrImageCaptureExtender hdrImageCaptureExtender = new HdrImageCaptureExtender(
+                        builder);
+                if (hdrImageCaptureExtender.isExtensionAvailable()) {
+                    hdrImageCaptureExtender.enableExtension();
+                }
+                break;
+            case IMAGE_CAPTURE_TYPE_BOKEH:
+                BokehImageCaptureExtender bokehImageCapture = new BokehImageCaptureExtender(
+                        builder);
+                if (bokehImageCapture.isExtensionAvailable()) {
+                    bokehImageCapture.enableExtension();
+                }
+                break;
+            case IMAGE_CAPTURE_TYPE_DEFAULT:
+                break;
+            case IMAGE_CAPTURE_TYPE_NONE:
+                return;
+        }
+
+        mImageCaptureUseCase = new ImageCaptureUseCase(builder.build());
+
+        Button captureButton = findViewById(R.id.Picture);
+
+        final Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US);
+
+        final File dir =
+                new File(
+                        Environment.getExternalStoragePublicDirectory(
+                                Environment.DIRECTORY_PICTURES),
+                        "ExtensionsPictures");
+        dir.mkdirs();
+        captureButton.setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        mImageCaptureUseCase.takePicture(
+                                new File(
+                                        dir,
+                                        formatter.format(Calendar.getInstance().getTime())
+                                                + mCurrentImageCaptureType.name()
+                                                + ".jpg"),
+                                new ImageCaptureUseCase.OnImageSavedListener() {
+                                    @Override
+                                    public void onImageSaved(File file) {
+                                        Log.d(TAG, "Saved image to " + file);
+
+                                        // Trigger MediaScanner to scan the file
+                                        Intent intent = new Intent(
+                                                Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+                                        intent.setData(Uri.fromFile(file));
+                                        sendBroadcast(intent);
+
+                                        Toast.makeText(getApplicationContext(),
+                                                "Saved image to " + file,
+                                                Toast.LENGTH_SHORT).show();
+                                    }
+
+                                    @Override
+                                    public void onError(
+                                            ImageCaptureUseCase.UseCaseError useCaseError,
+                                            String message,
+                                            Throwable cause) {
+                                        Log.e(TAG, "Failed to save image - " + message, cause);
+                                    }
+                                });
+                    }
+                });
+    }
+
+    void disableImageCaptureUseCase() {
+        if (mImageCaptureUseCase != null) {
+            CameraX.unbind(mImageCaptureUseCase);
+            mImageCaptureUseCase = null;
+        }
+
+        Button button = findViewById(R.id.Picture);
+        button.setOnClickListener(null);
+    }
+
+    /** Creates all the use cases. */
+    private void createUseCases() {
+        createImageCaptureUseCase();
+        createViewFinderUseCase();
+        bindUseCases();
+    }
+
+    private void bindUseCases() {
+        List<BaseUseCase> useCases = new ArrayList();
+        // When it is not IMAGE_CAPTURE_TYPE_NONE, mImageCaptureUseCase won't be null.
+        if (mImageCaptureUseCase != null) {
+            useCases.add(mImageCaptureUseCase);
+        }
+        useCases.add(mViewFinderUseCase);
+        CameraX.bindToLifecycle(this, useCases.toArray(new BaseUseCase[useCases.size()]));
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_camera_extensions);
+
+        StrictMode.VmPolicy policy =
+                new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
+        StrictMode.setVmPolicy(policy);
+
+        // Get params from adb extra string
+        Bundle bundle = getIntent().getExtras();
+        if (bundle != null) {
+            String newCameraFacing = bundle.getString("cameraFacing");
+            if (newCameraFacing != null) {
+                mCurrentCameraFacing = newCameraFacing;
+            }
+        }
+
+        new Thread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        setupCamera();
+                    }
+                })
+                .start();
+        setupPermissions();
+    }
+
+    private void setupCamera() {
+        try {
+            // Wait for permissions before proceeding.
+            if (!mCompletableFuture.get()) {
+                Log.d(TAG, "Permissions denied.");
+                return;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Exception occurred getting permission future: " + e);
+        }
+
+        try {
+            Log.d(TAG, "Camera Facing: " + mCurrentCameraFacing);
+            LensFacing facing = LensFacing.BACK;
+            if (mCurrentCameraFacing.equalsIgnoreCase("BACK")) {
+                facing = LensFacing.BACK;
+            } else if (mCurrentCameraFacing.equalsIgnoreCase("FRONT")) {
+                facing = LensFacing.FRONT;
+            } else {
+                throw new RuntimeException("Invalid lens facing: " + mCurrentCameraFacing);
+            }
+            mCurrentCameraId = CameraX.getCameraWithLensFacing(facing);
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to obtain camera with specified facing. " + e.getMessage());
+        }
+
+        Log.d(TAG, "Using cameraId: " + mCurrentCameraId);
+
+        // Run this on the UI thread to manipulate the Textures & Views.
+        CameraExtensionsActivity.this.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        createUseCases();
+                    }
+                });
+    }
+
+    private void setupPermissions() {
+        if (!allPermissionsGranted()) {
+            makePermissionRequest();
+        } else {
+            mSettableResult.set(true);
+            mCompletableFuture.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.");
+                    mSettableResult.set(true);
+                    mCompletableFuture.run();
+                } else {
+                    Log.d(TAG, "Permissions Denied.");
+                    mSettableResult.set(false);
+                    mCompletableFuture.run();
+                }
+                return;
+            }
+            default:
+                // No-op
+        }
+    }
+
+    /** A {@link Callable} whose return value can be set. */
+    private static final class SettableCallable<V> implements Callable<V> {
+        private final AtomicReference<V> mValue = new AtomicReference<>();
+
+        public void set(V value) {
+            mValue.set(value);
+        }
+
+        @Override
+        public V call() {
+            return mValue.get();
+        }
+    }
+}
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
new file mode 100644
index 0000000..9963603
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
@@ -0,0 +1,76 @@
+<?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.
+  -->
+
+<androidx.constraintlayout.widget.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.integration.extensions.CameraExtensionsActivity">
+
+    <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_constraintTop_toTopOf="parent"
+        app:layout_constraintStart_toStartOf="parent"/>
+
+    <androidx.constraintlayout.widget.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"/>
+
+    <androidx.constraintlayout.widget.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="Take Picture"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.415"
+        app:layout_constraintStart_toStartOf="@+id/takepicture"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="1.0"/>
+
+    <Button
+        android:id="@+id/PhotoToggle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:scaleType="fitXY"
+        android:text="DEFAULT"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@+id/constraintLayout"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.0"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/values/strings.xml b/camera/integration-tests/extensionstestapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..dd955d9
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/values/strings.xml
@@ -0,0 +1,18 @@
+<!--
+  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.
+  -->
+
+<resources>
+</resources>
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/values/style.xml b/camera/integration-tests/extensionstestapp/src/main/res/values/style.xml
new file mode 100644
index 0000000..dcaca1f
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/values/style.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+    </style>
+</resources>
diff --git a/camera/integration-tests/extensionstestlib/build.gradle b/camera/integration-tests/extensionstestlib/build.gradle
new file mode 100644
index 0000000..acc539a
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+// TODO(b/124783972): Switch to androidx.build.LibraryVersions and androidx.build.LibraryGroups when ready
+import androidx.build.UnpublishedLibraryVersions
+import androidx.build.UnpublishedLibraryGroups
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    api(project(":camera:camera-core"))
+    api(project(":camera:camera-camera2"))
+    api(project(":camera:camera-extensions"))
+    implementation("androidx.core:core:1.0.0")
+
+    implementation(GUAVA_LISTENABLE_FUTURE)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestlib/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestlib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..24ec9a7
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/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.extensions.impl"/>
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtender.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtender.java
new file mode 100644
index 0000000..60a9d37
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtender.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.extensions.impl;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.media.Image;
+import android.media.ImageWriter;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.camera.core.CaptureProcessor;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ImageProxyBundle;
+import androidx.camera.extensions.CaptureStage;
+import androidx.camera.extensions.ImageCaptureUseCaseExtender;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Implementation for bokeh image capture use case.
+ *
+ * <p>This class should be implemented by OEM and deployed to the target devices. 3P developers
+ * don't need to implement this, unless this is used for related testing usage.
+ */
+public class BokehImageCaptureExtender extends ImageCaptureUseCaseExtender {
+    private static final String TAG = "BokehICExtender";
+    private static final int DEFAULT_STAGE_ID = 0;
+
+    public BokehImageCaptureExtender(ImageCaptureUseCaseConfiguration.Builder builder) {
+        super(builder);
+    }
+
+    @Override
+    public void enableExtension() {
+        // 1. Sets necessary CaptureStage settings
+        CaptureStage captureStage = new CaptureStage(DEFAULT_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+
+        // 2. Sets CaptureProcess if necessary...
+        CaptureProcessor captureProcessor =
+                new CaptureProcessor() {
+                    @Override
+                    public void onOutputSurface(Surface surface, int imageFormat) {
+                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                            mImageWriter = ImageWriter.newInstance(surface, 1);
+                        }
+                    }
+
+                    @Override
+                    public void process(ImageProxyBundle bundle) {
+                        Log.d(TAG, "Started bokeh CaptureProcessor");
+
+                        ListenableFuture<ImageProxy> resultFuture = bundle.getImageProxy(
+                                DEFAULT_STAGE_ID);
+
+                        ImageProxy result = null;
+
+                        try {
+                            result = resultFuture.get(5, TimeUnit.SECONDS);
+
+                            Image image = null;
+                            if (android.os.Build.VERSION.SDK_INT
+                                    >= android.os.Build.VERSION_CODES.M) {
+                                image = mImageWriter.dequeueInputImage();
+
+                                // Do processing here
+                                ByteBuffer yByteBuffer = image.getPlanes()[0].getBuffer();
+                                ByteBuffer uByteBuffer = image.getPlanes()[2].getBuffer();
+                                ByteBuffer vByteBuffer = image.getPlanes()[1].getBuffer();
+
+                                // Sample here just simply copy/paste the capture image result
+                                yByteBuffer.put(result.getPlanes()[0].getBuffer());
+                                uByteBuffer.put(result.getPlanes()[2].getBuffer());
+                                vByteBuffer.put(result.getPlanes()[1].getBuffer());
+
+                                mImageWriter.queueInputImage(image);
+                            }
+                        } catch (ExecutionException | InterruptedException | TimeoutException e) {
+                            Log.e(TAG, "Failed to obtain result: " + e);
+                        } finally {
+                            result.close();
+                        }
+
+                        Log.d(TAG, "Completed bokeh CaptureProcessor");
+                    }
+
+                    ImageWriter mImageWriter;
+                };
+
+        setCaptureStages(Arrays.asList(captureStage));
+        setCaptureProcessor(captureProcessor);
+    }
+
+    @Override
+    public boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        // Implement the logic to check whether the extension function is supported or not.
+        return true;
+    }
+}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehViewFinderExtender.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehViewFinderExtender.java
new file mode 100644
index 0000000..89334d7
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehViewFinderExtender.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.extensions.impl;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.util.Log;
+
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.camera.extensions.CaptureStage;
+import androidx.camera.extensions.ViewFinderUseCaseExtender;
+
+/**
+ * Implementation for bokeh view finder use case.
+ *
+ * <p>This class should be implemented by OEM and deployed to the target devices. 3P developers
+ * don't need to implement this, unless this is used for related testing usage.
+ */
+public final class BokehViewFinderExtender extends ViewFinderUseCaseExtender {
+    private static final String TAG = "BokehViewFinderExtender";
+    private static final int DEFAULT_STAGE_ID = 0;
+
+    public BokehViewFinderExtender(ViewFinderUseCaseConfiguration.Builder builder) {
+        super(builder);
+    }
+
+    @Override
+    public void enableExtension() {
+        Log.d(TAG, "Adding effects to the view finder");
+        // Sets necessary CaptureRequest parameters via CaptureStage
+        CaptureStage captureStage = new CaptureStage(DEFAULT_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+        setCaptureStage(captureStage);
+    }
+
+    @Override
+    public boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        // Implement the logic to check whether the extension function is supported or not.
+        return true;
+    }
+}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtender.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtender.java
new file mode 100644
index 0000000..0cce141
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtender.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.extensions.impl;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.media.Image;
+import android.media.ImageWriter;
+import android.os.Build;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.camera.core.CaptureProcessor;
+import androidx.camera.core.ImageCaptureUseCaseConfiguration;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ImageProxyBundle;
+import androidx.camera.extensions.CaptureStage;
+import androidx.camera.extensions.ImageCaptureUseCaseExtender;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Implementation for HDR image capture use case.
+ *
+ * <p>This class should be implemented by OEM and deployed to the target devices. 3P developers
+ * don't need to implement this, unless this is used for related testing usage.
+ */
+public final class HdrImageCaptureExtender extends ImageCaptureUseCaseExtender {
+    private static final String TAG = "HdrImageCaptureExtender";
+    private static final int UNDER_STAGE_ID = 0;
+    private static final int NORMAL_STAGE_ID = 1;
+    private static final int OVER_STAGE_ID = 2;
+
+    public HdrImageCaptureExtender(ImageCaptureUseCaseConfiguration.Builder builder) {
+        super(builder);
+    }
+
+    @Override
+    public void enableExtension() {
+        Log.d(TAG, "Enabling HDR extension");
+        // 1. Sets necessary CaptureStage settings
+        // Under exposed capture stage
+        CaptureStage captureStageUnder = new CaptureStage(UNDER_STAGE_ID);
+        // Turn off AE so that ISO sensitivity can be controlled
+        captureStageUnder.addCaptureRequestParameters(CaptureRequest.CONTROL_AE_MODE,
+                CaptureRequest.CONTROL_AE_MODE_OFF);
+        captureStageUnder.addCaptureRequestParameters(CaptureRequest.SENSOR_EXPOSURE_TIME,
+                TimeUnit.MILLISECONDS.toNanos(8));
+
+        // Normal exposed capture stage
+        CaptureStage captureStageNormal = new CaptureStage(NORMAL_STAGE_ID);
+        captureStageNormal.addCaptureRequestParameters(CaptureRequest.SENSOR_EXPOSURE_TIME,
+                TimeUnit.MILLISECONDS.toNanos(16));
+
+        // Over exposed capture stage
+        CaptureStage captureStageOver = new CaptureStage(OVER_STAGE_ID);
+        captureStageOver.addCaptureRequestParameters(CaptureRequest.SENSOR_EXPOSURE_TIME,
+                TimeUnit.MILLISECONDS.toNanos(32));
+
+        // 2. Sets CaptureProcess if necessary...
+        CaptureProcessor captureProcessor =
+                new CaptureProcessor() {
+                    ImageWriter mImageWriter;
+
+                    @Override
+                    public void onOutputSurface(Surface surface, int imageFormat) {
+                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                            mImageWriter = ImageWriter.newInstance(surface, 1);
+                        }
+                    }
+
+                    @Override
+                    public void process(ImageProxyBundle bundle) {
+                        Log.d(TAG, "Started HDR CaptureProcessor");
+
+                        ListenableFuture<ImageProxy> underResultFuture = bundle.getImageProxy(
+                                UNDER_STAGE_ID);
+                        ListenableFuture<ImageProxy> normalResultFuture = bundle.getImageProxy(
+                                NORMAL_STAGE_ID);
+                        ListenableFuture<ImageProxy> overResultFuture = bundle.getImageProxy(
+                                OVER_STAGE_ID);
+
+                        List<ImageProxy> results = new ArrayList<>();
+
+                        try {
+                            results.add(underResultFuture.get(5, TimeUnit.SECONDS));
+                            results.add(normalResultFuture.get(5, TimeUnit.SECONDS));
+                            results.add(overResultFuture.get(5, TimeUnit.SECONDS));
+
+                            Image image = null;
+                            if (android.os.Build.VERSION.SDK_INT
+                                    >= android.os.Build.VERSION_CODES.M) {
+                                image = mImageWriter.dequeueInputImage();
+
+                                // Do processing here
+                                ByteBuffer yByteBuffer = image.getPlanes()[0].getBuffer();
+                                ByteBuffer uByteBuffer = image.getPlanes()[2].getBuffer();
+                                ByteBuffer vByteBuffer = image.getPlanes()[1].getBuffer();
+
+                                // Sample here just simply return the normal image result
+                                yByteBuffer.put(results.get(1).getPlanes()[0].getBuffer());
+                                uByteBuffer.put(results.get(1).getPlanes()[2].getBuffer());
+                                vByteBuffer.put(results.get(1).getPlanes()[1].getBuffer());
+
+                                mImageWriter.queueInputImage(image);
+                            }
+                        } catch (ExecutionException | InterruptedException | TimeoutException e) {
+                            Log.e(TAG, "Failed to obtain result: " + e);
+                        } finally {
+
+                            for (ImageProxy imageProxy : results) {
+                                imageProxy.close();
+                            }
+                        }
+
+                        Log.d(TAG, "Completed HDR CaptureProcessor");
+                    }
+                };
+
+        setCaptureStages(Arrays.asList(captureStageUnder, captureStageNormal, captureStageOver));
+        setCaptureProcessor(captureProcessor);
+    }
+
+    @Override
+    public boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        // Implement the logic to check whether the extension function is supported or not.
+        return true;
+    }
+}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrViewFinderExtender.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrViewFinderExtender.java
new file mode 100644
index 0000000..a860e14
--- /dev/null
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrViewFinderExtender.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions.impl;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CaptureRequest;
+import android.util.Log;
+
+import androidx.camera.core.ViewFinderUseCaseConfiguration;
+import androidx.camera.extensions.CaptureStage;
+import androidx.camera.extensions.ViewFinderUseCaseExtender;
+
+/**
+ * Implementation for HDR view finder use case.
+ *
+ * <p>This class should be implemented by OEM and deployed to the target devices. 3P developers
+ * don't need to implement this, unless this is used for related testing usage.
+ */
+public final class HdrViewFinderExtender extends ViewFinderUseCaseExtender {
+    private static final String TAG = "HdrViewFinderExtender";
+    private static final int DEFAULT_STAGE_ID = 0;
+
+    public HdrViewFinderExtender(ViewFinderUseCaseConfiguration.Builder builder) {
+        super(builder);
+    }
+
+    @Override
+    public void enableExtension() {
+        Log.d(TAG, "Adding effects to the view finder");
+        // Sets necessary CaptureRequest parameters via CaptureStage
+        CaptureStage captureStage = new CaptureStage(DEFAULT_STAGE_ID);
+        captureStage.addCaptureRequestParameters(CaptureRequest.CONTROL_EFFECT_MODE,
+                CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
+        setCaptureStage(captureStage);
+    }
+
+    @Override
+    public boolean isExtensionAvailable(String cameraId,
+            CameraCharacteristics cameraCharacteristics) {
+        // Implement the logic to check whether the extension function is supported or not.
+        return true;
+    }
+}
diff --git a/camera/integration-tests/timingtestapp/build.gradle b/camera/integration-tests/timingtestapp/build.gradle
new file mode 100644
index 0000000..3aa3257
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/build.gradle
@@ -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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.camera.integration.timing"
+        minSdkVersion 21
+        versionCode 1
+        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 {
+        }
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation(project(":camera:camera-camera2"))
+
+    // Android Support Library
+    api(CONSTRAINT_LAYOUT, { transitive = true })
+    implementation(project(":appcompat"))
+
+    // Guava
+    implementation(GUAVA_ANDROID)
+}
+
diff --git a/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..921b5cd
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?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.integration.timing">
+    <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/timingtestapp/src/main/java/androidx/camera/integration/timing/BaseActivity.java b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/BaseActivity.java
new file mode 100644
index 0000000..9ca2686
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/BaseActivity.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.integration.timing;
+
+import android.os.Bundle;
+
+import androidx.appcompat.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;
+
+    /**
+     * Prepares the use case.
+     */
+    public abstract void prepareUseCase();
+
+    /**
+     * Activates use case so it will receive data from camera.
+     *
+     * @throws InterruptedException on fatal errors.
+     */
+    public abstract void runUseCase() throws InterruptedException;
+
+    /**
+     * Called when the test case finishes.
+     * <p>Could be called in CameraDevice's state callbacks.
+     */
+    public void onUseCaseFinish() {
+        latch.countDown();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        latch = new CountDownLatch(1);
+    }
+}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/CustomLifecycle.java b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/CustomLifecycle.java
new file mode 100644
index 0000000..fd5308a
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/CustomLifecycle.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.integration.timing;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/** A customized lifecycle owner which obeys the lifecycle transition rules. */
+public final class CustomLifecycle implements LifecycleOwner {
+    private final LifecycleRegistry mLifecycleRegistry;
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+    public CustomLifecycle() {
+        mLifecycleRegistry = new LifecycleRegistry(this);
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.INITIALIZED);
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
+    }
+
+    @NonNull
+    @Override
+    public Lifecycle getLifecycle() {
+        return mLifecycleRegistry;
+    }
+
+    /**
+     * Called when activity resumes.
+     */
+    public void doOnResume() {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mMainHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    CustomLifecycle.this.doOnResume();
+                }
+            });
+            return;
+        }
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
+    }
+
+    /**
+     * Called when activity is destroyed.
+     */
+    public void doDestroyed() {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            mMainHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    CustomLifecycle.this.doDestroyed();
+                }
+            });
+            return;
+        }
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+    }
+}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/TakePhotoActivity.java b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/TakePhotoActivity.java
new file mode 100644
index 0000000..9b7345a
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/TakePhotoActivity.java
@@ -0,0 +1,282 @@
+/*
+ * 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.integration.timing;
+
+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.View;
+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 mDefaultCameraFacing = CAMERA_FACING_BACK;
+    private final CameraDevice.StateCallback mDeviceStateCallback =
+            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 mCaptureSessionStateCallback =
+            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 mCurrentCameraLensFacing = LensFacing.BACK;
+    private ImageCaptureUseCase mImageCaptureUseCase;
+    private ViewFinderUseCase mViewFinderUseCase;
+    private int mFrameCount;
+    private long mPreviewSampleStartTime;
+    private CaptureMode mCaptureMode = CaptureMode.MIN_LATENCY;
+    private CustomLifecycle mCustomLifecycle;
+
+    @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();
+        mImageCaptureUseCase.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(mCurrentCameraLensFacing)
+                        .setTargetName("ViewFinder");
+
+        new Camera2Configuration.Extender(configurationBuilder)
+                .setDeviceStateCallback(mDeviceStateCallback)
+                .setSessionStateCallback(mCaptureSessionStateCallback);
+
+        mViewFinderUseCase = new ViewFinderUseCase(configurationBuilder.build());
+        openCameraStartTime = System.currentTimeMillis();
+
+        mViewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                new ViewFinderUseCase.OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderUseCase.ViewFinderOutput viewFinderOutput) {
+                        TextureView textureView = TakePhotoActivity.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 == mFrameCount) {
+                                            mPreviewSampleStartTime = System.currentTimeMillis();
+                                        } else if (FRAMERATE_SAMPLE_WINDOW == mFrameCount) {
+                                            final long duration =
+                                                    System.currentTimeMillis()
+                                                            - mPreviewSampleStartTime;
+                                            previewFrameRate =
+                                                    (MICROS_IN_SECOND
+                                                            * FRAMERATE_SAMPLE_WINDOW
+                                                            / duration);
+                                            closeCameraStartTime = System.currentTimeMillis();
+                                            mCustomLifecycle.doDestroyed();
+                                        }
+                                        mFrameCount++;
+                                    }
+                                });
+                    }
+                });
+
+        CameraX.bindToLifecycle(mCustomLifecycle, mViewFinderUseCase);
+    }
+
+    void createImageCaptureUseCase() {
+        ImageCaptureUseCaseConfiguration configuration =
+                new ImageCaptureUseCaseConfiguration.Builder()
+                        .setTargetName("ImageCapture")
+                        .setLensFacing(mCurrentCameraLensFacing)
+                        .setCaptureMode(mCaptureMode)
+                        .build();
+
+        mImageCaptureUseCase = new ImageCaptureUseCase(configuration);
+        CameraX.bindToLifecycle(mCustomLifecycle, mImageCaptureUseCase);
+
+        final Button button = this.findViewById(R.id.Picture);
+        button.setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View view) {
+                        startTime = System.currentTimeMillis();
+                        mImageCaptureUseCase.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) {
+                mCaptureMode = CaptureMode.valueOf(Ascii.toUpperCase(captureModeString));
+            }
+            final String cameraLensFacing = bundle.getString(EXTRA_CAMERA_FACING);
+            if (cameraLensFacing != null) {
+                setupCamera(cameraLensFacing);
+            } else {
+                setupCamera(mDefaultCameraFacing);
+            }
+        }
+        mCustomLifecycle = new CustomLifecycle();
+        prepareUseCase();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mCustomLifecycle.doOnResume();
+    }
+
+    void setupCamera(String cameraFacing) {
+        Log.d(TAG, "Camera Facing: " + cameraFacing);
+        if (Ascii.equalsIgnoreCase(cameraFacing, CAMERA_FACING_BACK)) {
+            mCurrentCameraLensFacing = LensFacing.BACK;
+        } else if (Ascii.equalsIgnoreCase(cameraFacing, CAMERA_FACING_FRONT)) {
+            mCurrentCameraLensFacing = LensFacing.FRONT;
+        } else {
+            throw new RuntimeException("Invalid lens facing: " + cameraFacing);
+        }
+    }
+}
diff --git a/camera/integration-tests/timingtestapp/src/main/res/layout/activity_main.xml b/camera/integration-tests/timingtestapp/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..3dc76a9
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/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.
+-->
+<androidx.constraintlayout.widget.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.integration.timing">
+
+    <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" />
+
+    <androidx.constraintlayout.widget.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" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/camera/integration-tests/timingtestapp/src/main/res/values/strings.xml b/camera/integration-tests/timingtestapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2df0565
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/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/timingtestapp/src/main/res/values/style.xml b/camera/integration-tests/timingtestapp/src/main/res/values/style.xml
new file mode 100644
index 0000000..7503cc0
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/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/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
new file mode 100644
index 0000000..0076c3d
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+}
+
+android {
+    defaultConfig {
+        applicationId "androidx.camera.integration.view"
+        minSdkVersion 21
+        versionCode 1
+        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 {
+        }
+    }
+}
+
+dependencies {
+    // Internal library
+    implementation(project(":camera:camera-camera2"))
+    implementation(project(":camera:camera-view"))
+
+    // Lifecycle and LiveData
+    implementation(project(":lifecycle:lifecycle-livedata"))
+
+    // Android Support Library
+    implementation(project(":appcompat"))
+}
+
diff --git a/camera/integration-tests/viewtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/viewtestapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7e8333b
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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.integration.view">
+    <!-- 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/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java
new file mode 100644
index 0000000..56b762e
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/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.integration.view;
+
+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 = "ViewOnTouchListener";
+
+    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 mLongPress = ViewConfiguration.getLongPressTimeout();
+    private final CameraView mCameraView;
+
+    // TODO: Use a Handler for a background thread, rather than running on the current (main)
+    // thread.
+    private final Handler mHandler =
+            new Handler() {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                        case TAP:
+                            onTap();
+                            break;
+                        case HOLD:
+                            onHold();
+                            if (mCameraView.getMaxVideoDuration() > 0) {
+                                sendEmptyMessageDelayed(RELEASE, mCameraView.getMaxVideoDuration());
+                            }
+                            break;
+                        case RELEASE:
+                            onRelease();
+                            break;
+                        default:
+                            // No op
+                    }
+                }
+            };
+
+    private long mDownEventTimestamp;
+    private Rect mViewBoundsRect;
+
+    /** Creates a new listener which links to the given {@link CameraView}. */
+    CaptureViewOnTouchListener(CameraView cameraView) {
+        mCameraView = cameraView;
+    }
+
+    /** Called when the user taps. */
+    void onTap() {
+        if (mCameraView.getCaptureMode() == CaptureMode.IMAGE
+                || mCameraView.getCaptureMode() == CaptureMode.MIXED) {
+            mCameraView.takePicture(createNewFile(PHOTO_EXTENSION), this);
+        }
+    }
+
+    /** Called when the user holds (long presses). */
+    void onHold() {
+        if (mCameraView.getCaptureMode() == CaptureMode.VIDEO
+                || mCameraView.getCaptureMode() == CaptureMode.MIXED) {
+            mCameraView.startRecording(createNewFile(VIDEO_EXTENSION), this);
+        }
+    }
+
+    /** Called when the user releases. */
+    void onRelease() {
+        if (mCameraView.getCaptureMode() == CaptureMode.VIDEO
+                || mCameraView.getCaptureMode() == CaptureMode.MIXED) {
+            mCameraView.stopRecording();
+        }
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mDownEventTimestamp = System.currentTimeMillis();
+                mViewBoundsRect =
+                        new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+                mHandler.sendEmptyMessageDelayed(HOLD, mLongPress);
+                view.setPressed(true);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // If the user moves their finger off the button, trigger RELEASE
+                if (mViewBoundsRect.contains(
+                        view.getLeft() + (int) event.getX(), view.getTop() + (int) event.getY())) {
+                    break;
+                }
+                // Fall-through
+            case MotionEvent.ACTION_CANCEL:
+                clearHandler();
+                if (deltaSinceDownEvent() > mLongPress
+                        && (mCameraView.getMaxVideoDuration() <= 0
+                        || deltaSinceDownEvent() < mCameraView.getMaxVideoDuration())) {
+                    mHandler.sendEmptyMessage(RELEASE);
+                }
+                view.setPressed(false);
+                break;
+            case MotionEvent.ACTION_UP:
+                clearHandler();
+                if (deltaSinceDownEvent() < mLongPress) {
+                    mHandler.sendEmptyMessage(TAP);
+                } else if ((mCameraView.getMaxVideoDuration() <= 0
+                        || deltaSinceDownEvent() < mCameraView.getMaxVideoDuration())) {
+                    mHandler.sendEmptyMessage(RELEASE);
+                }
+                view.setPressed(false);
+                break;
+            default:
+                // No op
+        }
+        return true;
+    }
+
+    private long deltaSinceDownEvent() {
+        return System.currentTimeMillis() - mDownEventTimestamp;
+    }
+
+    private void clearHandler() {
+        mHandler.removeMessages(TAP);
+        mHandler.removeMessages(HOLD);
+        mHandler.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(mCameraView.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));
+            mCameraView.getContext().sendBroadcast(intent);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+            intent.setData(Uri.fromFile(file));
+            mCameraView.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));
+            mCameraView.getContext().sendBroadcast(intent);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+            intent.setData(Uri.fromFile(file));
+            mCameraView.getContext().sendBroadcast(intent);
+        }
+    }
+}
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
new file mode 100644
index 0000000..be81c2d
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.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.integration.view;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+
+/** 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 mCheckedPermissions = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        if (null == savedInstanceState) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                if (allPermissionsGranted()) {
+                    startCamera();
+                } else if (!mCheckedPermissions) {
+                    requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
+                    mCheckedPermissions = true;
+                }
+            } else {
+                startCamera();
+            }
+        }
+    }
+
+    @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/viewtestapp/src/main/java/androidx/camera/integration/view/MainFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainFragment.java
new file mode 100644
index 0000000..f74bc7c
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainFragment.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.integration.view;
+
+import android.content.pm.PackageManager;
+import android.graphics.Rect;
+import android.os.Bundle;
+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 androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+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 mCameraHolder;
+    private CameraView mCameraView;
+    private View mCaptureView;
+    private CompoundButton mModeButton;
+    @Nullable
+    private CompoundButton mToggleCameraButton;
+    private CompoundButton mToggleCropButton;
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        mCameraHolder = view.findViewById(R.id.layout_camera);
+        mCameraView = view.findViewById(R.id.camera);
+        mToggleCameraButton = view.findViewById(R.id.toggle);
+        mToggleCropButton = view.findViewById(R.id.toggle_crop);
+        mCaptureView = mCameraHolder.findViewById(R.id.capture);
+        if (mCameraHolder == null) {
+            throw new IllegalStateException("No View found with id R.id.layout_camera");
+        }
+        if (mCameraView == null) {
+            throw new IllegalStateException("No CameraView found with id R.id.camera");
+        }
+        if (mCaptureView == null) {
+            throw new IllegalStateException("No CameraView found with id R.id.capture");
+        }
+
+        mModeButton = mCameraHolder.findViewById(R.id.mode);
+
+        if (mModeButton == 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(mCameraView, "camera_view");
+        logCenterCoordinates(mCaptureView, "capture");
+        logCenterCoordinates(mToggleCameraButton, "toggle_camera");
+        logCenterCoordinates(mToggleCropButton, "toggle_crop");
+        logCenterCoordinates(mModeButton, "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());
+                mCameraView.setCameraByLensFacing(lensFacing);
+            }
+
+            String captureModeString = bundle.getString(INTENT_EXTRA_CAPTURE_MODE);
+            if (captureModeString != null) {
+                CaptureMode captureMode = CaptureMode.valueOf(captureModeString.toUpperCase());
+                mCameraView.setCaptureMode(captureMode);
+            }
+        }
+    }
+
+    @Override
+    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
+        super.onViewStateRestored(savedInstanceState);
+
+        if (ContextCompat.checkSelfPermission(getContext(), android.Manifest.permission.CAMERA)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new IllegalStateException("App has not been granted CAMERA permission");
+        }
+
+        // Set the lifecycle that will be used to control the camera
+        mCameraView.bindToLifecycle(getActivity());
+
+        mCameraView.setPinchToZoomEnabled(true);
+        mCaptureView.setOnTouchListener(new CaptureViewOnTouchListener(mCameraView));
+
+        // Set clickable, Let the cameraView can be interacted by Voice Access
+        mCameraView.setClickable(true);
+
+        if (mToggleCameraButton != null) {
+            mToggleCameraButton.setVisibility(
+                    (mCameraView.hasCameraWithLensFacing(LensFacing.BACK)
+                            && mCameraView.hasCameraWithLensFacing(LensFacing.FRONT))
+                            ? View.VISIBLE
+                            : View.INVISIBLE);
+            mToggleCameraButton.setChecked(mCameraView.getCameraLensFacing() == LensFacing.FRONT);
+        }
+
+        // Set listeners here, or else restoring state will trigger them.
+        if (mToggleCameraButton != null) {
+            mToggleCameraButton.setOnCheckedChangeListener(
+                    new CompoundButton.OnCheckedChangeListener() {
+                        @Override
+                        public void onCheckedChanged(CompoundButton b, boolean checked) {
+                            mCameraView.setCameraByLensFacing(
+                                    checked ? LensFacing.FRONT : LensFacing.BACK);
+                        }
+                    });
+        }
+
+        mToggleCropButton.setChecked(mCameraView.getScaleType() == ScaleType.CENTER_CROP);
+        mToggleCropButton.setOnCheckedChangeListener(
+                new CompoundButton.OnCheckedChangeListener() {
+                    @Override
+                    public void onCheckedChanged(CompoundButton b, boolean checked) {
+                        if (checked) {
+                            mCameraView.setScaleType(ScaleType.CENTER_CROP);
+                        } else {
+                            mCameraView.setScaleType(ScaleType.CENTER_INSIDE);
+                        }
+                    }
+                });
+
+        if (mModeButton != null) {
+            updateModeButtonIcon();
+
+            mModeButton.setOnClickListener(
+                    new View.OnClickListener() {
+                        @Override
+                        public void onClick(View view) {
+                            if (mCameraView.isRecording()) {
+                                Toast.makeText(
+                                        MainFragment.this.getContext(),
+                                        "Can not switch mode during video recording.",
+                                        Toast.LENGTH_SHORT)
+                                        .show();
+                                return;
+                            }
+
+                            if (mCameraView.getCaptureMode() == CaptureMode.MIXED) {
+                                mCameraView.setCaptureMode(CaptureMode.IMAGE);
+                            } else if (mCameraView.getCaptureMode() == CaptureMode.IMAGE) {
+                                mCameraView.setCaptureMode(CaptureMode.VIDEO);
+                            } else {
+                                mCameraView.setCaptureMode(CaptureMode.MIXED);
+                            }
+
+                            MainFragment.this.updateModeButtonIcon();
+                        }
+                    });
+        }
+    }
+
+    private void updateModeButtonIcon() {
+        if (mCameraView.getCaptureMode() == CaptureMode.MIXED) {
+            mModeButton.setButtonDrawable(R.drawable.ic_photo_camera);
+        } else if (mCameraView.getCaptureMode() == CaptureMode.IMAGE) {
+            mModeButton.setButtonDrawable(R.drawable.ic_camera);
+        } else {
+            mModeButton.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/viewtestapp/src/main/res/drawable/fullscreen_selector.xml b/camera/integration-tests/viewtestapp/src/main/res/drawable/fullscreen_selector.xml
new file mode 100644
index 0000000..ff39715
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/drawable/ic_camera.xml b/camera/integration-tests/viewtestapp/src/main/res/drawable/ic_camera.xml
new file mode 100644
index 0000000..a35682e
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/drawable/ic_fullscreen.xml b/camera/integration-tests/viewtestapp/src/main/res/drawable/ic_fullscreen.xml
new file mode 100644
index 0000000..467ce90
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/drawable/ic_fullscreen_exit.xml b/camera/integration-tests/viewtestapp/src/main/res/drawable/ic_fullscreen_exit.xml
new file mode 100644
index 0000000..8ee9dfc
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/drawable/ic_photo_camera.xml b/camera/integration-tests/viewtestapp/src/main/res/drawable/ic_photo_camera.xml
new file mode 100644
index 0000000..625e155
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/drawable/ic_videocam.xml b/camera/integration-tests/viewtestapp/src/main/res/drawable/ic_videocam.xml
new file mode 100644
index 0000000..f009891
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/layout-land/fragment_main.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml
new file mode 100644
index 0000000..ee0b902
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/layout/activity_main.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..a88437d0
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/layout/fragment_main.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/fragment_main.xml
new file mode 100644
index 0000000..72f8ecb
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/values/ids.xml b/camera/integration-tests/viewtestapp/src/main/res/values/ids.xml
new file mode 100644
index 0000000..91326df
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/values/strings.xml b/camera/integration-tests/viewtestapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ca95f1a
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/viewtestapp/src/main/res/values/styles.xml b/camera/integration-tests/viewtestapp/src/main/res/values/styles.xml
new file mode 100644
index 0000000..2d26a15
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/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/testing/build.gradle b/camera/testing/build.gradle
new file mode 100644
index 0000000..b64c36b
--- /dev/null
+++ b/camera/testing/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+// TODO(b/124783972): Switch to androidx.build.LibraryVersions and androidx.build.LibraryGroups when ready
+import androidx.build.UnpublishedLibraryVersions
+import androidx.build.UnpublishedLibraryGroups
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    implementation(TEST_CORE)
+    implementation("androidx.lifecycle:lifecycle-common:2.0.0", libs.exclude_annotations_transitive)
+    implementation(ARCH_LIFECYCLE_EXTENSIONS, libs.exclude_annotations_transitive)
+    implementation("androidx.annotation:annotation:1.0.0")
+    implementation(project(":camera:camera-core"))
+}
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+}
+supportLibrary {
+    name = "Jetpack Camera Testing Library"
+    publish = true
+    mavenVersion = UnpublishedLibraryVersions.CAMERA
+    mavenGroup = UnpublishedLibraryGroups.CAMERA
+    inceptionYear = "2019"
+    description = "Testing components for the Jetpack Camera Library, a library providing a " +
+            "consistent and reliable camera foundation that enables great camera driven " +"" +
+            "experiences across all of Android."
+}
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/CameraUtil.java b/camera/testing/src/main/java/androidx/camera/testing/CameraUtil.java
new file mode 100644
index 0000000..2ab7516
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/CameraUtil.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.testing;
+
+import android.Manifest;
+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.annotation.RequiresPermission;
+import androidx.camera.core.BaseCamera;
+import androidx.camera.core.BaseUseCase;
+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
+     */
+    @RequiresPermission(Manifest.permission.CAMERA)
+    public static CameraDevice getCameraDevice()
+            throws CameraAccessException, InterruptedException {
+        // Setup threading required for callback on openCamera()
+        final 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.
+        final AtomicReference<CameraDevice> cameraDeviceHolder = new AtomicReference<>();
+
+        // Open the camera using the CameraManager which returns a valid and open CameraDevice only
+        // when onOpened() is called.
+        final 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 cameraId to open
+     * @param camera   to open
+     * @param useCases to associate with
+     */
+    public static void openCameraWithUseCase(String cameraId, BaseCamera camera,
+            BaseUseCase... useCases) {
+        camera.addOnlineUseCase(Arrays.asList(useCases));
+        for (BaseUseCase useCase : useCases) {
+            useCase.attachCameraControl(cameraId, camera.getCameraControl());
+            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/testing/src/main/java/androidx/camera/testing/StreamConfigurationMapUtil.java b/camera/testing/src/main/java/androidx/camera/testing/StreamConfigurationMapUtil.java
new file mode 100644
index 0000000..b830de9
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/StreamConfigurationMapUtil.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.testing;
+
+import android.graphics.ImageFormat;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.util.Size;
+
+import androidx.camera.core.ImageFormatConstants;
+
+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/testing/src/main/java/androidx/camera/testing/fakes/FakeActivity.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeActivity.java
new file mode 100644
index 0000000..3becf04
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeActivity.java
@@ -0,0 +1,38 @@
+/*
+ * 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.app.Activity;
+import android.os.Bundle;
+
+import androidx.camera.core.CameraX;
+
+/** A fake {@link Activity} that checks properties of the CameraX library. */
+public class FakeActivity extends Activity {
+    private volatile boolean mIsCameraXInitializedAtOnCreate = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstance) {
+        super.onCreate(savedInstance);
+        mIsCameraXInitializedAtOnCreate = CameraX.isInitialized();
+    }
+
+    /** Returns true if CameraX is initialized when {@link #onCreate(Bundle)} is called. */
+    public boolean isCameraXInitializedAtOnCreate() {
+        return mIsCameraXInitializedAtOnCreate;
+    }
+}
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..06e33f80
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfiguration.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 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.CameraX;
+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.
+ * @hide Hidden until {@link CameraX#init(Context, AppConfiguration)} is public.
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeAppConfiguration {
+
+    /** Generates a fake {@link androidx.camera.core.AppConfiguration}. */
+    public static 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..7c2eda7
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -0,0 +1,99 @@
+/*
+ * 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 androidx.camera.core.SessionConfiguration;
+
+import java.util.Collection;
+
+/** A fake camera which will not produce any data. */
+public class FakeCamera implements BaseCamera {
+    private final CameraControl mCameraControl;
+
+    private final CameraInfo mCameraInfo;
+
+    public FakeCamera() {
+        this(new FakeCameraInfo(), CameraControl.DEFAULT_EMPTY_INSTANCE);
+    }
+
+    public FakeCamera(CameraInfo cameraInfo, CameraControl cameraControl) {
+        mCameraInfo = cameraInfo;
+        mCameraControl = cameraControl;
+    }
+
+    @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) {
+    }
+
+    // Returns fixed CameraControl instance in order to verify the instance is correctly attached.
+    @Override
+    public CameraControl getCameraControl() {
+        return mCameraControl;
+    }
+
+    @Override
+    public CameraInfo getCameraInfo() {
+        return mCameraInfo;
+    }
+
+    @Override
+    public void onCameraControlUpdateSessionConfiguration(
+            SessionConfiguration sessionConfiguration) {
+    }
+
+    @Override
+    public void onCameraControlSingleRequest(
+            CaptureRequestConfiguration captureRequestConfiguration) {
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCaptureResult.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCaptureResult.java
new file mode 100644
index 0000000..60ade96
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraCaptureResult.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CameraCaptureMetaData;
+import androidx.camera.core.CameraCaptureResult;
+
+/**
+ * A fake implementation of {@link CameraCaptureResult} where the values are settable.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeCameraCaptureResult implements CameraCaptureResult {
+    private CameraCaptureMetaData.AfMode mAfMode = CameraCaptureMetaData.AfMode.UNKNOWN;
+    private CameraCaptureMetaData.AfState mAfState = CameraCaptureMetaData.AfState.UNKNOWN;
+    private CameraCaptureMetaData.AeState mAeState = CameraCaptureMetaData.AeState.UNKNOWN;
+    private CameraCaptureMetaData.AwbState mAwbState = CameraCaptureMetaData.AwbState.UNKNOWN;
+    private CameraCaptureMetaData.FlashState mFlashState = CameraCaptureMetaData.FlashState.UNKNOWN;
+    private long mTimestamp = -1L;
+    private Object mTag = null;
+
+    public void setAfMode(CameraCaptureMetaData.AfMode mode) {
+        mAfMode = mode;
+    }
+
+    public void setAfState(CameraCaptureMetaData.AfState state) {
+        mAfState = state;
+    }
+
+    public void setAeState(CameraCaptureMetaData.AeState state) {
+        mAeState = state;
+    }
+
+    public void setAwbState(CameraCaptureMetaData.AwbState state) {
+        mAwbState = state;
+    }
+
+    public void setFlashState(CameraCaptureMetaData.FlashState state) {
+        mFlashState = state;
+    }
+
+    public void setTimestamp(long timestamp) {
+        mTimestamp = timestamp;
+    }
+
+    public void setTag(Object tag) {
+        mTag = tag;
+    }
+
+    @NonNull
+    @Override
+    public CameraCaptureMetaData.AfMode getAfMode() {
+        return mAfMode;
+    }
+
+    @NonNull
+    @Override
+    public CameraCaptureMetaData.AfState getAfState() {
+        return mAfState;
+    }
+
+    @NonNull
+    @Override
+    public CameraCaptureMetaData.AeState getAeState() {
+        return mAeState;
+    }
+
+    @NonNull
+    @Override
+    public CameraCaptureMetaData.AwbState getAwbState() {
+        return mAwbState;
+    }
+
+    @NonNull
+    @Override
+    public CameraCaptureMetaData.FlashState getFlashState() {
+        return mFlashState;
+    }
+
+    @NonNull
+    @Override
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
+    @Override
+    public Object getTag() {
+        return mTag;
+    }
+}
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..fa7e7f9
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraFactory.java
@@ -0,0 +1,101 @@
+/*
+ * 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 Set<String> mCameraIds;
+
+    private final Map<String, BaseCamera> mCameraMap = new HashMap<>();
+
+    public FakeCameraFactory() {
+        HashSet<String> camIds = new HashSet<>();
+        camIds.add(BACK_ID);
+        camIds.add(FRONT_ID);
+
+        mCameraIds = Collections.unmodifiableSet(camIds);
+    }
+
+    @Override
+    public BaseCamera getCamera(String cameraId) {
+        if (mCameraIds.contains(cameraId)) {
+            BaseCamera camera = mCameraMap.get(cameraId);
+            if (camera == null) {
+                camera = new FakeCamera();
+                mCameraMap.put(cameraId, camera);
+            }
+            return camera;
+        }
+        throw new IllegalArgumentException("Unknown camera: " + cameraId);
+    }
+
+    /**
+     * Inserts a camera with the given camera ID.
+     *
+     * @param cameraId Identifier to use for the camera.
+     * @param camera Camera implementation.
+     */
+    public void insertCamera(String cameraId, BaseCamera camera) {
+        if (!mCameraIds.contains(cameraId)) {
+            HashSet<String> newCameraIds = new HashSet<>(mCameraIds);
+            newCameraIds.add(cameraId);
+            mCameraIds = Collections.unmodifiableSet(newCameraIds);
+        }
+
+        mCameraMap.put(cameraId, camera);
+    }
+
+    @Override
+    public Set<String> getAvailableCameraIds() {
+        return mCameraIds;
+    }
+
+    @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..62d44be
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfo.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.testing.fakes;
+
+import android.view.Surface;
+
+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.
+ */
+public class FakeCameraInfo implements CameraInfo {
+
+    private final int mSensorRotation;
+    private final LensFacing mLensFacing;
+
+    public FakeCameraInfo() {
+        this(/*sensorRotation=*/ 0, /*lensFacing=*/ LensFacing.BACK);
+    }
+
+    public FakeCameraInfo(int sensorRotation, LensFacing lensFacing) {
+        mSensorRotation = sensorRotation;
+        mLensFacing = lensFacing;
+    }
+
+    @Nullable
+    @Override
+    public LensFacing getLensFacing() {
+        return mLensFacing;
+    }
+
+    @Override
+    public int getSensorRotationDegrees(@RotationValue int relativeRotation) {
+        return mSensorRotation;
+    }
+
+    @Override
+    public int getSensorRotationDegrees() {
+        return getSensorRotationDegrees(Surface.ROTATION_0);
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCaptureStage.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCaptureStage.java
new file mode 100644
index 0000000..e298d8f
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeCaptureStage.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.camera.core.CaptureRequestConfiguration;
+import androidx.camera.core.CaptureStage;
+
+/**
+ * A fake {@link CaptureStage} where the values can be set.
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public class FakeCaptureStage implements CaptureStage {
+
+    private final int mId;
+    private final CaptureRequestConfiguration mCaptureRequestConfiguration;
+
+    /** Create a FakeCaptureStage with the given parameters. */
+    public FakeCaptureStage(int id, CaptureRequestConfiguration captureRequestConfiguration) {
+        mId = id;
+        mCaptureRequestConfiguration = captureRequestConfiguration;
+    }
+
+    @Override
+    public int getId() {
+        return mId;
+    }
+
+    @Override
+    public CaptureRequestConfiguration getCaptureRequestConfiguration() {
+        return mCaptureRequestConfiguration;
+    }
+}
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..7818126
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeConfiguration.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.testing.fakes;
+
+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.Set;
+
+/**
+ * Wrapper for an empty Configuration
+ *
+ * @hide
+ */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class FakeConfiguration implements Configuration.Reader {
+
+    private final Configuration mConfig;
+
+    FakeConfiguration(Configuration config) {
+        mConfig = config;
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return mConfig;
+    }
+
+    /** Builder for an empty Configuration */
+    public static final class Builder implements Configuration.Builder<FakeConfiguration, Builder> {
+
+        private final MutableOptionsBundle mOptionsBundle;
+
+        public Builder() {
+            mOptionsBundle = MutableOptionsBundle.create();
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mOptionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public FakeConfiguration build() {
+            return new FakeConfiguration(OptionsBundle.from(mOptionsBundle));
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
new file mode 100644
index 0000000..2e55c1f
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageInfo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.camera.core.ImageInfo;
+
+/**
+ * A fake implementation of {@link ImageInfo} where the values are settable.
+ */
+public final class FakeImageInfo implements ImageInfo {
+    private Object mTag;
+    private long mTimestamp;
+
+    public void setTag(Object tag) {
+        mTag = tag;
+    }
+    public void setTimestamp(long timestamp) {
+        mTimestamp = timestamp;
+    }
+
+    @Override
+    public Object getTag() {
+        return mTag;
+    }
+    @Override
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageProxy.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageProxy.java
new file mode 100644
index 0000000..dd669d2
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageProxy.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import android.graphics.Rect;
+import android.media.Image;
+
+import androidx.camera.core.ImageInfo;
+import androidx.camera.core.ImageProxy;
+
+/**
+ * A fake implementation of {@link ImageProxy} where the values are settable.
+ */
+public final class FakeImageProxy implements ImageProxy {
+    private Rect mCropRect = new Rect();
+    private int mFormat = 0;
+    private int mHeight = 0;
+    private int mWidth = 0;
+    private Long mTimestamp = -1L;
+    private PlaneProxy[] mPlaneProxy = new PlaneProxy[0];
+    private ImageInfo mImageInfo;
+    private Image mImage;
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public Rect getCropRect() {
+        return mCropRect;
+    }
+
+    @Override
+    public void setCropRect(Rect rect) {
+        mCropRect = rect;
+    }
+
+    @Override
+    public int getFormat() {
+        return mFormat;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
+    @Override
+    public void setTimestamp(long timestamp) {
+        mTimestamp = timestamp;
+    }
+
+    @Override
+    public PlaneProxy[] getPlanes() {
+        return mPlaneProxy;
+    }
+
+    @Override
+    public ImageInfo getImageInfo() {
+        return mImageInfo;
+    }
+
+    @Override
+    public Image getImage() {
+        return mImage;
+    }
+
+    public void setImageInfo(ImageInfo imageInfo) {
+        mImageInfo = imageInfo;
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageReaderProxy.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageReaderProxy.java
new file mode 100644
index 0000000..75333d9
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeImageReaderProxy.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.view.Surface;
+
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.ImageReaderProxy;
+
+/**
+ * A fake implementation of ImageReaderProxy where the values are settable and the
+ * OnImageAvailableListener can be triggered.
+ */
+public class FakeImageReaderProxy implements ImageReaderProxy {
+    private int mWidth = 100;
+    private int mHeight = 100;
+    private int mImageFormat = ImageFormat.JPEG;
+    private int mMaxImages = 8;
+    private Surface mSurface;
+    private Handler mHandler;
+    private ImageProxy mImageProxy;
+
+    ImageReaderProxy.OnImageAvailableListener mListener;
+
+    @Override
+    public ImageProxy acquireLatestImage() {
+        return mImageProxy;
+    }
+
+    @Override
+    public ImageProxy acquireNextImage() {
+        return mImageProxy;
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public int getWidth() {
+        return mWidth;
+    }
+
+    @Override
+    public int getHeight() {
+        return mHeight;
+    }
+
+    @Override
+    public int getImageFormat() {
+        return mImageFormat;
+    }
+
+    @Override
+    public int getMaxImages() {
+        return mMaxImages;
+    }
+
+    @Override
+    @Nullable
+    public Surface getSurface() {
+        return mSurface;
+    }
+
+    @Override
+    public void setOnImageAvailableListener(
+            @Nullable final ImageReaderProxy.OnImageAvailableListener listener,
+            @Nullable Handler handler) {
+        mListener = listener;
+        mHandler = handler;
+    }
+
+    public void setMaxImages(int maxImages) {
+        mMaxImages = maxImages;
+    }
+
+    public void setImageProxy(ImageProxy imageProxy) {
+        mImageProxy = imageProxy;
+    }
+
+    public void setSurface(Surface surface) {
+        mSurface = surface;
+    }
+
+    /**
+     * Manually trigger OnImageAvailableListener to notify the Image is ready.
+     */
+    public void triggerImageAvailable() {
+        if (mListener != null) {
+            if (mHandler != null) {
+                mHandler.post(
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                mListener.onImageAvailable(FakeImageReaderProxy.this);
+                            }
+                        });
+            } else {
+                mListener.onImageAvailable(FakeImageReaderProxy.this);
+            }
+        }
+    }
+}
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..be61f09
--- /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 mLifecycleRegistry;
+
+    /**
+     * Creates a new lifecycle owner.
+     *
+     * <p>The lifecycle is initial put into the INITIALIZED and CREATED states.
+     */
+    public FakeLifecycleOwner() {
+        mLifecycleRegistry = new LifecycleRegistry(this);
+        mLifecycleRegistry.markState(Lifecycle.State.INITIALIZED);
+        mLifecycleRegistry.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 (mLifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        mLifecycleRegistry.markState(Lifecycle.State.STARTED);
+        mLifecycleRegistry.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 (mLifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        mLifecycleRegistry.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 (mLifecycleRegistry.getCurrentState() != Lifecycle.State.RESUMED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        mLifecycleRegistry.markState(Lifecycle.State.STARTED);
+        mLifecycleRegistry.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 (mLifecycleRegistry.getCurrentState() != Lifecycle.State.STARTED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        mLifecycleRegistry.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 (mLifecycleRegistry.getCurrentState() != Lifecycle.State.CREATED) {
+            throw new IllegalStateException("Invalid state transition.");
+        }
+        mLifecycleRegistry.markState(Lifecycle.State.DESTROYED);
+    }
+
+    /** Returns the number of observers of this lifecycle. */
+    public int getObserverCount() {
+        return mLifecycleRegistry.getObserverCount();
+    }
+
+    @Override
+    public Lifecycle getLifecycle() {
+        return mLifecycleRegistry;
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.java
new file mode 100644
index 0000000..1864da2
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCase.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.testing.fakes;
+
+import android.util.Size;
+
+import androidx.camera.core.BaseUseCase;
+import androidx.camera.core.CameraX.LensFacing;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseConfiguration;
+
+import java.util.Map;
+
+/**
+ * A fake {@link BaseUseCase}.
+ */
+public class FakeUseCase extends BaseUseCase {
+    private volatile boolean mIsCleared = false;
+
+    /**
+     * Creates a new instance of a {@link FakeUseCase} with a given configuration.
+     */
+    public FakeUseCase(FakeUseCaseConfiguration configuration) {
+        super(configuration);
+    }
+
+    /**
+     * Creates a new instance of a {@link FakeUseCase} with a default configuration.
+     */
+    public FakeUseCase() {
+        this(new FakeUseCaseConfiguration.Builder().build());
+    }
+
+    @Override
+    protected UseCaseConfiguration.Builder<?, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
+        return new FakeUseCaseConfiguration.Builder()
+                .setLensFacing(lensFacing)
+                .setOptionUnpacker(new SessionConfiguration.OptionUnpacker() {
+                    @Override
+                    public void unpack(UseCaseConfiguration<?> useCaseConfig,
+                            SessionConfiguration.Builder sessionConfigBuilder) {
+                    }
+                });
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        mIsCleared = 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 mIsCleared;
+    }
+}
diff --git a/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfiguration.java b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfiguration.java
new file mode 100644
index 0000000..2d79768
--- /dev/null
+++ b/camera/testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfiguration.java
@@ -0,0 +1,261 @@
+/*
+ * 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.CameraDeviceConfiguration;
+import androidx.camera.core.CameraX;
+import androidx.camera.core.Configuration;
+import androidx.camera.core.MutableConfiguration;
+import androidx.camera.core.MutableOptionsBundle;
+import androidx.camera.core.OptionsBundle;
+import androidx.camera.core.SessionConfiguration;
+import androidx.camera.core.UseCaseConfiguration;
+
+import java.util.Set;
+import java.util.UUID;
+
+/** A fake configuration for {@link FakeUseCase}. */
+public class FakeUseCaseConfiguration
+        implements UseCaseConfiguration<FakeUseCase>, CameraDeviceConfiguration {
+
+    private final Configuration mConfig;
+
+    FakeUseCaseConfiguration(Configuration config) {
+        mConfig = config;
+    }
+
+    @Override
+    public Configuration getConfiguration() {
+        return mConfig;
+    }
+
+    /** Builder for an empty Configuration */
+    public static final class Builder
+            implements UseCaseConfiguration.Builder<FakeUseCase, FakeUseCaseConfiguration, Builder>,
+            CameraDeviceConfiguration.Builder<FakeUseCaseConfiguration, Builder> {
+
+        private final MutableOptionsBundle mOptionsBundle;
+
+        public Builder() {
+            mOptionsBundle = MutableOptionsBundle.create();
+            setTargetClass(FakeUseCase.class);
+            setLensFacing(CameraX.LensFacing.BACK);
+        }
+
+        @Override
+        public MutableConfiguration getMutableConfiguration() {
+            return mOptionsBundle;
+        }
+
+        @Override
+        public Builder builder() {
+            return this;
+        }
+
+        @Override
+        public FakeUseCaseConfiguration build() {
+            return new FakeUseCaseConfiguration(OptionsBundle.from(mOptionsBundle));
+        }
+
+        // Start of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+
+        // Implementations of Configuration.Builder default methods
+
+        @Override
+        public <ValueT> Builder insertOption(Option<ValueT> opt, ValueT value) {
+            getMutableConfiguration().insertOption(opt, value);
+            return builder();
+        }
+
+        @Override
+        @Nullable
+        public <ValueT> Builder removeOption(Option<ValueT> opt) {
+            getMutableConfiguration().removeOption(opt);
+            return builder();
+        }
+
+        // Implementations of TargetConfiguration.Builder default methods
+
+        @Override
+        public Builder setTargetClass(Class<FakeUseCase> 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();
+        }
+
+        @Override
+        public Builder setTargetName(String targetName) {
+            getMutableConfiguration().insertOption(OPTION_TARGET_NAME, targetName);
+            return builder();
+        }
+
+        // Implementations of CameraDeviceConfiguration.Builder default methods
+
+        @Override
+        public Builder setLensFacing(CameraX.LensFacing lensFacing) {
+            getMutableConfiguration().insertOption(OPTION_LENS_FACING, lensFacing);
+            return builder();
+        }
+
+        // Implementations of UseCaseConfiguration.Builder default methods
+
+        @Override
+        public Builder setDefaultSessionConfiguration(SessionConfiguration sessionConfig) {
+            getMutableConfiguration().insertOption(OPTION_DEFAULT_SESSION_CONFIG, sessionConfig);
+            return builder();
+        }
+
+        @Override
+        public Builder setOptionUnpacker(SessionConfiguration.OptionUnpacker optionUnpacker) {
+            getMutableConfiguration().insertOption(OPTION_CONFIG_UNPACKER, optionUnpacker);
+            return builder();
+        }
+
+        @Override
+        public Builder setSurfaceOccupancyPriority(int priority) {
+            getMutableConfiguration().insertOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, priority);
+            return builder();
+        }
+
+        // End of the default implementation of Configuration.Builder
+        // *****************************************************************************************
+    }
+
+    // Start of the default implementation of Configuration
+    // *********************************************************************************************
+
+    // Implementations of Configuration.Reader default methods
+
+    @Override
+    public boolean containsOption(Option<?> id) {
+        return getConfiguration().containsOption(id);
+    }
+
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id) {
+        return getConfiguration().retrieveOption(id);
+    }
+
+    @Override
+    @Nullable
+    public <ValueT> ValueT retrieveOption(Option<ValueT> id, @Nullable ValueT valueIfMissing) {
+        return getConfiguration().retrieveOption(id, valueIfMissing);
+    }
+
+    @Override
+    public void findOptions(String idStem, OptionMatcher matcher) {
+        getConfiguration().findOptions(idStem, matcher);
+    }
+
+    @Override
+    public Set<Option<?>> listOptions() {
+        return getConfiguration().listOptions();
+    }
+
+    // Implementations of TargetConfiguration default methods
+
+    @Override
+    @Nullable
+    public Class<FakeUseCase> getTargetClass(
+            @Nullable Class<FakeUseCase> valueIfMissing) {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<FakeUseCase> storedClass = (Class<FakeUseCase>) retrieveOption(
+                OPTION_TARGET_CLASS,
+                valueIfMissing);
+        return storedClass;
+    }
+
+    @Override
+    public Class<FakeUseCase> getTargetClass() {
+        @SuppressWarnings("unchecked") // Value should only be added via Builder#setTargetClass()
+                Class<FakeUseCase> storedClass = (Class<FakeUseCase>) retrieveOption(
+                OPTION_TARGET_CLASS);
+        return storedClass;
+    }
+
+    @Override
+    @Nullable
+    public String getTargetName(@Nullable String valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_NAME, valueIfMissing);
+    }
+
+    @Override
+    public String getTargetName() {
+        return retrieveOption(OPTION_TARGET_NAME);
+    }
+
+    // Implementations of CameraDeviceConfiguration default methods
+
+    @Override
+    @Nullable
+    public CameraX.LensFacing getLensFacing(@Nullable CameraX.LensFacing valueIfMissing) {
+        return retrieveOption(OPTION_LENS_FACING, valueIfMissing);
+    }
+
+    @Override
+    public CameraX.LensFacing getLensFacing() {
+        return retrieveOption(OPTION_LENS_FACING);
+    }
+
+    // Implementations of UseCaseConfiguration default methods
+
+    @Override
+    @Nullable
+    public SessionConfiguration getDefaultSessionConfiguration(
+            @Nullable SessionConfiguration valueIfMissing) {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG, valueIfMissing);
+    }
+
+    @Override
+    public SessionConfiguration getDefaultSessionConfiguration() {
+        return retrieveOption(OPTION_DEFAULT_SESSION_CONFIG);
+    }
+
+    @Override
+    @Nullable
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker(
+            @Nullable SessionConfiguration.OptionUnpacker valueIfMissing) {
+        return retrieveOption(OPTION_CONFIG_UNPACKER, valueIfMissing);
+    }
+
+    @Override
+    public SessionConfiguration.OptionUnpacker getOptionUnpacker() {
+        return retrieveOption(OPTION_CONFIG_UNPACKER);
+    }
+
+    @Override
+    public int getSurfaceOccupancyPriority(int valueIfMissing) {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY, valueIfMissing);
+    }
+
+    @Override
+    public int getSurfaceOccupancyPriority() {
+        return retrieveOption(OPTION_SURFACE_OCCUPANCY_PRIORITY);
+    }
+
+    // End of the default implementation of Configuration
+    // *********************************************************************************************
+}
diff --git a/camera/view/build.gradle b/camera/view/build.gradle
new file mode 100644
index 0000000..81810fd
--- /dev/null
+++ b/camera/view/build.gradle
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+// TODO(b/124783972): Switch to androidx.build.LibraryVersions and androidx.build.LibraryGroups when ready
+import androidx.build.UnpublishedLibraryVersions
+import androidx.build.UnpublishedLibraryGroups
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    api("androidx.lifecycle:lifecycle-common:2.0.0", libs.exclude_annotations_transitive)
+    implementation("androidx.annotation:annotation:1.0.0")
+
+    api(project(":camera:camera-core"))
+}
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+}
+supportLibrary {
+    name = "Jetpack Camera View Library"
+    publish = true
+    mavenVersion = UnpublishedLibraryVersions.CAMERA
+    mavenGroup = UnpublishedLibraryGroups.CAMERA
+    inceptionYear = "2019"
+    description = "UI tools for the Jetpack Camera Library, a library providing a consistent and " +
+            "reliable camera foundation that enables great camera driven experiences across all " +
+            "of Android."
+}
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..f99ccda
--- /dev/null
+++ b/camera/view/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?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 package="androidx.camera.view"/>
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..86cd272
--- /dev/null
+++ b/camera/view/src/main/java/androidx/camera/view/CameraView.java
@@ -0,0 +1,1172 @@
+/*
+ * 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 mFocusingRect = new Rect();
+    private final Rect mMeteringRect = new Rect();
+    // For tap-to-focus
+    private long mDownEventTimestamp;
+    // For pinch-to-zoom
+    private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
+    private boolean mIsPinchToZoomEnabled = true;
+    CameraXModule mCameraModule;
+    private final DisplayManager.DisplayListener mDisplayListener =
+            new DisplayListener() {
+                @Override
+                public void onDisplayAdded(int displayId) {
+                }
+
+                @Override
+                public void onDisplayRemoved(int displayId) {
+                }
+
+                @Override
+                public void onDisplayChanged(int displayId) {
+                    mCameraModule.invalidateView();
+                }
+            };
+    private TextureView mCameraTextureView;
+    private Size mViewFinderSrcSize = new Size(0, 0);
+    private ScaleType mScaleType = ScaleType.CENTER_CROP;
+    // For accessibility event
+    private MotionEvent mUpEvent;
+    private @Nullable
+            Paint mLayerPaint;
+
+    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
+     * androidx.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
+     * androidx.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
+     *                                  androidx.lifecycle.Lifecycle.State#DESTROYED} state.
+     * @throws IllegalStateException    if camera permissions are not granted.
+     */
+    @RequiresPermission(permission.CAMERA)
+    public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
+        mCameraModule.bindToLifecycle(lifecycleOwner);
+    }
+
+    private void init(Context context, @Nullable AttributeSet attrs) {
+        addView(mCameraTextureView = new TextureView(getContext()), 0 /* view position */);
+        mCameraTextureView.setLayerPaint(mLayerPaint);
+        mCameraModule = 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().getId())));
+            setQuality(
+                    Quality.fromId(
+                            a.getInteger(R.styleable.CameraView_quality, getQuality().getId())));
+            setPinchToZoomEnabled(
+                    a.getBoolean(
+                            R.styleable.CameraView_pinchToZoomEnabled, isPinchToZoomEnabled()));
+            setCaptureMode(
+                    CaptureMode.fromId(
+                            a.getInteger(R.styleable.CameraView_captureMode,
+                                    getCaptureMode().getId())));
+
+            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);
+        }
+
+        mPinchToZoomGestureDetector = 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().getId());
+        state.putInt(EXTRA_QUALITY, getQuality().getId());
+        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().getId());
+        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
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public void setLayerPaint(@Nullable Paint paint) {
+        super.setLayerPaint(paint);
+        mLayerPaint = paint;
+        mCameraTextureView.setLayerPaint(paint);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        DisplayManager dpyMgr =
+                (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+        dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        DisplayManager dpyMgr =
+                (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+        dpyMgr.unregisterDisplayListener(mDisplayListener);
+    }
+
+    // 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 (mViewFinderSrcSize.getHeight() == 0 || mViewFinderSrcSize.getWidth() == 0) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            mCameraTextureView.measure(viewWidth, viewHeight);
+        } else {
+            Size scaled =
+                    calculateViewfinderViewDimens(
+                            mViewFinderSrcSize, viewWidth, viewHeight, displayRotation, mScaleType);
+            super.setMeasuredDimension(
+                    Math.min(scaled.getWidth(), viewWidth),
+                    Math.min(scaled.getHeight(), viewHeight));
+            mCameraTextureView.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) {
+            mCameraModule.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
+        mCameraModule.bindToLifecycleAfterViewMeasured();
+
+        // If we don't know the src buffer size yet, set the viewfinder to be the parent size
+        if (mViewFinderSrcSize.getWidth() == 0 || mViewFinderSrcSize.getHeight() == 0) {
+            mCameraTextureView.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(
+                        mViewFinderSrcSize, viewWidth, viewHeight, displayRotation, mScaleType);
+
+        // 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:  " + mViewFinderSrcSize.getWidth());
+        log("layout: sizeHeight: " + mViewFinderSrcSize.getHeight());
+        log(
+                "layout: sizeRatio:  "
+                        + (mViewFinderSrcSize.getWidth() / (float) mViewFinderSrcSize.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())
+                        + " - "
+                        + mScaleType
+                        + "-"
+                        + displayRotationToString(displayRotation)
+                        + ")");
+        log("layout: final       " + layoutL + ", " + layoutT + ", " + layoutR + ", " + layoutB);
+
+        mCameraTextureView.layout(layoutL, layoutT, layoutR, layoutB);
+
+        mCameraModule.invalidateView();
+    }
+
+    /** Records the size of the viewfinder's buffers. */
+    @UiThread
+    void onViewfinderSourceDimensUpdated(int srcWidth, int srcHeight) {
+        if (srcWidth != mViewFinderSrcSize.getWidth()
+                || srcHeight != mViewFinderSrcSize.getHeight()) {
+            mViewFinderSrcSize = 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 (mCameraTextureView != null) {
+            return mCameraTextureView.getSurfaceTexture();
+        }
+
+        return null;
+    }
+
+    @UiThread
+    void setSurfaceTexture(SurfaceTexture surfaceTexture) {
+        if (mCameraTextureView.getSurfaceTexture() != surfaceTexture) {
+            if (mCameraTextureView.isAvailable()) {
+                // Remove the old TextureView to properly detach the old SurfaceTexture from the GL
+                // Context.
+                removeView(mCameraTextureView);
+                addView(mCameraTextureView = new TextureView(getContext()), 0);
+                mCameraTextureView.setLayerPaint(mLayerPaint);
+                requestLayout();
+            }
+
+            mCameraTextureView.setSurfaceTexture(surfaceTexture);
+        }
+    }
+
+    @UiThread
+    Matrix getTransform(Matrix matrix) {
+        return mCameraTextureView.getTransform(matrix);
+    }
+
+    @UiThread
+    int getViewFinderWidth() {
+        return mCameraTextureView.getWidth();
+    }
+
+    @UiThread
+    int getViewFinderHeight() {
+        return mCameraTextureView.getHeight();
+    }
+
+    @UiThread
+    void setTransform(final Matrix matrix) {
+        if (mCameraTextureView != null) {
+            mCameraTextureView.setTransform(matrix);
+        }
+    }
+
+    /**
+     * Returns the scale type used to scale the viewfinder.
+     *
+     * @return The current {@link ScaleType}.
+     */
+    public ScaleType getScaleType() {
+        return mScaleType;
+    }
+
+    /**
+     * 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 != mScaleType) {
+            mScaleType = scaleType;
+            requestLayout();
+        }
+    }
+
+    /**
+     * Gets the current quality for image and video outputs.
+     *
+     * @return The current {@link Quality}. Currently only {@link Quality#HIGH} is supported.
+     */
+    Quality getQuality() {
+        return mCameraModule.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.
+     */
+    void setQuality(Quality quality) {
+        mCameraModule.setQuality(quality);
+    }
+
+    /**
+     * Returns the scale type used to scale the viewfinder.
+     *
+     * @return The current {@link CaptureMode}.
+     */
+    public CaptureMode getCaptureMode() {
+        return mCameraModule.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) {
+        mCameraModule.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 mCameraModule.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) {
+        mCameraModule.setMaxVideoDuration(duration);
+    }
+
+    /**
+     * Returns the maximum size of videos in bytes, or {@link #INDEFINITE_VIDEO_SIZE} if there is no
+     * timeout.
+     */
+    private long getMaxVideoSize() {
+        return mCameraModule.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) {
+        mCameraModule.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) {
+        mCameraModule.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) {
+        mCameraModule.takePicture(file, listener);
+    }
+
+    /**
+     * Takes a video and calls the OnVideoSavedListener when done.
+     *
+     * @param file The destination.
+     */
+    public void startRecording(File file, OnVideoSavedListener listener) {
+        mCameraModule.startRecording(file, listener);
+    }
+
+    /** Stops an in progress video. */
+    public void stopRecording() {
+        mCameraModule.stopRecording();
+    }
+
+    /** @return True if currently recording. */
+    public boolean isRecording() {
+        return mCameraModule.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 mCameraModule.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() {
+        mCameraModule.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) {
+        mCameraModule.setCameraByLensFacing(lensFacing);
+    }
+
+    /** Returns the currently selected {@link LensFacing}. */
+    @Nullable
+    public LensFacing getCameraLensFacing() {
+        return mCameraModule.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) {
+        mCameraModule.focus(focus, metering);
+    }
+
+    /** Gets the active flash strategy. */
+    public FlashMode getFlash() {
+        return mCameraModule.getFlash();
+    }
+
+    /** Sets the active flash strategy. */
+    public void setFlash(FlashMode flashMode) {
+        mCameraModule.setFlash(flashMode);
+    }
+
+    private int getRelativeCameraOrientation(boolean compensateForMirroring) {
+        return mCameraModule.getRelativeCameraOrientation(compensateForMirroring);
+    }
+
+    private long delta() {
+        return System.currentTimeMillis() - mDownEventTimestamp;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        // Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
+        if (mCameraModule.isPaused()) {
+            return false;
+        }
+        // Only forward the event to the pinch-to-zoom gesture detector when pinch-to-zoom is
+        // enabled.
+        if (isPinchToZoomEnabled()) {
+            mPinchToZoomGestureDetector.onTouchEvent(event);
+        }
+        if (event.getPointerCount() == 2 && isPinchToZoomEnabled() && isZoomSupported()) {
+            return true;
+        }
+
+        // Camera focus
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mDownEventTimestamp = System.currentTimeMillis();
+                break;
+            case MotionEvent.ACTION_UP:
+                if (delta() < ViewConfiguration.getLongPressTimeout()) {
+                    mUpEvent = 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 = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f;
+        final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
+        mUpEvent = null;
+        calculateTapArea(mFocusingRect, x, y, 1f);
+        calculateTapArea(mMeteringRect, x, y, 1.5f);
+        if (area(mFocusingRect) > 0 && area(mMeteringRect) > 0) {
+            focus(mFocusingRect, mMeteringRect);
+        }
+
+        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 mIsPinchToZoomEnabled;
+    }
+
+    /**
+     * 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) {
+        mIsPinchToZoomEnabled = enabled;
+    }
+
+    /**
+     * Returns the current zoom level.
+     *
+     * @return The current zoom level.
+     */
+    public float getZoomLevel() {
+        return mCameraModule.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) {
+        mCameraModule.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 mCameraModule.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 mCameraModule.getMaxZoomLevel();
+    }
+
+    /**
+     * Returns whether the bound camera supports zooming.
+     *
+     * @return True if the camera supports zooming.
+     */
+    public boolean isZoomSupported() {
+        return mCameraModule.isZoomSupported();
+    }
+
+    /**
+     * Turns on/off torch.
+     *
+     * @param torch True to turn on torch, false to turn off torch.
+     */
+    public void enableTorch(boolean torch) {
+        mCameraModule.enableTorch(torch);
+    }
+
+    /**
+     * Returns current torch status.
+     *
+     * @return true if torch is on , otherwise false
+     */
+    public boolean isTorchOn() {
+        return mCameraModule.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);
+
+        private int mId;
+
+        int getId() {
+            return mId;
+        }
+
+        ScaleType(int id) {
+            mId = id;
+        }
+
+        static ScaleType fromId(int id) {
+            for (ScaleType st : values()) {
+                if (st.mId == 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.
+     */
+    enum Quality {
+        MAX(0),
+        HIGH(1),
+        MEDIUM(2),
+        LOW(3);
+
+        private int mId;
+
+        int getId() {
+            return mId;
+        }
+
+        Quality(int id) {
+            mId = id;
+        }
+
+        static Quality fromId(int id) {
+            for (Quality f : values()) {
+                if (f.mId == 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);
+
+        private int mId;
+
+        int getId() {
+            return mId;
+        }
+
+        CaptureMode(int id) {
+            mId = id;
+        }
+
+        static CaptureMode fromId(int id) {
+            for (CaptureMode f : values()) {
+                if (f.mId == id) {
+                    return f;
+                }
+            }
+            throw new IllegalArgumentException();
+        }
+    }
+
+    static class S extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+        private ScaleGestureDetector.OnScaleGestureListener mListener;
+
+        void setRealGestureDetector(ScaleGestureDetector.OnScaleGestureListener l) {
+            mListener = l;
+        }
+
+        @Override
+        public boolean onScale(ScaleGestureDetector detector) {
+            return mListener.onScale(detector);
+        }
+    }
+
+    private class PinchToZoomGestureDetector extends ScaleGestureDetector
+            implements ScaleGestureDetector.OnScaleGestureListener {
+        private static final float SCALE_MULTIPIER = 0.75f;
+        private final BaseInterpolator mInterpolator = new DecelerateInterpolator(2f);
+        private float mNormalizedScaleFactor = 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) {
+            mNormalizedScaleFactor += (detector.getScaleFactor() - 1f) * SCALE_MULTIPIER;
+            // Since the scale factor is normalized, it should always be in the range [0, 1]
+            mNormalizedScaleFactor = rangeLimit(mNormalizedScaleFactor, 1f, 0);
+
+            // Apply decelerate interpolation. This will cause the differences to seem less
+            // pronounced
+            // at higher zoom levels.
+            float transformedScale = mInterpolator.getInterpolation(mNormalizedScaleFactor);
+
+            // 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();
+            mNormalizedScaleFactor =
+                    (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..23d7f76
--- /dev/null
+++ b/camera/view/src/main/java/androidx/camera/view/CameraXModule.java
@@ -0,0 +1,801 @@
+/*
+ * 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.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+
+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 static final Rational ASPECT_RATIO_9_16 = new Rational(9, 16);
+    private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
+
+    private final CameraManager mCameraManager;
+    private final ViewFinderUseCaseConfiguration.Builder mViewFinderConfigBuilder;
+    private final VideoCaptureUseCaseConfiguration.Builder mVideoCaptureConfigBuilder;
+    private final ImageCaptureUseCaseConfiguration.Builder mImageCaptureConfigBuilder;
+    private final CameraView mCameraView;
+    final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
+    private CameraView.Quality mQuality = CameraView.Quality.HIGH;
+    private CameraView.CaptureMode mCaptureMode = CaptureMode.IMAGE;
+    private long mMaxVideoDuration = CameraView.INDEFINITE_VIDEO_DURATION;
+    private long mMaxVideoSize = CameraView.INDEFINITE_VIDEO_SIZE;
+    private FlashMode mFlash = FlashMode.OFF;
+    @Nullable
+    private ImageCaptureUseCase mImageCaptureUseCase;
+    @Nullable
+    private VideoCaptureUseCase mVideoCaptureUseCase;
+    @Nullable
+    ViewFinderUseCase mViewFinderUseCase;
+    @Nullable
+    LifecycleOwner mCurrentLifecycle;
+    private final LifecycleObserver mCurrentLifecycleObserver =
+            new LifecycleObserver() {
+                @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+                public void onDestroy(LifecycleOwner owner) {
+                    if (owner == mCurrentLifecycle) {
+                        clearCurrentLifecycle();
+                        mViewFinderUseCase.removeViewFinderOutputListener();
+                    }
+                }
+            };
+    @Nullable
+    private LifecycleOwner mNewLifecycle;
+    private float mZoomLevel = UNITY_ZOOM_SCALE;
+    @Nullable
+    private Rect mCropRegion;
+    @Nullable
+    private CameraX.LensFacing mCameraLensFacing = LensFacing.BACK;
+
+    CameraXModule(CameraView view) {
+        this.mCameraView = view;
+
+        mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
+
+        mViewFinderConfigBuilder =
+                new ViewFinderUseCaseConfiguration.Builder().setTargetName("ViewFinder");
+
+        mImageCaptureConfigBuilder =
+                new ImageCaptureUseCaseConfiguration.Builder().setTargetName("ImageCapture");
+
+        mVideoCaptureConfigBuilder =
+                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) {
+        mNewLifecycle = lifecycleOwner;
+
+        if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
+            bindToLifecycleAfterViewMeasured();
+        }
+    }
+
+    @RequiresPermission(permission.CAMERA)
+    void bindToLifecycleAfterViewMeasured() {
+        if (mNewLifecycle == null) {
+            return;
+        }
+
+        clearCurrentLifecycle();
+        mCurrentLifecycle = mNewLifecycle;
+        mNewLifecycle = null;
+        if (mCurrentLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
+            mCurrentLifecycle = null;
+            throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
+        }
+
+        final int cameraOrientation;
+        try {
+            String cameraId;
+            Set<LensFacing> available = getAvailableCameraLensFacing();
+
+            if (available.isEmpty()) {
+                Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
+                mCameraLensFacing = null;
+            }
+
+            // Ensure the current camera exists, or default to another camera
+            if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
+                Log.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
+
+                // Default to the first available camera direction
+                mCameraLensFacing = available.iterator().next();
+
+                Log.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
+            }
+
+            // 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 (mCameraLensFacing == null) {
+                return;
+            }
+
+            cameraId = CameraX.getCameraWithLensFacing(mCameraLensFacing);
+            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.
+
+        boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
+                || getDisplayRotationDegrees() == 180;
+
+        if (getCaptureMode() == CaptureMode.IMAGE) {
+            mImageCaptureConfigBuilder.setTargetAspectRatio(
+                    isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3);
+            mViewFinderConfigBuilder.setTargetAspectRatio(
+                    isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3);
+        } else {
+            mImageCaptureConfigBuilder.setTargetAspectRatio(
+                    isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9);
+            mViewFinderConfigBuilder.setTargetAspectRatio(
+                    isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9);
+        }
+
+        mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
+        mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
+        mImageCaptureUseCase = new ImageCaptureUseCase(mImageCaptureConfigBuilder.build());
+
+        mVideoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
+        mVideoCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
+        mVideoCaptureUseCase = new VideoCaptureUseCase(mVideoCaptureConfigBuilder.build());
+        mViewFinderConfigBuilder.setLensFacing(mCameraLensFacing);
+
+        int relativeCameraOrientation = getRelativeCameraOrientation(false);
+
+        if (relativeCameraOrientation == 90 || relativeCameraOrientation == 270) {
+            mViewFinderConfigBuilder.setTargetResolution(
+                    new Size(getMeasuredHeight(), getMeasuredWidth()));
+        } else {
+            mViewFinderConfigBuilder.setTargetResolution(
+                    new Size(getMeasuredWidth(), getMeasuredHeight()));
+        }
+
+        mViewFinderUseCase = new ViewFinderUseCase(mViewFinderConfigBuilder.build());
+        mViewFinderUseCase.setOnViewFinderOutputUpdateListener(
+                new ViewFinderUseCase.OnViewFinderOutputUpdateListener() {
+                    @Override
+                    public void onUpdated(ViewFinderUseCase.ViewFinderOutput 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();
+                        CameraXModule.this.onViewfinderSourceDimensUpdated(textureWidth,
+                                textureHeight);
+                        CameraXModule.this.setSurfaceTexture(output.getSurfaceTexture());
+                    }
+                });
+
+        if (getCaptureMode() == CaptureMode.IMAGE) {
+            CameraX.bindToLifecycle(mCurrentLifecycle, mImageCaptureUseCase, mViewFinderUseCase);
+        } else if (getCaptureMode() == CaptureMode.VIDEO) {
+            CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCaptureUseCase, mViewFinderUseCase);
+        } else {
+            CameraX.bindToLifecycle(
+                    mCurrentLifecycle, mImageCaptureUseCase, mVideoCaptureUseCase,
+                    mViewFinderUseCase);
+        }
+        setZoomLevel(mZoomLevel);
+        mCurrentLifecycle.getLifecycle().addObserver(mCurrentLifecycleObserver);
+        // 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 (mImageCaptureUseCase == 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");
+        }
+
+        mImageCaptureUseCase.takePicture(listener);
+    }
+
+    public void takePicture(File saveLocation, OnImageSavedListener listener) {
+        if (mImageCaptureUseCase == 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 = mCameraLensFacing == LensFacing.FRONT;
+        mImageCaptureUseCase.takePicture(saveLocation, listener, metadata);
+    }
+
+    public void startRecording(File file, final OnVideoSavedListener listener) {
+        if (mVideoCaptureUseCase == 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");
+        }
+
+        mVideoIsRecording.set(true);
+        mVideoCaptureUseCase.startRecording(
+                file,
+                new VideoCaptureUseCase.OnVideoSavedListener() {
+                    @Override
+                    public void onVideoSaved(File savedFile) {
+                        mVideoIsRecording.set(false);
+                        listener.onVideoSaved(savedFile);
+                    }
+
+                    @Override
+                    public void onError(
+                            VideoCaptureUseCase.UseCaseError useCaseError,
+                            String message,
+                            @Nullable Throwable cause) {
+                        mVideoIsRecording.set(false);
+                        Log.e(TAG, message, cause);
+                        listener.onError(useCaseError, message, cause);
+                    }
+                });
+    }
+
+    public void stopRecording() {
+        if (mVideoCaptureUseCase == null) {
+            return;
+        }
+
+        mVideoCaptureUseCase.stopRecording();
+    }
+
+    public boolean isRecording() {
+        return mVideoIsRecording.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 (mCameraLensFacing != lensFacing) {
+            // If we're not bound to a lifecycle, just update the camera that will be opened when we
+            // attach to a lifecycle.
+            mCameraLensFacing = lensFacing;
+
+            if (mCurrentLifecycle != null) {
+                // Re-bind to lifecycle with new camera
+                bindToLifecycle(mCurrentLifecycle);
+            }
+        }
+    }
+
+    @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 mCameraLensFacing;
+    }
+
+    public void toggleCamera() {
+        // TODO(b/124269166): Rethink how we can handle permissions here.
+        @SuppressLint("MissingPermission")
+        Set<LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
+
+        if (availableCameraLensFacing.isEmpty()) {
+            return;
+        }
+
+        if (mCameraLensFacing == null) {
+            setCameraByLensFacing(availableCameraLensFacing.iterator().next());
+            return;
+        }
+
+        if (mCameraLensFacing == LensFacing.BACK
+                && availableCameraLensFacing.contains(LensFacing.FRONT)) {
+            setCameraByLensFacing(LensFacing.FRONT);
+            return;
+        }
+
+        if (mCameraLensFacing == LensFacing.FRONT
+                && availableCameraLensFacing.contains(LensFacing.BACK)) {
+            setCameraByLensFacing(LensFacing.BACK);
+            return;
+        }
+    }
+
+    public void focus(Rect focus, Rect metering) {
+        if (mViewFinderUseCase == null) {
+            // Nothing to focus on since we don't yet have a viewfinder
+            return;
+        }
+
+        Rect rescaledFocus;
+        Rect rescaledMetering;
+        try {
+            Rect sensorRegion;
+            if (mCropRegion != null) {
+                sensorRegion = mCropRegion;
+            } 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;
+        }
+
+        mViewFinderUseCase.focus(rescaledFocus, rescaledMetering);
+    }
+
+    public float getZoomLevel() {
+        return mZoomLevel;
+    }
+
+    public void setZoomLevel(float zoomLevel) {
+        // Set the zoom level in case it is set before binding to a lifecycle
+        this.mZoomLevel = zoomLevel;
+
+        if (mViewFinderUseCase == 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.mZoomLevel < minZoom) {
+            Log.e(TAG, "Requested zoom level is less than minimum zoom level.");
+        }
+        if (this.mZoomLevel > maxZoom) {
+            Log.e(TAG, "Requested zoom level is greater than maximum zoom level.");
+        }
+        this.mZoomLevel = Math.max(minZoom, Math.min(maxZoom, this.mZoomLevel));
+
+        float zoomScaleFactor =
+                (maxZoom == minZoom) ? minZoom : (this.mZoomLevel - 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.mCropRegion = cropRegion;
+
+        mViewFinderUseCase.zoom(cropRegion);
+    }
+
+    public float getMinZoomLevel() {
+        return UNITY_ZOOM_SCALE;
+    }
+
+    public float getMaxZoomLevel() {
+        try {
+            CameraCharacteristics characteristics =
+                    mCameraManager.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 (mCurrentLifecycle != null) {
+            bindToLifecycle(mCurrentLifecycle);
+        }
+    }
+
+    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 mQuality;
+    }
+
+    public void setQuality(Quality quality) {
+        if (quality != Quality.HIGH) {
+            throw new UnsupportedOperationException("Only supported Quality is HIGH");
+        }
+        this.mQuality = quality;
+    }
+
+    public void invalidateView() {
+        transformPreview();
+        updateViewInfo();
+    }
+
+    void clearCurrentLifecycle() {
+        if (mCurrentLifecycle != null) {
+            // Remove previous use cases
+            CameraX.unbind(mImageCaptureUseCase, mVideoCaptureUseCase, mViewFinderUseCase);
+        }
+
+        mCurrentLifecycle = null;
+    }
+
+    private Rect getSensorSize(String cameraId) throws CameraAccessException {
+        CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
+        return characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+    }
+
+    String getActiveCamera() throws CameraInfoUnavailableException {
+        return CameraX.getCameraWithLensFacing(mCameraLensFacing);
+    }
+
+    @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 (mImageCaptureUseCase != null) {
+            mImageCaptureUseCase.setTargetAspectRatio(new Rational(getWidth(), getHeight()));
+            mImageCaptureUseCase.setTargetRotation(getDisplaySurfaceRotation());
+        }
+
+        if (mVideoCaptureUseCase != null) {
+            mVideoCaptureUseCase.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 (mCurrentLifecycle != null) {
+            if (!hasCameraWithLensFacing(LensFacing.BACK)) {
+                available.remove(LensFacing.BACK);
+            }
+
+            if (!hasCameraWithLensFacing(LensFacing.FRONT)) {
+                available.remove(LensFacing.FRONT);
+            }
+        }
+
+        return available;
+    }
+
+    public FlashMode getFlash() {
+        return mFlash;
+    }
+
+    public void setFlash(FlashMode flash) {
+        this.mFlash = flash;
+
+        if (mImageCaptureUseCase == null) {
+            // Do nothing if there is no imageCaptureUseCase
+            return;
+        }
+
+        mImageCaptureUseCase.setFlashMode(flash);
+    }
+
+    public void enableTorch(boolean torch) {
+        if (mViewFinderUseCase == null) {
+            return;
+        }
+        mViewFinderUseCase.enableTorch(torch);
+    }
+
+    public boolean isTorchOn() {
+        if (mViewFinderUseCase == null) {
+            return false;
+        }
+        return mViewFinderUseCase.isTorchOn();
+    }
+
+    public Context getContext() {
+        return mCameraView.getContext();
+    }
+
+    public int getWidth() {
+        return mCameraView.getWidth();
+    }
+
+    public int getHeight() {
+        return mCameraView.getHeight();
+    }
+
+    public int getDisplayRotationDegrees() {
+        return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
+    }
+
+    protected int getDisplaySurfaceRotation() {
+        return mCameraView.getDisplaySurfaceRotation();
+    }
+
+    public void setSurfaceTexture(SurfaceTexture st) {
+        mCameraView.setSurfaceTexture(st);
+    }
+
+    private int getViewFinderWidth() {
+        return mCameraView.getViewFinderWidth();
+    }
+
+    private int getViewFinderHeight() {
+        return mCameraView.getViewFinderHeight();
+    }
+
+    private int getMeasuredWidth() {
+        return mCameraView.getMeasuredWidth();
+    }
+
+    private int getMeasuredHeight() {
+        return mCameraView.getMeasuredHeight();
+    }
+
+    void setTransform(final Matrix matrix) {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            mCameraView.post(
+                    new Runnable() {
+                        @Override
+                        public void run() {
+                            setTransform(matrix);
+                        }
+                    });
+        } else {
+            mCameraView.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.
+     */
+    void onViewfinderSourceDimensUpdated(int width, int height) {
+        mCameraView.onViewfinderSourceDimensUpdated(width, height);
+    }
+
+    public CameraView.CaptureMode getCaptureMode() {
+        return mCaptureMode;
+    }
+
+    public void setCaptureMode(CameraView.CaptureMode captureMode) {
+        this.mCaptureMode = captureMode;
+        rebindToLifecycle();
+    }
+
+    public long getMaxVideoDuration() {
+        return mMaxVideoDuration;
+    }
+
+    public void setMaxVideoDuration(long duration) {
+        mMaxVideoDuration = duration;
+    }
+
+    public long getMaxVideoSize() {
+        return mMaxVideoSize;
+    }
+
+    public void setMaxVideoSize(long size) {
+        mMaxVideoSize = 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>
diff --git a/car/moderator/res/values-as/strings.xml b/car/moderator/res/values-as/strings.xml
deleted file mode 100644
index ad8b096..0000000
--- a/car/moderator/res/values-as/strings.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--  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.
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="speed_bump_lockout_message" msgid="5540717186175632753">"গাড়ী চলোৱাত ধ্যান দিয়ক"</string>
-</resources>
diff --git a/core/res/values-as/strings.xml b/core/res/values-as/strings.xml
deleted file mode 100644
index 3039039..0000000
--- a/core/res/values-as/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright (C) 2017 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 xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="status_bar_notification_info_overflow" msgid="8106346172024741305">"৯৯৯+"</string>
-</resources>
diff --git a/leanback/src/main/res/values-as/strings.xml b/leanback/src/main/res/values-as/strings.xml
index 05b84d6..eb3b2b0 100644
--- a/leanback/src/main/res/values-as/strings.xml
+++ b/leanback/src/main/res/values-as/strings.xml
@@ -49,7 +49,7 @@
     <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"ছাব-টাইটেল অক্ষম কৰক"</string>
     <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"চিত্ৰৰ ভিতৰত চিত্ৰ ম\'ড আৰম্ভ কৰক"</string>
     <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
-    <string name="lb_playback_controls_shown" msgid="7794717158616536936">"মিডিয়াৰ নিয়ন্ত্ৰণসমূহ দেখুওৱা হ’ল"</string>
+    <string name="lb_playback_controls_shown" msgid="7794717158616536936">"মিডিয়াৰ নিয়ন্ত্ৰণসমূহ দেখুওৱা হ\'ল"</string>
     <string name="lb_playback_controls_hidden" msgid="619396299825306757">"মিডিয়াৰ নিয়ন্ত্ৰণসমূহ লুকুৱাই ৰখা হৈছে, দেখুওৱাবলৈ ডি-পেডত টিপক"</string>
     <string name="lb_guidedaction_finish_title" msgid="7747913934287176843">"সমাপ্ত"</string>
     <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"অব্যাহত ৰাখক"</string>
diff --git a/leanback/src/main/res/values-bs/strings.xml b/leanback/src/main/res/values-bs/strings.xml
index 6c429a0..dbdfdea 100644
--- a/leanback/src/main/res/values-bs/strings.xml
+++ b/leanback/src/main/res/values-bs/strings.xml
@@ -55,5 +55,5 @@
     <string name="lb_guidedaction_continue_title" msgid="1122271825827282965">"Nastavi"</string>
     <string name="lb_media_player_error" msgid="8748646000835486516">"Kôd greške MediaPlayera %1$d dodatno %2$d"</string>
     <string name="lb_onboarding_get_started" msgid="7674487829030291492">"ZAPOČNITE"</string>
-    <string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"Naprijed"</string>
+    <string name="lb_onboarding_accessibility_next" msgid="4213611627196077555">"Dalje"</string>
 </resources>
diff --git a/leanback/src/main/res/values-ne/strings.xml b/leanback/src/main/res/values-ne/strings.xml
index 5cf37cd..416fc65 100644
--- a/leanback/src/main/res/values-ne/strings.xml
+++ b/leanback/src/main/res/values-ne/strings.xml
@@ -47,7 +47,7 @@
     <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"उच्च गुणस्तरलाई असक्षम पार्नुहोस्"</string>
     <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"उप शीर्षकहरू देखाउने सुविधालाई सक्षम पार्नुहोस्"</string>
     <string name="lb_playback_controls_closed_captioning_disable" msgid="5508271941331836786">"उप शीर्षकहरू देखाउने सुविधालाई असक्षम पार्नुहोस्"</string>
-    <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"तस्बिरभित्र तस्बिर नामक मोडमा प्रविष्टि गर्नुहोस्"</string>
+    <string name="lb_playback_controls_picture_in_picture" msgid="8800305194045609275">"तस्बिरभित्र तस्बिर नामक मोडमा प्रविष्ट गर्नुहोस्"</string>
     <string name="lb_playback_time_separator" msgid="6549544638083578695">"/"</string>
     <string name="lb_playback_controls_shown" msgid="7794717158616536936">"मिडियाका नियन्त्रणहरू देखाइएका छन्"</string>
     <string name="lb_playback_controls_hidden" msgid="619396299825306757">"मिडियाका नियन्त्रणहरूलाई लुकाइएको छ, देखाउनका लागि d-pad नामक बटन थिच्नुहोस्"</string>
diff --git a/leanback/src/main/res/values-ta/strings.xml b/leanback/src/main/res/values-ta/strings.xml
index d0b2641..10c84ca 100644
--- a/leanback/src/main/res/values-ta/strings.xml
+++ b/leanback/src/main/res/values-ta/strings.xml
@@ -41,8 +41,8 @@
     <string name="lb_playback_controls_repeat_none" msgid="5812341701962930499">"எதையும் மீண்டும் இயக்காதே"</string>
     <string name="lb_playback_controls_repeat_all" msgid="5164826436271322261">"அனைத்தையும் மீண்டும் இயக்கு"</string>
     <string name="lb_playback_controls_repeat_one" msgid="7675097479246139440">"ஒன்றை மட்டும் மீண்டும் இயக்கு"</string>
-    <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"வரிசை மாற்றி இயக்கு"</string>
-    <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"வரிசை மாற்றி இயக்குவதை நிறுத்து"</string>
+    <string name="lb_playback_controls_shuffle_enable" msgid="7809089255981448519">"கலைத்து இயக்கு"</string>
+    <string name="lb_playback_controls_shuffle_disable" msgid="8182435535948303910">"கலைக்காமல் இயக்கு"</string>
     <string name="lb_playback_controls_high_quality_enable" msgid="1862669142355962638">"உயர்தரத்தை இயக்கு"</string>
     <string name="lb_playback_controls_high_quality_disable" msgid="3000046054608531995">"உயர்தரத்தை முடக்கு"</string>
     <string name="lb_playback_controls_closed_captioning_enable" msgid="3934392140182327163">"விரிவான வசனங்களை இயக்கு"</string>
diff --git a/media2/src/main/res/values-as/strings.xml b/media2/src/main/res/values-as/strings.xml
deleted file mode 100644
index 73e9775..0000000
--- a/media2/src/main/res/values-as/strings.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 
-  ~ Copyright 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.
-   -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="default_notification_channel_name" msgid="3980628489251198480">"এতিয়া প্লে’ হৈ আছে"</string>
-    <string name="play_button_content_description" msgid="5898492519310326971">"প্লে\'"</string>
-    <string name="pause_button_content_description" msgid="7844788760550939526">"পজ"</string>
-    <string name="skip_to_previous_item_button_content_description" msgid="250823119142607886">"পূৰ্বৱৰ্তী বস্তুটোলৈ যাওক"</string>
-    <string name="skip_to_next_item_button_content_description" msgid="3508534122676007656">"পৰৱৰ্তী বস্তুটোলৈ যাওক"</string>
-</resources>
diff --git a/mediarouter/src/main/res/values-pa/strings.xml b/mediarouter/src/main/res/values-pa/strings.xml
index 46a04e3..71224a6 100644
--- a/mediarouter/src/main/res/values-pa/strings.xml
+++ b/mediarouter/src/main/res/values-pa/strings.xml
@@ -22,7 +22,7 @@
     <string name="mr_cast_button_disconnected" msgid="5501231066847739632">"\'ਕਾਸਟ ਕਰੋ\' ਬਟਨ। ਡਿਸਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string>
     <string name="mr_cast_button_connecting" msgid="8959304318293841992">"\'ਕਾਸਟ ਕਰੋ\' ਬਟਨ। ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
     <string name="mr_cast_button_connected" msgid="1350095112462806159">"\'ਕਾਸਟ ਕਰੋ\' ਬਟਨ। ਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string>
-    <string name="mr_chooser_title" msgid="7548226170787476564">"ਇਸਦੇ ਨਾਲ ਕਾਸਟ ਕਰੋ"</string>
+    <string name="mr_chooser_title" msgid="7548226170787476564">"ਏਥੇ ਕਾਸਟ ਕਰੋ"</string>
     <string name="mr_chooser_searching" msgid="5504553798429329689">"ਡੀਵਾਈਸ ਲੱਭੇ ਜਾ ਰਹੇ ਹਨ"</string>
     <string name="mr_controller_disconnect" msgid="1370654436555555647">"ਡਿਸਕਨੈਕਟ ਕਰੋ"</string>
     <string name="mr_controller_stop_casting" msgid="7617024847862349259">"ਕਾਸਟ ਕਰਨਾ ਬੰਦ ਕਰੋ"</string>
diff --git a/navigation/ui/src/main/res/values-bs/strings.xml b/navigation/ui/src/main/res/values-bs/strings.xml
index f4b93f0..39602a7 100644
--- a/navigation/ui/src/main/res/values-bs/strings.xml
+++ b/navigation/ui/src/main/res/values-bs/strings.xml
@@ -17,6 +17,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="nav_app_bar_open_drawer_description" msgid="7456070600745802113">"Otvaranje ladice za navigaciju"</string>
+    <string name="nav_app_bar_open_drawer_description" msgid="7456070600745802113">"Otvaranje panela za navigaciju"</string>
     <string name="nav_app_bar_navigate_up_description" msgid="6301633601645100427">"Idi gore"</string>
 </resources>
diff --git a/preference/res/values-zh-rTW/strings.xml b/preference/res/values-zh-rTW/strings.xml
index 4628cf0..7787479 100644
--- a/preference/res/values-zh-rTW/strings.xml
+++ b/preference/res/values-zh-rTW/strings.xml
@@ -4,7 +4,7 @@
     <string name="v7_preference_on" msgid="27351710992731591">"開啟"</string>
     <string name="v7_preference_off" msgid="5138405918326871307">"關閉"</string>
     <string name="expand_button_title" msgid="1234962710353108940">"進階"</string>
-    <string name="summary_collapsed_preference_list" msgid="5190123168583152844">"<xliff:g id="CURRENT_ITEMS">%1$s</xliff:g>、<xliff:g id="ADDED_ITEMS">%2$s</xliff:g>"</string>
+    <string name="summary_collapsed_preference_list" msgid="5190123168583152844">"<xliff:g id="CURRENT_ITEMS">%1$s</xliff:g>,<xliff:g id="ADDED_ITEMS">%2$s</xliff:g>"</string>
     <string name="copy" msgid="3209159573327985035">"複製"</string>
     <string name="preference_copied" msgid="7961817945132860002">"已將「<xliff:g id="SUMMARY">%1$s</xliff:g>」複製到剪貼簿。"</string>
     <string name="not_set" msgid="478774118347071097">"未設定"</string>
diff --git a/samples/Support7Demos/build.gradle b/samples/Support7Demos/build.gradle
index 69c4d67..26545a6 100644
--- a/samples/Support7Demos/build.gradle
+++ b/samples/Support7Demos/build.gradle
@@ -18,6 +18,7 @@
         vectorDrawables.useSupportLibrary = true
     }
     lintOptions {
+        disable "WrongThread"
         // TODO: Enable lint after appcompat:1.1.0 release or use lint-baseline.xml instead.
         abortOnError false
     }
diff --git a/settings.gradle b/settings.gradle
index 15004af..021facd 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -272,6 +272,22 @@
 includeProject(":noto-emoji-compat", new File(externalRoot, "noto-fonts/emoji-compat"))
 includeProject(":webview-support-interfaces", new File(externalRoot, "webview_support_interfaces"))
 
+///// CAMERA START
+// TODO(b/124783972): Merge this section with the Libraries section above before release
+
+includeProject(":camera:camera-core", "camera/core")
+includeProject(":camera:camera-camera2", "camera/camera2")
+includeProject(":camera:camera-view", "camera/view")
+includeProject(":camera:camera-extensions", "camera/extensions")
+includeProject(":camera:camera-testing", "camera/testing")
+includeProject(":camera:integration-tests:camera-testapp-core", "camera/integration-tests/coretestapp")
+includeProject(":camera:integration-tests:camera-testapp-extensions", "camera/integration-tests/extensionstestapp")
+includeProject(":camera:integration-tests:camera-testapp-view", "camera/integration-tests/viewtestapp")
+includeProject(":camera:integration-tests:camera-testapp-timing", "camera/integration-tests/timingtestapp")
+includeProject(":camera:integration-tests:camera-testlib-extensions", "camera/integration-tests/extensionstestlib")
+
+///// CAMERA END
+
 // fake project which is used for docs generation from prebuilts
 // we need real android project to generate R.java, aidl etc files that mentioned in sources
 if (!startParameter.projectProperties.containsKey('android.injected.invoked.from.ide')) {
diff --git a/slices/core/src/main/res-public/values-as/strings.xml b/slices/core/src/main/res-public/values-as/strings.xml
deleted file mode 100644
index 5ddcef2..0000000
--- a/slices/core/src/main/res-public/values-as/strings.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="slice_provider" msgid="7510675753826300262">"androidx.slice.compat.SliceProviderCompat"</string>
-</resources>
diff --git a/slices/view/src/main/res/values-as/strings.xml b/slices/view/src/main/res/values-as/strings.xml
index 78cc260..dbc598f 100644
--- a/slices/view/src/main/res/values-as/strings.xml
+++ b/slices/view/src/main/res/values-as/strings.xml
@@ -20,7 +20,7 @@
     <string name="abc_slice_more_content" msgid="6405516388971241142">"+ <xliff:g id="NUMBER">%1$d</xliff:g>"</string>
     <string name="abc_slice_more" msgid="1983560225998630901">"অধিক"</string>
     <string name="abc_slice_show_more" msgid="1567717014004692768">"অধিক দেখুৱাওক"</string>
-    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> আপডে’ট কৰা হৈছিল"</string>
+    <string name="abc_slice_updated" msgid="8155085405396453848">"<xliff:g id="TIME">%1$s</xliff:g> আপডেট কৰা হৈছিল"</string>
     <plurals name="abc_slice_duration_min" formatted="false" msgid="6996334305156847955">
       <item quantity="one"><xliff:g id="ID_2">%d</xliff:g> মিনিট আগেয়ে</item>
       <item quantity="other"><xliff:g id="ID_2">%d</xliff:g> মিনিট আগেয়ে</item>
diff --git a/textclassifier/src/main/res/values-am/strings.xml b/textclassifier/src/main/res/values-am/strings.xml
index 831ac2b..7218771 100644
--- a/textclassifier/src/main/res/values-am/strings.xml
+++ b/textclassifier/src/main/res/values-am/strings.xml
@@ -19,7 +19,7 @@
     <string name="email" msgid="5568050657313893478">"ኢሜይል"</string>
     <string name="email_desc" msgid="6941280589171810022">"ለተመረጡ አድራሻዎች ኢሜይል ላክ"</string>
     <string name="dial" msgid="7317293545368448453">"ደውል"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"ወደ ተመረጠው ስልክ ቁጥር ደውል"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"ወደተመረጠው ስልክ ቁጥር ደውል"</string>
     <string name="browse" msgid="3733970143542020945">"ክፈት"</string>
     <string name="browse_desc" msgid="3898254913938219011">"የተመረጠውን ዩአርኤል ክፈት"</string>
     <string name="sms" msgid="5495416906312064886">"መልዕክት"</string>
diff --git a/textclassifier/src/main/res/values-ar/strings.xml b/textclassifier/src/main/res/values-ar/strings.xml
index 52fb7d7..b2569b9 100644
--- a/textclassifier/src/main/res/values-ar/strings.xml
+++ b/textclassifier/src/main/res/values-ar/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"إرسال بريد إلكتروني"</string>
+    <string name="email" msgid="5568050657313893478">"البريد الإلكتروني"</string>
     <string name="email_desc" msgid="6941280589171810022">"مراسلة العنوان المحدد عبر البريد الإلكتروني"</string>
     <string name="dial" msgid="7317293545368448453">"اتصال"</string>
     <string name="dial_desc" msgid="5129451396208040332">"الاتصال برقم الهاتف المحدد"</string>
     <string name="browse" msgid="3733970143542020945">"فتح"</string>
     <string name="browse_desc" msgid="3898254913938219011">"‏فتح عنوان URL المحدد"</string>
-    <string name="sms" msgid="5495416906312064886">"إرسال رسائل قصيرة"</string>
+    <string name="sms" msgid="5495416906312064886">"رسالة"</string>
     <string name="sms_desc" msgid="8293660783374489324">"مراسلة رقم الهاتف المحدد"</string>
     <string name="add_contact" msgid="9005634177208282449">"إضافة"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"إضافة إلى جهات الاتصال"</string>
diff --git a/textclassifier/src/main/res/values-as/strings.xml b/textclassifier/src/main/res/values-as/strings.xml
deleted file mode 100644
index 7f52871..0000000
--- a/textclassifier/src/main/res/values-as/strings.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--  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.
- -->
-
-<resources xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ইমেইল কৰক"</string>
-    <string name="email_desc" msgid="6941280589171810022">"বাছনি কৰা ঠিকনালৈ ইমেইল পঠিয়াওক"</string>
-    <string name="dial" msgid="7317293545368448453">"কল কৰক"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"বাছনি কৰা ফ\'ন নম্বৰত কল কৰক"</string>
-    <string name="browse" msgid="3733970143542020945">"খোলক"</string>
-    <string name="browse_desc" msgid="3898254913938219011">"বাছনি কৰা URL খোলক"</string>
-    <string name="sms" msgid="5495416906312064886">"বাৰ্তা পঠিয়াওক"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"বাছনি কৰা ফ’ন নম্বৰলৈ বাৰ্তা পঠিয়াওক"</string>
-    <string name="add_contact" msgid="9005634177208282449">"যোগ কৰক"</string>
-    <string name="add_contact_desc" msgid="2475604767309086575">"সর্ম্পকসূচীত যোগ কৰক"</string>
-    <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"অধিক বিকল্প"</string>
-    <string name="floating_toolbar_close_overflow_description" msgid="6243666280435354232">"অভাৰফ্ল\' বন্ধ কৰক"</string>
-    <string name="abc_share" msgid="7091841667818715717">"শ্বেয়াৰ কৰক"</string>
-</resources>
diff --git a/textclassifier/src/main/res/values-az/strings.xml b/textclassifier/src/main/res/values-az/strings.xml
index b8de8f8..e8b583e 100644
--- a/textclassifier/src/main/res/values-az/strings.xml
+++ b/textclassifier/src/main/res/values-az/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"E-poçt yazın"</string>
+    <string name="email" msgid="5568050657313893478">"E-poçt"</string>
     <string name="email_desc" msgid="6941280589171810022">"Seçilmiş ünvana e-məktub yazın"</string>
     <string name="dial" msgid="7317293545368448453">"Zəng edin"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Seçilmiş telefon nömrəsinə zəng edin"</string>
     <string name="browse" msgid="3733970143542020945">"Açın"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Seçilmiş linki açın"</string>
-    <string name="sms" msgid="5495416906312064886">"Mesaj yazın"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Seçilmiş telefon nömrəsinə mesaj göndərin"</string>
+    <string name="sms" msgid="5495416906312064886">"Mesaj"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Seçilmiş telefon nömrəsini mesajla göndərin"</string>
     <string name="add_contact" msgid="9005634177208282449">"Əlavə edin"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Kontakta əlavə edin"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Digər seçimlər"</string>
diff --git a/textclassifier/src/main/res/values-bs/strings.xml b/textclassifier/src/main/res/values-bs/strings.xml
index 0ad050a..0bc610f8 100644
--- a/textclassifier/src/main/res/values-bs/strings.xml
+++ b/textclassifier/src/main/res/values-bs/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Pošalji e-poruku"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Pošalji e-poruku na odabranu adresu"</string>
-    <string name="dial" msgid="7317293545368448453">"Pozovi"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Pozovi odabrani broj telefona"</string>
+    <string name="email" msgid="5568050657313893478">"E-pošta"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Pošaljite e-poruku na odabrane adrese"</string>
+    <string name="dial" msgid="7317293545368448453">"Poziv"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Pozovite odabrani broj telefona"</string>
     <string name="browse" msgid="3733970143542020945">"Otvori"</string>
-    <string name="browse_desc" msgid="3898254913938219011">"Otvori odabrani URL"</string>
-    <string name="sms" msgid="5495416906312064886">"Pošalji SMS"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Pošalji SMS odabranom broju telefona"</string>
+    <string name="browse_desc" msgid="3898254913938219011">"Otvorite odabrani URL"</string>
+    <string name="sms" msgid="5495416906312064886">"Poruka"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Pošaljite poruku odabranom broju telefona"</string>
     <string name="add_contact" msgid="9005634177208282449">"Dodaj"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Dodaj u kontakte"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Više opcija"</string>
diff --git a/textclassifier/src/main/res/values-ca/strings.xml b/textclassifier/src/main/res/values-ca/strings.xml
index e4932cb..0f2cf5ab 100644
--- a/textclassifier/src/main/res/values-ca/strings.xml
+++ b/textclassifier/src/main/res/values-ca/strings.xml
@@ -22,7 +22,7 @@
     <string name="dial_desc" msgid="5129451396208040332">"Truca al número de telèfon seleccionat"</string>
     <string name="browse" msgid="3733970143542020945">"Obre"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Obre l\'URL seleccionat"</string>
-    <string name="sms" msgid="5495416906312064886">"Envia un SMS"</string>
+    <string name="sms" msgid="5495416906312064886">"Missatge"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Envia un missatge al número de telèfon seleccionat"</string>
     <string name="add_contact" msgid="9005634177208282449">"Afegeix"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Afegeix als contactes"</string>
diff --git a/textclassifier/src/main/res/values-cs/strings.xml b/textclassifier/src/main/res/values-cs/strings.xml
index 757a68d..cc7360f 100644
--- a/textclassifier/src/main/res/values-cs/strings.xml
+++ b/textclassifier/src/main/res/values-cs/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Poslat e-mail"</string>
+    <string name="email" msgid="5568050657313893478">"E‑mail"</string>
     <string name="email_desc" msgid="6941280589171810022">"Napsat na vybranou e‑mailovou adresu"</string>
-    <string name="dial" msgid="7317293545368448453">"Zavolat"</string>
+    <string name="dial" msgid="7317293545368448453">"Volat"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Zavolat na vybrané telefonní číslo"</string>
     <string name="browse" msgid="3733970143542020945">"Otevřít"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Otevřít vybranou adresu URL"</string>
-    <string name="sms" msgid="5495416906312064886">"Napsat zprávu"</string>
+    <string name="sms" msgid="5495416906312064886">"Zpráva"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Napsat SMS na vybrané telefonní číslo"</string>
     <string name="add_contact" msgid="9005634177208282449">"Přidat"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Přidat do kontaktů"</string>
diff --git a/textclassifier/src/main/res/values-da/strings.xml b/textclassifier/src/main/res/values-da/strings.xml
index d519fc9..50e7f3d 100644
--- a/textclassifier/src/main/res/values-da/strings.xml
+++ b/textclassifier/src/main/res/values-da/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Send mail"</string>
+    <string name="email" msgid="5568050657313893478">"Mail"</string>
     <string name="email_desc" msgid="6941280589171810022">"Send en mail til den valgte adresse"</string>
-    <string name="dial" msgid="7317293545368448453">"Ring op"</string>
+    <string name="dial" msgid="7317293545368448453">"Opkald"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Ring til det valgte telefonnummer"</string>
     <string name="browse" msgid="3733970143542020945">"Åbn"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Åbn den valgte webadresse"</string>
-    <string name="sms" msgid="5495416906312064886">"Send besked"</string>
+    <string name="sms" msgid="5495416906312064886">"Besked"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Send en besked til det valgte telefonnummer"</string>
     <string name="add_contact" msgid="9005634177208282449">"Tilføj"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Føj til kontakter"</string>
diff --git a/textclassifier/src/main/res/values-de/strings.xml b/textclassifier/src/main/res/values-de/strings.xml
index 463609c..373ba36 100644
--- a/textclassifier/src/main/res/values-de/strings.xml
+++ b/textclassifier/src/main/res/values-de/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"E-Mail senden"</string>
+    <string name="email" msgid="5568050657313893478">"E-Mail"</string>
     <string name="email_desc" msgid="6941280589171810022">"E-Mail an ausgewählte Adresse senden"</string>
     <string name="dial" msgid="7317293545368448453">"Anruf"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Ausgewählte Telefonnummer anrufen"</string>
     <string name="browse" msgid="3733970143542020945">"Öffnen"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Ausgewählte URL öffnen"</string>
-    <string name="sms" msgid="5495416906312064886">"SMS senden"</string>
+    <string name="sms" msgid="5495416906312064886">"SMS"</string>
     <string name="sms_desc" msgid="8293660783374489324">"SMS an ausgewählte Telefonnummer senden"</string>
     <string name="add_contact" msgid="9005634177208282449">"Hinzufügen"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Zu Kontakten hinzufügen"</string>
diff --git a/textclassifier/src/main/res/values-es/strings.xml b/textclassifier/src/main/res/values-es/strings.xml
index dd4c644..007e62e 100644
--- a/textclassifier/src/main/res/values-es/strings.xml
+++ b/textclassifier/src/main/res/values-es/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Enviar correo"</string>
+    <string name="email" msgid="5568050657313893478">"Correo electrónico"</string>
     <string name="email_desc" msgid="6941280589171810022">"Enviar un correo electrónico a la dirección seleccionada"</string>
     <string name="dial" msgid="7317293545368448453">"Llamar"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Llamar al número de teléfono seleccionado"</string>
     <string name="browse" msgid="3733970143542020945">"Abrir"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Abrir la URL seleccionada"</string>
-    <string name="sms" msgid="5495416906312064886">"Enviar SMS"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Enviar SMS al teléfono seleccionado"</string>
+    <string name="sms" msgid="5495416906312064886">"Mensaje"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Enviar un mensaje al número de teléfono seleccionado"</string>
     <string name="add_contact" msgid="9005634177208282449">"Añadir"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Añadir a contactos"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Más opciones"</string>
diff --git a/textclassifier/src/main/res/values-et/strings.xml b/textclassifier/src/main/res/values-et/strings.xml
index b4b2e29..da2ecfb 100644
--- a/textclassifier/src/main/res/values-et/strings.xml
+++ b/textclassifier/src/main/res/values-et/strings.xml
@@ -16,16 +16,16 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Saada meil"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Saada valitud aadressile meil"</string>
-    <string name="dial" msgid="7317293545368448453">"Helista"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Helista valitud telefoninumbrile"</string>
+    <string name="email" msgid="5568050657313893478">"E-post"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Valitud aadressile meili saatmine"</string>
+    <string name="dial" msgid="7317293545368448453">"Kõne"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Valitud telefoninumbrile helistamine"</string>
     <string name="browse" msgid="3733970143542020945">"Ava"</string>
-    <string name="browse_desc" msgid="3898254913938219011">"Ava valitud URL"</string>
-    <string name="sms" msgid="5495416906312064886">"Saada sõnum"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Saada valitud telefoninumbrile sõnum"</string>
+    <string name="browse_desc" msgid="3898254913938219011">"Valitud URL-i avamine"</string>
+    <string name="sms" msgid="5495416906312064886">"Sõnum"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Valitud telefoninumbrile sõnumi saatmine"</string>
     <string name="add_contact" msgid="9005634177208282449">"Lisa"</string>
-    <string name="add_contact_desc" msgid="2475604767309086575">"Lisa kontaktidesse"</string>
+    <string name="add_contact_desc" msgid="2475604767309086575">"Lisamine kontaktidesse"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Rohkem valikuid"</string>
     <string name="floating_toolbar_close_overflow_description" msgid="6243666280435354232">"Ületäite sulgemine"</string>
     <string name="abc_share" msgid="7091841667818715717">"Jaga"</string>
diff --git a/textclassifier/src/main/res/values-eu/strings.xml b/textclassifier/src/main/res/values-eu/strings.xml
index 5b98777..0cfaf81 100644
--- a/textclassifier/src/main/res/values-eu/strings.xml
+++ b/textclassifier/src/main/res/values-eu/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Bidali mezu bat"</string>
+    <string name="email" msgid="5568050657313893478">"Bidali mezu elektroniko bat"</string>
     <string name="email_desc" msgid="6941280589171810022">"Bidali mezu elektroniko bat hautatutako helbidera"</string>
     <string name="dial" msgid="7317293545368448453">"Deitu"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Deitu hautatutako telefono-zenbakira"</string>
diff --git a/textclassifier/src/main/res/values-fa/strings.xml b/textclassifier/src/main/res/values-fa/strings.xml
index 0c01dc6..f9d259f 100644
--- a/textclassifier/src/main/res/values-fa/strings.xml
+++ b/textclassifier/src/main/res/values-fa/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"فرستادن ایمیل"</string>
+    <string name="email" msgid="5568050657313893478">"ایمیل"</string>
     <string name="email_desc" msgid="6941280589171810022">"ارسال ایمیل به نشانی انتخابی"</string>
-    <string name="dial" msgid="7317293545368448453">"تماس گرفتن"</string>
+    <string name="dial" msgid="7317293545368448453">"تماس"</string>
     <string name="dial_desc" msgid="5129451396208040332">"تماس با شماره تلفن انتخابی"</string>
     <string name="browse" msgid="3733970143542020945">"باز کردن"</string>
     <string name="browse_desc" msgid="3898254913938219011">"باز کردن نشانی وب انتخابی"</string>
-    <string name="sms" msgid="5495416906312064886">"فرستادن پیام"</string>
+    <string name="sms" msgid="5495416906312064886">"پیام"</string>
     <string name="sms_desc" msgid="8293660783374489324">"ارسال پیام به شماره تلفن انتخابی"</string>
     <string name="add_contact" msgid="9005634177208282449">"افزودن"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"افزودن به مخاطبین"</string>
diff --git a/textclassifier/src/main/res/values-fr-rCA/strings.xml b/textclassifier/src/main/res/values-fr-rCA/strings.xml
index d56c13f..d1ff08c 100644
--- a/textclassifier/src/main/res/values-fr-rCA/strings.xml
+++ b/textclassifier/src/main/res/values-fr-rCA/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Envoyer un courriel"</string>
+    <string name="email" msgid="5568050657313893478">"Courriel"</string>
     <string name="email_desc" msgid="6941280589171810022">"Envoyer un courriel à l\'adresse sélectionnée"</string>
     <string name="dial" msgid="7317293545368448453">"Appeler"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Téléphoner au numéro sélectionné"</string>
     <string name="browse" msgid="3733970143542020945">"Ouvrir"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Ouvrir l\'adresse URL sélectionnée"</string>
-    <string name="sms" msgid="5495416906312064886">"Envoyer un texto"</string>
+    <string name="sms" msgid="5495416906312064886">"Message"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Envoyer un message texte au numéro de téléphone sélectionné"</string>
     <string name="add_contact" msgid="9005634177208282449">"Ajouter"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Ajouter aux contacts"</string>
diff --git a/textclassifier/src/main/res/values-fr/strings.xml b/textclassifier/src/main/res/values-fr/strings.xml
index 8fc7a9a..87beba5 100644
--- a/textclassifier/src/main/res/values-fr/strings.xml
+++ b/textclassifier/src/main/res/values-fr/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Envoyer un e-mail"</string>
+    <string name="email" msgid="5568050657313893478">"E-mail"</string>
     <string name="email_desc" msgid="6941280589171810022">"Envoyer un e-mail à l\'adresse sélectionnée"</string>
     <string name="dial" msgid="7317293545368448453">"Appeler"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Appeler le numéro de téléphone sélectionné"</string>
     <string name="browse" msgid="3733970143542020945">"Ouvrir"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Ouvrir l\'URL sélectionnée"</string>
-    <string name="sms" msgid="5495416906312064886">"Envoyer un SMS"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Envoyer un SMS au numéro de téléphone sélectionné"</string>
+    <string name="sms" msgid="5495416906312064886">"Message"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Envoyer un message au numéro de téléphone sélectionné"</string>
     <string name="add_contact" msgid="9005634177208282449">"Ajouter"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Ajouter aux contacts"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Autres options"</string>
diff --git a/textclassifier/src/main/res/values-gl/strings.xml b/textclassifier/src/main/res/values-gl/strings.xml
index 2c4385c..b17e574 100644
--- a/textclassifier/src/main/res/values-gl/strings.xml
+++ b/textclassifier/src/main/res/values-gl/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Enviar correo e."</string>
+    <string name="email" msgid="5568050657313893478">"Correo electrónico"</string>
     <string name="email_desc" msgid="6941280589171810022">"Envía un correo electrónico ao enderezo seleccionado"</string>
     <string name="dial" msgid="7317293545368448453">"Chamar"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Chama ao número de teléfono seleccionado"</string>
     <string name="browse" msgid="3733970143542020945">"Abrir"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Abre o URL seleccionado"</string>
-    <string name="sms" msgid="5495416906312064886">"Enviar SMS"</string>
+    <string name="sms" msgid="5495416906312064886">"Enviar mensaxe"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Envía unha mensaxe ao número de teléfono seleccionado"</string>
     <string name="add_contact" msgid="9005634177208282449">"Engadir"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Engade o elemento aos contactos"</string>
diff --git a/textclassifier/src/main/res/values-gu/strings.xml b/textclassifier/src/main/res/values-gu/strings.xml
index c92cbd17..84ec7d3 100644
--- a/textclassifier/src/main/res/values-gu/strings.xml
+++ b/textclassifier/src/main/res/values-gu/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ઇમેઇલ કરો"</string>
+    <string name="email" msgid="5568050657313893478">"ઇમેઇલ"</string>
     <string name="email_desc" msgid="6941280589171810022">"પસંદ કરેલા ઍડ્રેસ પર ઇમેઇલ મોકલો"</string>
-    <string name="dial" msgid="7317293545368448453">"કૉલ કરો"</string>
+    <string name="dial" msgid="7317293545368448453">"કૉલ"</string>
     <string name="dial_desc" msgid="5129451396208040332">"પસંદ કરેલા ફોન નંબર પર કૉલ કરો"</string>
     <string name="browse" msgid="3733970143542020945">"ખોલો"</string>
     <string name="browse_desc" msgid="3898254913938219011">"પસંદ કરેલું URL ખોલો"</string>
-    <string name="sms" msgid="5495416906312064886">"સંદેશ મોકલો"</string>
+    <string name="sms" msgid="5495416906312064886">"સંદેશ"</string>
     <string name="sms_desc" msgid="8293660783374489324">"પસંદ કરેલા ફોન નંબર પર સંદેશ મોકલો"</string>
     <string name="add_contact" msgid="9005634177208282449">"ઉમેરો"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"સંપર્કોમાં ઉમેરો"</string>
diff --git a/textclassifier/src/main/res/values-hi/strings.xml b/textclassifier/src/main/res/values-hi/strings.xml
index e9cf510..0d3da9e2 100644
--- a/textclassifier/src/main/res/values-hi/strings.xml
+++ b/textclassifier/src/main/res/values-hi/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ईमेल करें"</string>
+    <string name="email" msgid="5568050657313893478">"ईमेल"</string>
     <string name="email_desc" msgid="6941280589171810022">"चुने गए पते पर ईमेल भेजें"</string>
     <string name="dial" msgid="7317293545368448453">"कॉल करें"</string>
     <string name="dial_desc" msgid="5129451396208040332">"चुने गए फ़ोन नंबर पर कॉल करें"</string>
     <string name="browse" msgid="3733970143542020945">"खोलें"</string>
     <string name="browse_desc" msgid="3898254913938219011">"चुना गया यूआरएल खोलें"</string>
-    <string name="sms" msgid="5495416906312064886">"मैसेज करें"</string>
+    <string name="sms" msgid="5495416906312064886">"मैसेज"</string>
     <string name="sms_desc" msgid="8293660783374489324">"चुने गए फ़ोन नंबर को मैसेज करें"</string>
     <string name="add_contact" msgid="9005634177208282449">"जोड़ें"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"संपर्क सूची में जोड़ें"</string>
diff --git a/textclassifier/src/main/res/values-hr/strings.xml b/textclassifier/src/main/res/values-hr/strings.xml
index 0a05d23..197bf47 100644
--- a/textclassifier/src/main/res/values-hr/strings.xml
+++ b/textclassifier/src/main/res/values-hr/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Pošalji e-poštu"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Pošalji e-poštu na odabranu adresu"</string>
-    <string name="dial" msgid="7317293545368448453">"Nazovi"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Nazovi odabrani telefonski broj"</string>
+    <string name="email" msgid="5568050657313893478">"E-pošta"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Slanje e-poruke na odabranu adresu"</string>
+    <string name="dial" msgid="7317293545368448453">"Poziv"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Pozivanje odabranog telefonskog broja"</string>
     <string name="browse" msgid="3733970143542020945">"Otvori"</string>
-    <string name="browse_desc" msgid="3898254913938219011">"Otvori odabrani URL"</string>
-    <string name="sms" msgid="5495416906312064886">"Pošalji poruku"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Pošalji poruku na odabrani telefonski broj"</string>
+    <string name="browse_desc" msgid="3898254913938219011">"Otvaranje odabranog URL-a"</string>
+    <string name="sms" msgid="5495416906312064886">"Poruka"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Slanje poruke na odabrani telefonski broj"</string>
     <string name="add_contact" msgid="9005634177208282449">"Dodaj"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Dodaj u kontakte"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Više opcija"</string>
diff --git a/textclassifier/src/main/res/values-hu/strings.xml b/textclassifier/src/main/res/values-hu/strings.xml
index 59d8bf4..b58ea3c 100644
--- a/textclassifier/src/main/res/values-hu/strings.xml
+++ b/textclassifier/src/main/res/values-hu/strings.xml
@@ -17,7 +17,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="email" msgid="5568050657313893478">"E-mail"</string>
-    <string name="email_desc" msgid="6941280589171810022">"E-mail küldése a kiválasztott címre"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Kiválasztott cím elküldése e-mailben"</string>
     <string name="dial" msgid="7317293545368448453">"Hívás"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Kiválasztott telefonszám hívása"</string>
     <string name="browse" msgid="3733970143542020945">"Megnyitás"</string>
diff --git a/textclassifier/src/main/res/values-in/strings.xml b/textclassifier/src/main/res/values-in/strings.xml
index 85dbb52..ea214c8 100644
--- a/textclassifier/src/main/res/values-in/strings.xml
+++ b/textclassifier/src/main/res/values-in/strings.xml
@@ -17,13 +17,13 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="email" msgid="5568050657313893478">"Email"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Kirim email ke alamat yang dipilih"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Mengirimkan email ke alamat yang dipilih"</string>
     <string name="dial" msgid="7317293545368448453">"Panggil"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Panggil nomor telepon yang dipilih"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Memanggil nomor telepon yang dipilih"</string>
     <string name="browse" msgid="3733970143542020945">"Buka"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Buka URL yang dipilih"</string>
     <string name="sms" msgid="5495416906312064886">"Pesan"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Kirim SMS ke nomor telepon yang dipilih"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Mengirimkan SMS ke nomor telepon yang dipilih"</string>
     <string name="add_contact" msgid="9005634177208282449">"Tambahkan"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Tambahkan ke kontak"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Opsi lain"</string>
diff --git a/textclassifier/src/main/res/values-iw/strings.xml b/textclassifier/src/main/res/values-iw/strings.xml
index b577732..84df5c1 100644
--- a/textclassifier/src/main/res/values-iw/strings.xml
+++ b/textclassifier/src/main/res/values-iw/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"התכתבות באימייל"</string>
+    <string name="email" msgid="5568050657313893478">"אימייל"</string>
     <string name="email_desc" msgid="6941280589171810022">"שליחת אימייל לכתובת שנבחרה"</string>
-    <string name="dial" msgid="7317293545368448453">"ביצוע שיחה"</string>
+    <string name="dial" msgid="7317293545368448453">"שיחה"</string>
     <string name="dial_desc" msgid="5129451396208040332">"התקשרות למספר הטלפון שנבחר"</string>
     <string name="browse" msgid="3733970143542020945">"פתיחה"</string>
     <string name="browse_desc" msgid="3898254913938219011">"פתיחה של כתובת האתר שנבחרה"</string>
-    <string name="sms" msgid="5495416906312064886">"התכתבות בהודעות"</string>
+    <string name="sms" msgid="5495416906312064886">"הודעה"</string>
     <string name="sms_desc" msgid="8293660783374489324">"שליחת הודעה למספר הטלפון שנבחר"</string>
     <string name="add_contact" msgid="9005634177208282449">"הוספה"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"הוספה לאנשי הקשר"</string>
diff --git a/textclassifier/src/main/res/values-kk/strings.xml b/textclassifier/src/main/res/values-kk/strings.xml
index 70a0090..49cae0b 100644
--- a/textclassifier/src/main/res/values-kk/strings.xml
+++ b/textclassifier/src/main/res/values-kk/strings.xml
@@ -16,16 +16,16 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Эл. поштаны ашу"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Таңдалған мекенжайға хабар жіберу"</string>
+    <string name="email" msgid="5568050657313893478">"Электрондық пошта"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Таңдалған электрондық пошта мекенжайына хабар жіберу"</string>
     <string name="dial" msgid="7317293545368448453">"Қоңырау шалу"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Таңдалған телефон нөміріне қоңырау шалу"</string>
     <string name="browse" msgid="3733970143542020945">"Ашу"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Таңдалған URL мекенжайын ашу"</string>
-    <string name="sms" msgid="5495416906312064886">"Хабар жіберу"</string>
+    <string name="sms" msgid="5495416906312064886">"Хабар"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Таңдалған телефон нөміріне хабар жіберу"</string>
-    <string name="add_contact" msgid="9005634177208282449">"Енгізу"</string>
-    <string name="add_contact_desc" msgid="2475604767309086575">"Контактілер тізіміне енгізу"</string>
+    <string name="add_contact" msgid="9005634177208282449">"Қосу"</string>
+    <string name="add_contact_desc" msgid="2475604767309086575">"Контактілерге қосу"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Басқа опциялар"</string>
     <string name="floating_toolbar_close_overflow_description" msgid="6243666280435354232">"Қосымша мәзірді жабу"</string>
     <string name="abc_share" msgid="7091841667818715717">"Бөлісу"</string>
diff --git a/textclassifier/src/main/res/values-km/strings.xml b/textclassifier/src/main/res/values-km/strings.xml
index e71c64b5..11fcb96 100644
--- a/textclassifier/src/main/res/values-km/strings.xml
+++ b/textclassifier/src/main/res/values-km/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ផ្ញើអ៊ីមែល"</string>
+    <string name="email" msgid="5568050657313893478">"អ៊ីមែល"</string>
     <string name="email_desc" msgid="6941280589171810022">"ផ្ញើ​អ៊ីមែល​ទៅ​អាសយដ្ឋាន​ដែល​បាន​ជ្រើសរើស"</string>
-    <string name="dial" msgid="7317293545368448453">"ហៅទូរសព្ទ"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"ហៅ​ទូរសព្ទ​ទៅ​លេខ​ដែល​បាន​ជ្រើសរើស"</string>
+    <string name="dial" msgid="7317293545368448453">"ហៅ"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"ហៅ​ទៅ​លេខ​ទូរសព្ទដែល​បាន​ជ្រើសរើស"</string>
     <string name="browse" msgid="3733970143542020945">"បើក"</string>
     <string name="browse_desc" msgid="3898254913938219011">"បើក URL ដែល​បាន​ជ្រើសរើស"</string>
-    <string name="sms" msgid="5495416906312064886">"ផ្ញើសារ"</string>
+    <string name="sms" msgid="5495416906312064886">"សារ"</string>
     <string name="sms_desc" msgid="8293660783374489324">"ផ្ញើសារ​ទៅ​លេខ​ទូរសព្ទ​ដែល​បាន​ជ្រើសរើស"</string>
     <string name="add_contact" msgid="9005634177208282449">"បញ្ចូល"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"បញ្ចូល​ទៅ​ក្នុង​ទំនាក់ទំនង"</string>
diff --git a/textclassifier/src/main/res/values-kn/strings.xml b/textclassifier/src/main/res/values-kn/strings.xml
index 14dfb18..57769c7 100644
--- a/textclassifier/src/main/res/values-kn/strings.xml
+++ b/textclassifier/src/main/res/values-kn/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ಇಮೇಲ್ ಮಾಡಿ"</string>
+    <string name="email" msgid="5568050657313893478">"ಇಮೇಲ್"</string>
     <string name="email_desc" msgid="6941280589171810022">"ಆಯ್ಕೆಮಾಡಿದ ವಿಳಾಸಕ್ಕೆ ಇಮೇಲ್‌ ಮಾಡಿ"</string>
-    <string name="dial" msgid="7317293545368448453">"ಕರೆ ಮಾಡಿ"</string>
+    <string name="dial" msgid="7317293545368448453">"ಕರೆ"</string>
     <string name="dial_desc" msgid="5129451396208040332">"ಆಯ್ಕೆಮಾಡಿದ ಫೋನ್ ಸಂಖ್ಯೆಗೆ ಕರೆ ಮಾಡಿ"</string>
     <string name="browse" msgid="3733970143542020945">"ತೆರೆಯಿರಿ"</string>
     <string name="browse_desc" msgid="3898254913938219011">"ಆಯ್ಕೆ ಮಾಡಿದ URL ತೆರೆಯಿರಿ"</string>
-    <string name="sms" msgid="5495416906312064886">"ಸಂದೇಶ ಕಳುಹಿಸಿ"</string>
+    <string name="sms" msgid="5495416906312064886">"ಸಂದೇಶ"</string>
     <string name="sms_desc" msgid="8293660783374489324">"ಆಯ್ಕೆಮಾಡಿದ ಫೋನ್ ಸಂಖ್ಯೆಗೆ ಸಂದೇಶ ಕಳುಹಿಸಿ"</string>
     <string name="add_contact" msgid="9005634177208282449">"ಸೇರಿಸಿ"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"ಸಂಪರ್ಕಗಳಿಗೆ ಸೇರಿಸಿ"</string>
diff --git a/textclassifier/src/main/res/values-ky/strings.xml b/textclassifier/src/main/res/values-ky/strings.xml
index 1ae09c4..f0da54b 100644
--- a/textclassifier/src/main/res/values-ky/strings.xml
+++ b/textclassifier/src/main/res/values-ky/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Электрондук кат жөнөтүү"</string>
+    <string name="email" msgid="5568050657313893478">"Электрондук почта"</string>
     <string name="email_desc" msgid="6941280589171810022">"Тандалган дарекке электрондук кат жөнөтүү"</string>
     <string name="dial" msgid="7317293545368448453">"Чалуу"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Тандалган телефон номерине чалуу"</string>
     <string name="browse" msgid="3733970143542020945">"Ачуу"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Тандалган URL\'ди ачуу"</string>
-    <string name="sms" msgid="5495416906312064886">"Билдирүү жөнөтүү"</string>
+    <string name="sms" msgid="5495416906312064886">"Билдирүү"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Тандалган телефон номерине билдирүү жөнөтүү"</string>
     <string name="add_contact" msgid="9005634177208282449">"Кошуу"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Байланыштарга кошуу"</string>
diff --git a/textclassifier/src/main/res/values-lt/strings.xml b/textclassifier/src/main/res/values-lt/strings.xml
index 8055d7e..f0affdd 100644
--- a/textclassifier/src/main/res/values-lt/strings.xml
+++ b/textclassifier/src/main/res/values-lt/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Rašyti el. laišką"</string>
+    <string name="email" msgid="5568050657313893478">"El. paštas"</string>
     <string name="email_desc" msgid="6941280589171810022">"Siųsti el. laišką pasirinktu adresu"</string>
     <string name="dial" msgid="7317293545368448453">"Skambinti"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Skambinti pasirinktu telefono numeriu"</string>
     <string name="browse" msgid="3733970143542020945">"Atidaryti"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Atidaryti pasirinktą URL"</string>
-    <string name="sms" msgid="5495416906312064886">"Rašyti pranešimą"</string>
+    <string name="sms" msgid="5495416906312064886">"Pranešimas"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Siųsti pranešimą pasirinktu telefono numeriu"</string>
     <string name="add_contact" msgid="9005634177208282449">"Pridėti"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Pridėti prie kontaktų"</string>
diff --git a/textclassifier/src/main/res/values-lv/strings.xml b/textclassifier/src/main/res/values-lv/strings.xml
index 01b7230..b3d2599 100644
--- a/textclassifier/src/main/res/values-lv/strings.xml
+++ b/textclassifier/src/main/res/values-lv/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"E-pasts"</string>
+    <string name="email" msgid="5568050657313893478">"E-pasta ziņojums"</string>
     <string name="email_desc" msgid="6941280589171810022">"Nosūtīt e-pasta ziņojumu uz atlasīto adresi"</string>
     <string name="dial" msgid="7317293545368448453">"Zvans"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Zvanīt uz atlasīto tālruņa numuru"</string>
diff --git a/textclassifier/src/main/res/values-mk/strings.xml b/textclassifier/src/main/res/values-mk/strings.xml
index 39235db..dc5183b 100644
--- a/textclassifier/src/main/res/values-mk/strings.xml
+++ b/textclassifier/src/main/res/values-mk/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Испрати е-пошта"</string>
+    <string name="email" msgid="5568050657313893478">"Е-пошта"</string>
     <string name="email_desc" msgid="6941280589171810022">"Испрати е-порака до избраната адреса"</string>
     <string name="dial" msgid="7317293545368448453">"Повикај"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Повикај го избраниот телефонски број"</string>
     <string name="browse" msgid="3733970143542020945">"Отвори"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Отвори ја избраната URL-адреса"</string>
-    <string name="sms" msgid="5495416906312064886">"Испрати порака"</string>
+    <string name="sms" msgid="5495416906312064886">"Порака"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Испрати порака до избраниот телефонски број"</string>
     <string name="add_contact" msgid="9005634177208282449">"Додај"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Додај во контакти"</string>
diff --git a/textclassifier/src/main/res/values-ml/strings.xml b/textclassifier/src/main/res/values-ml/strings.xml
index b8726a9..1d5d95b 100644
--- a/textclassifier/src/main/res/values-ml/strings.xml
+++ b/textclassifier/src/main/res/values-ml/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ഇമെയിൽ അയയ്ക്കൂ"</string>
+    <string name="email" msgid="5568050657313893478">"ഇമെയിൽ"</string>
     <string name="email_desc" msgid="6941280589171810022">"തിരഞ്ഞെടുത്ത വിലാസത്തിലേക്ക് ഇമെയിൽ അയയ്ക്കുക"</string>
     <string name="dial" msgid="7317293545368448453">"വിളിക്കുക"</string>
     <string name="dial_desc" msgid="5129451396208040332">"തിരഞ്ഞെടുത്ത ഫോൺ നമ്പറിലേക്ക് വിളിക്കുക"</string>
     <string name="browse" msgid="3733970143542020945">"തുറക്കുക"</string>
     <string name="browse_desc" msgid="3898254913938219011">"തിരഞ്ഞെടുത്ത URL തുറക്കുക"</string>
-    <string name="sms" msgid="5495416906312064886">"സന്ദേശം അയയ്ക്കൂ"</string>
+    <string name="sms" msgid="5495416906312064886">"സന്ദേശം"</string>
     <string name="sms_desc" msgid="8293660783374489324">"തിരഞ്ഞെടുത്ത ഫോൺ നമ്പറിലേക്ക് സന്ദേശം അയയ്ക്കുക"</string>
     <string name="add_contact" msgid="9005634177208282449">"ചേർക്കുക"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"കോൺടാക്‌റ്റുകളിലേക്ക് ചേർക്കുക"</string>
diff --git a/textclassifier/src/main/res/values-mn/strings.xml b/textclassifier/src/main/res/values-mn/strings.xml
index 9dfd17b..c4e9d7e 100644
--- a/textclassifier/src/main/res/values-mn/strings.xml
+++ b/textclassifier/src/main/res/values-mn/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Имэйл бичих"</string>
+    <string name="email" msgid="5568050657313893478">"Имэйл"</string>
     <string name="email_desc" msgid="6941280589171810022">"Сонгосон хаяг руу имэйл илгээх"</string>
     <string name="dial" msgid="7317293545368448453">"Залгах"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Сонгосон утасны дугаар руу залгах"</string>
     <string name="browse" msgid="3733970143542020945">"Нээх"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Сонгосон URL-г нээх"</string>
-    <string name="sms" msgid="5495416906312064886">"Мессеж бичих"</string>
+    <string name="sms" msgid="5495416906312064886">"Мессеж"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Сонгосон утасны дугаар руу мессеж илгээх"</string>
     <string name="add_contact" msgid="9005634177208282449">"Нэмэх"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Харилцагчид нэмэх"</string>
diff --git a/textclassifier/src/main/res/values-mr/strings.xml b/textclassifier/src/main/res/values-mr/strings.xml
index 11a2d55..895d1dc 100644
--- a/textclassifier/src/main/res/values-mr/strings.xml
+++ b/textclassifier/src/main/res/values-mr/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ईमेल करा"</string>
+    <string name="email" msgid="5568050657313893478">"ईमेल"</string>
     <string name="email_desc" msgid="6941280589171810022">"निवडलेल्या अॅड्रेसवर ईमेल करा"</string>
     <string name="dial" msgid="7317293545368448453">"कॉल करा"</string>
     <string name="dial_desc" msgid="5129451396208040332">"निवडलेल्या फोन नंबरवर कॉल करा"</string>
     <string name="browse" msgid="3733970143542020945">"उघडा"</string>
     <string name="browse_desc" msgid="3898254913938219011">"निवडलेली URL उघडा"</string>
-    <string name="sms" msgid="5495416906312064886">"मेसेज करा"</string>
+    <string name="sms" msgid="5495416906312064886">"मेसेज"</string>
     <string name="sms_desc" msgid="8293660783374489324">"निवडलेल्या फोन नंबरवर मेसेज करा"</string>
     <string name="add_contact" msgid="9005634177208282449">"जोडा"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"संपर्कांमध्ये जोडा"</string>
diff --git a/textclassifier/src/main/res/values-my/strings.xml b/textclassifier/src/main/res/values-my/strings.xml
index 78edba2..9a159f0 100644
--- a/textclassifier/src/main/res/values-my/strings.xml
+++ b/textclassifier/src/main/res/values-my/strings.xml
@@ -16,16 +16,16 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"အီးမေးလ်ပို့ရန်"</string>
+    <string name="email" msgid="5568050657313893478">"အီးမေးလ်"</string>
     <string name="email_desc" msgid="6941280589171810022">"ရွေးထားသည့် လိပ်စာသို့ အီးမေးလ်ပို့ရန်"</string>
     <string name="dial" msgid="7317293545368448453">"ခေါ်ဆိုရန်"</string>
     <string name="dial_desc" msgid="5129451396208040332">"ရွေးထားသည့် ဖုန်းနံပါတ်ကို ခေါ်ရန်"</string>
     <string name="browse" msgid="3733970143542020945">"ဖွင့်ရန်"</string>
     <string name="browse_desc" msgid="3898254913938219011">"ရွေးထားသည့် URL ကို ဖွင့်ရန်"</string>
-    <string name="sms" msgid="5495416906312064886">"မက်ဆေ့ဂျ်ပို့ရန်"</string>
+    <string name="sms" msgid="5495416906312064886">"မက်ဆေ့ဂျ်"</string>
     <string name="sms_desc" msgid="8293660783374489324">"ရွေးထားသည့် ဖုန်းနံပါတ်ကို မက်ဆေ့ဂျ်ပို့ရန်"</string>
     <string name="add_contact" msgid="9005634177208282449">"ထည့်ရန်"</string>
-    <string name="add_contact_desc" msgid="2475604767309086575">"အဆက်အသွယ်များသို့ ထည့်ရန်"</string>
+    <string name="add_contact_desc" msgid="2475604767309086575">"ထည့်ရန်"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"နောက်ထပ် ရွေးစရာများ"</string>
     <string name="floating_toolbar_close_overflow_description" msgid="6243666280435354232">"အပိုမီနူးကို ပိတ်ရန်"</string>
     <string name="abc_share" msgid="7091841667818715717">"မျှဝေရန်"</string>
diff --git a/textclassifier/src/main/res/values-nb/strings.xml b/textclassifier/src/main/res/values-nb/strings.xml
index 1959f52..31bac6e 100644
--- a/textclassifier/src/main/res/values-nb/strings.xml
+++ b/textclassifier/src/main/res/values-nb/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Send e-post"</string>
+    <string name="email" msgid="5568050657313893478">"E-post"</string>
     <string name="email_desc" msgid="6941280589171810022">"Send e-post til den valgte adressen"</string>
     <string name="dial" msgid="7317293545368448453">"Ring"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Ring det valgte telefonnummeret"</string>
     <string name="browse" msgid="3733970143542020945">"Åpne"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Åpne den valgte nettadressen"</string>
-    <string name="sms" msgid="5495416906312064886">"Send melding"</string>
+    <string name="sms" msgid="5495416906312064886">"Melding"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Send melding til det valgte telefonnummeret"</string>
     <string name="add_contact" msgid="9005634177208282449">"Legg til"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Legg til i kontakter"</string>
diff --git a/textclassifier/src/main/res/values-ne/strings.xml b/textclassifier/src/main/res/values-ne/strings.xml
index 34325fe..e33e441 100644
--- a/textclassifier/src/main/res/values-ne/strings.xml
+++ b/textclassifier/src/main/res/values-ne/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"इमेल गर्नुहोस्"</string>
+    <string name="email" msgid="5568050657313893478">"इमेल"</string>
     <string name="email_desc" msgid="6941280589171810022">"चयन गरिएको ठेगानामा इमेल पठाउनुहोस्"</string>
     <string name="dial" msgid="7317293545368448453">"कल गर्नुहोस्"</string>
     <string name="dial_desc" msgid="5129451396208040332">"चयन गरिएको फोन नम्बरमा कल गर्नुहोस्"</string>
     <string name="browse" msgid="3733970143542020945">"खोल्नुहोस्"</string>
     <string name="browse_desc" msgid="3898254913938219011">"चयन गरिएको URL खोल्नुहोस्"</string>
-    <string name="sms" msgid="5495416906312064886">"सन्देश पठाउनुहोस्"</string>
+    <string name="sms" msgid="5495416906312064886">"सन्देश"</string>
     <string name="sms_desc" msgid="8293660783374489324">"चयन गरिएको फोन नम्बरमा सन्देश पठाउनुहोस्‌"</string>
     <string name="add_contact" msgid="9005634177208282449">"थप्नुहोस्"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"सम्पर्कहरूमा थप्नुहोस्"</string>
diff --git a/textclassifier/src/main/res/values-or/strings.xml b/textclassifier/src/main/res/values-or/strings.xml
index 00a45bb..3454944 100644
--- a/textclassifier/src/main/res/values-or/strings.xml
+++ b/textclassifier/src/main/res/values-or/strings.xml
@@ -18,14 +18,14 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="email" msgid="5568050657313893478">"ଇମେଲ୍"</string>
     <string name="email_desc" msgid="6941280589171810022">"ଚୟନିତ ଠିକଣାକୁ ଇମେଲ୍‌ ପଠାନ୍ତୁ"</string>
-    <string name="dial" msgid="7317293545368448453">"କଲ୍‍"</string>
+    <string name="dial" msgid="7317293545368448453">"କଲ୍ କରନ୍ତୁ"</string>
     <string name="dial_desc" msgid="5129451396208040332">"ଚୟନିତ ଫୋନ୍‌ ନମ୍ବର୍‌କୁ କଲ୍‌ କରନ୍ତୁ"</string>
     <string name="browse" msgid="3733970143542020945">"ଖୋଲନ୍ତୁ"</string>
     <string name="browse_desc" msgid="3898254913938219011">"ଚୟନିତ URL ଖୋଲନ୍ତୁ"</string>
     <string name="sms" msgid="5495416906312064886">"ମେସେଜ୍"</string>
     <string name="sms_desc" msgid="8293660783374489324">"ଚୟନିତ ଫୋନ୍‌ ନମ୍ବର୍‌କୁ ମେସେଜ୍‌ ପଠାନ୍ତୁ"</string>
-    <string name="add_contact" msgid="9005634177208282449">"ଯୋଗ କରନ୍ତୁ"</string>
-    <string name="add_contact_desc" msgid="2475604767309086575">"ଯୋଗାଯୋଗରେ ଯୋଗ କରନ୍ତୁ"</string>
+    <string name="add_contact" msgid="9005634177208282449">"ଯୋଡ଼ନ୍ତୁ"</string>
+    <string name="add_contact_desc" msgid="2475604767309086575">"ଯୋଗାଯୋଗରେ ଯୋଡ଼ନ୍ତୁ"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"ଅଧିକ ବିକଳ୍ପ"</string>
     <string name="floating_toolbar_close_overflow_description" msgid="6243666280435354232">"ଓଭରଫ୍ଲୋ ବନ୍ଦ କରନ୍ତୁ"</string>
     <string name="abc_share" msgid="7091841667818715717">"ଶେୟାର୍ କରନ୍ତୁ"</string>
diff --git a/textclassifier/src/main/res/values-pa/strings.xml b/textclassifier/src/main/res/values-pa/strings.xml
index 004bc9c..6831001 100644
--- a/textclassifier/src/main/res/values-pa/strings.xml
+++ b/textclassifier/src/main/res/values-pa/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ਈਮੇਲ ਖੋਲ੍ਹੋ"</string>
+    <string name="email" msgid="5568050657313893478">"ਈਮੇਲ"</string>
     <string name="email_desc" msgid="6941280589171810022">"ਚੁਣੇ ਹੋਏ ਪਤੇ \'ਤੇ ਈਮੇਲ ਭੇਜੋ"</string>
     <string name="dial" msgid="7317293545368448453">"ਕਾਲ ਕਰੋ"</string>
     <string name="dial_desc" msgid="5129451396208040332">"ਚੁਣੇ ਗਏ ਫ਼ੋਨ ਨੰਬਰ \'ਤੇ ਕਾਲ ਕਰੋ"</string>
     <string name="browse" msgid="3733970143542020945">"ਖੋਲ੍ਹੋ"</string>
     <string name="browse_desc" msgid="3898254913938219011">"ਚੁਣਿਆ ਗਿਆ URL ਖੋਲ੍ਹੋ"</string>
-    <string name="sms" msgid="5495416906312064886">"ਸੁਨੇਹਾ ਖੋਲ੍ਹੋ"</string>
+    <string name="sms" msgid="5495416906312064886">"ਸੁਨੇਹਾ"</string>
     <string name="sms_desc" msgid="8293660783374489324">"ਚੁਣੇ ਗਏ ਫ਼ੋਨ ਨੰਬਰ \'ਤੇ ਸੁਨੇਹਾ ਭੇਜੋ"</string>
     <string name="add_contact" msgid="9005634177208282449">"ਸ਼ਾਮਲ ਕਰੋ"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"ਸੰਪਰਕਾਂ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ"</string>
diff --git a/textclassifier/src/main/res/values-pt-rPT/strings.xml b/textclassifier/src/main/res/values-pt-rPT/strings.xml
index c1ff32b..3eef4f1e 100644
--- a/textclassifier/src/main/res/values-pt-rPT/strings.xml
+++ b/textclassifier/src/main/res/values-pt-rPT/strings.xml
@@ -22,7 +22,7 @@
     <string name="dial_desc" msgid="5129451396208040332">"Ligar para o número de telefone selecionado"</string>
     <string name="browse" msgid="3733970143542020945">"Abrir"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Abrir o URL selecionado"</string>
-    <string name="sms" msgid="5495416906312064886">"Enviar mensagem"</string>
+    <string name="sms" msgid="5495416906312064886">"Mensagem"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Enviar uma mensagem para o número de telefone selecionado"</string>
     <string name="add_contact" msgid="9005634177208282449">"Adicionar"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Adicionar aos contactos"</string>
diff --git a/textclassifier/src/main/res/values-ro/strings.xml b/textclassifier/src/main/res/values-ro/strings.xml
index 3656640..d41f9b7 100644
--- a/textclassifier/src/main/res/values-ro/strings.xml
+++ b/textclassifier/src/main/res/values-ro/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Trimiteți un e-mail"</string>
+    <string name="email" msgid="5568050657313893478">"E-mail"</string>
     <string name="email_desc" msgid="6941280589171810022">"Trimiteți un e-mail la adresa selectată"</string>
-    <string name="dial" msgid="7317293545368448453">"Sunați"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Sunați la numărul de telefon selectat"</string>
+    <string name="dial" msgid="7317293545368448453">"Apelați"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Apelați numărul de telefon selectat"</string>
     <string name="browse" msgid="3733970143542020945">"Deschideți"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Deschideți adresa URL selectată"</string>
-    <string name="sms" msgid="5495416906312064886">"Trimiteți mesaj"</string>
+    <string name="sms" msgid="5495416906312064886">"Mesaj"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Trimiteți un mesaj la numărul de telefon selectat"</string>
     <string name="add_contact" msgid="9005634177208282449">"Adăugați"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Adăugați în agendă"</string>
diff --git a/textclassifier/src/main/res/values-ru/strings.xml b/textclassifier/src/main/res/values-ru/strings.xml
index c3e4610..a34d303 100644
--- a/textclassifier/src/main/res/values-ru/strings.xml
+++ b/textclassifier/src/main/res/values-ru/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Написать письмо"</string>
+    <string name="email" msgid="5568050657313893478">"Написать по почте"</string>
     <string name="email_desc" msgid="6941280589171810022">"Отправить письмо выбранному адресату"</string>
     <string name="dial" msgid="7317293545368448453">"Позвонить"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Позвонить по выбранному номеру"</string>
diff --git a/textclassifier/src/main/res/values-sl/strings.xml b/textclassifier/src/main/res/values-sl/strings.xml
index 9cf16f6..7a9981f 100644
--- a/textclassifier/src/main/res/values-sl/strings.xml
+++ b/textclassifier/src/main/res/values-sl/strings.xml
@@ -16,16 +16,16 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Odpri e-pošto"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Pošlji e-poštno sporočilo na izbrani naslov"</string>
-    <string name="dial" msgid="7317293545368448453">"Pokliči"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Pokliči izbrano telefonsko številko"</string>
-    <string name="browse" msgid="3733970143542020945">"Odpri"</string>
-    <string name="browse_desc" msgid="3898254913938219011">"Odpri izbrani URL"</string>
-    <string name="sms" msgid="5495416906312064886">"Pošlji SMS"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Pošlji SMS na izbrano telefonsko številko"</string>
-    <string name="add_contact" msgid="9005634177208282449">"Dodaj"</string>
-    <string name="add_contact_desc" msgid="2475604767309086575">"Dodaj med stike"</string>
+    <string name="email" msgid="5568050657313893478">"E-pošta"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Pošiljanje e-poštnega sporočila na izbrani naslov"</string>
+    <string name="dial" msgid="7317293545368448453">"Telefonski klic"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Klic izbrane telefonske številke"</string>
+    <string name="browse" msgid="3733970143542020945">"Odpiranje"</string>
+    <string name="browse_desc" msgid="3898254913938219011">"Odpiranje izbranega URL-ja"</string>
+    <string name="sms" msgid="5495416906312064886">"Sporočilo"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Pošiljanje sporočila na izbrano telefonsko številko"</string>
+    <string name="add_contact" msgid="9005634177208282449">"Dodajanje"</string>
+    <string name="add_contact_desc" msgid="2475604767309086575">"Dodajanje med stike"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Več možnosti"</string>
     <string name="floating_toolbar_close_overflow_description" msgid="6243666280435354232">"Zapiranje dodatnih elementov"</string>
     <string name="abc_share" msgid="7091841667818715717">"Skup. raba"</string>
diff --git a/textclassifier/src/main/res/values-sq/strings.xml b/textclassifier/src/main/res/values-sq/strings.xml
index 90f6619..2735efc 100644
--- a/textclassifier/src/main/res/values-sq/strings.xml
+++ b/textclassifier/src/main/res/values-sq/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Dërgo email"</string>
+    <string name="email" msgid="5568050657313893478">"Email"</string>
     <string name="email_desc" msgid="6941280589171810022">"Dërgo email tek adresa e zgjedhur"</string>
     <string name="dial" msgid="7317293545368448453">"Telefono"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Telefono në numrin e zgjedhur të telefonit"</string>
     <string name="browse" msgid="3733970143542020945">"Hap"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Hap URL-në e zgjedhur"</string>
-    <string name="sms" msgid="5495416906312064886">"Dërgo mesazh"</string>
+    <string name="sms" msgid="5495416906312064886">"Mesazh"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Dërgo mesazh te numri i zgjedhur i telefonit"</string>
     <string name="add_contact" msgid="9005634177208282449">"Shto"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Shto te kontaktet"</string>
diff --git a/textclassifier/src/main/res/values-sv/strings.xml b/textclassifier/src/main/res/values-sv/strings.xml
index 8a677c4..483a79f 100644
--- a/textclassifier/src/main/res/values-sv/strings.xml
+++ b/textclassifier/src/main/res/values-sv/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Skicka e-post"</string>
+    <string name="email" msgid="5568050657313893478">"E-post"</string>
     <string name="email_desc" msgid="6941280589171810022">"Skicka e-post till vald adress"</string>
     <string name="dial" msgid="7317293545368448453">"Ring"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Ring valt telefonnummer"</string>
diff --git a/textclassifier/src/main/res/values-sw/strings.xml b/textclassifier/src/main/res/values-sw/strings.xml
index e940614..0171aa6 100644
--- a/textclassifier/src/main/res/values-sw/strings.xml
+++ b/textclassifier/src/main/res/values-sw/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Tuma barua pepe"</string>
+    <string name="email" msgid="5568050657313893478">"Barua pepe"</string>
     <string name="email_desc" msgid="6941280589171810022">"Tuma barua pepe kwa anwani uliyochagua"</string>
-    <string name="dial" msgid="7317293545368448453">"Piga simu"</string>
+    <string name="dial" msgid="7317293545368448453">"Simu"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Piga simu kwa nambari uliyochagua"</string>
     <string name="browse" msgid="3733970143542020945">"Fungua"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Fungua URL uliyochagua"</string>
diff --git a/textclassifier/src/main/res/values-ta/strings.xml b/textclassifier/src/main/res/values-ta/strings.xml
index c90ac79..4f3123b 100644
--- a/textclassifier/src/main/res/values-ta/strings.xml
+++ b/textclassifier/src/main/res/values-ta/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"மின்னஞ்சல் அனுப்பு"</string>
+    <string name="email" msgid="5568050657313893478">"மின்னஞ்சல்"</string>
     <string name="email_desc" msgid="6941280589171810022">"தேர்ந்தெடுத்த முகவரிக்கு மின்னஞ்சலை அனுப்பும்"</string>
     <string name="dial" msgid="7317293545368448453">"அழை"</string>
     <string name="dial_desc" msgid="5129451396208040332">"தேர்ந்தெடுத்த ஃபோன் எண்ணை அழைக்கும்"</string>
     <string name="browse" msgid="3733970143542020945">"திற"</string>
     <string name="browse_desc" msgid="3898254913938219011">"தேர்ந்தெடுத்த URLலைத் திறக்கும்"</string>
-    <string name="sms" msgid="5495416906312064886">"செய்தி அனுப்பு"</string>
+    <string name="sms" msgid="5495416906312064886">"மெசேஜ்"</string>
     <string name="sms_desc" msgid="8293660783374489324">"தேர்ந்தெடுத்த ஃபோன் எண்ணிற்கு மெசேஜ் அனுப்பும்"</string>
     <string name="add_contact" msgid="9005634177208282449">"சேர்"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"தொடர்புகளில் சேர்க்கும்"</string>
diff --git a/textclassifier/src/main/res/values-te/strings.xml b/textclassifier/src/main/res/values-te/strings.xml
index 25e41ff..4adcfbf 100644
--- a/textclassifier/src/main/res/values-te/strings.xml
+++ b/textclassifier/src/main/res/values-te/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ఇమెయిల్ పంపు"</string>
+    <string name="email" msgid="5568050657313893478">"ఇమెయిల్"</string>
     <string name="email_desc" msgid="6941280589171810022">"ఎంచుకున్న చిరునామాకు ఇమెయిల్‌ను పంపుతుంది"</string>
-    <string name="dial" msgid="7317293545368448453">"కాల్ చేయి"</string>
+    <string name="dial" msgid="7317293545368448453">"కాల్"</string>
     <string name="dial_desc" msgid="5129451396208040332">"ఎంచుకున్న ఫోన్ నంబర్‌కు కాల్ చేస్తుంది"</string>
     <string name="browse" msgid="3733970143542020945">"తెరువు"</string>
     <string name="browse_desc" msgid="3898254913938219011">"ఎంచుకున్న URLని తెరుస్తుంది"</string>
-    <string name="sms" msgid="5495416906312064886">"సందేశం పంపు"</string>
+    <string name="sms" msgid="5495416906312064886">"సందేశం"</string>
     <string name="sms_desc" msgid="8293660783374489324">"ఎంచుకున్న ఫోన్ నంబర్‌కి సందేశం పంపుతుంది"</string>
     <string name="add_contact" msgid="9005634177208282449">"జోడించు"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"పరిచయాలకు జోడిస్తుంది"</string>
diff --git a/textclassifier/src/main/res/values-tl/strings.xml b/textclassifier/src/main/res/values-tl/strings.xml
index f75b004..734524e 100644
--- a/textclassifier/src/main/res/values-tl/strings.xml
+++ b/textclassifier/src/main/res/values-tl/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Mag-email"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Mag-email sa piniling address"</string>
-    <string name="dial" msgid="7317293545368448453">"Tumawag"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Tawagan ang piniling numero ng telepono"</string>
+    <string name="email" msgid="5568050657313893478">"Email"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Mag-email sa napiling address"</string>
+    <string name="dial" msgid="7317293545368448453">"Tawag"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Tawagan ang napiling numero ng telepono"</string>
     <string name="browse" msgid="3733970143542020945">"Buksan"</string>
-    <string name="browse_desc" msgid="3898254913938219011">"Buksan ang piniling URL"</string>
-    <string name="sms" msgid="5495416906312064886">"Magmensahe"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"Padalhan ng mensahe ang piniling numero ng telepono"</string>
+    <string name="browse_desc" msgid="3898254913938219011">"Buksan ang napiling URL"</string>
+    <string name="sms" msgid="5495416906312064886">"Mensahe"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"Padalhan ng mensahe ang napiling numero ng telepono"</string>
     <string name="add_contact" msgid="9005634177208282449">"Magdagdag"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Idagdag sa mga contact"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Higit pang opsyon"</string>
diff --git a/textclassifier/src/main/res/values-tr/strings.xml b/textclassifier/src/main/res/values-tr/strings.xml
index 044d43f..be12836 100644
--- a/textclassifier/src/main/res/values-tr/strings.xml
+++ b/textclassifier/src/main/res/values-tr/strings.xml
@@ -18,7 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="email" msgid="5568050657313893478">"E-posta"</string>
     <string name="email_desc" msgid="6941280589171810022">"Seçilen adrese e-posta gönder"</string>
-    <string name="dial" msgid="7317293545368448453">"Telefon et"</string>
+    <string name="dial" msgid="7317293545368448453">"Ara"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Seçilen telefon numarasını ara"</string>
     <string name="browse" msgid="3733970143542020945">"Aç"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Seçilen URL\'yi aç"</string>
diff --git a/textclassifier/src/main/res/values-uk/strings.xml b/textclassifier/src/main/res/values-uk/strings.xml
index a40df77..241021a 100644
--- a/textclassifier/src/main/res/values-uk/strings.xml
+++ b/textclassifier/src/main/res/values-uk/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"Написати лист"</string>
+    <string name="email" msgid="5568050657313893478">"Електронна адреса"</string>
     <string name="email_desc" msgid="6941280589171810022">"Надіслати електронний лист на вибрану адресу"</string>
-    <string name="dial" msgid="7317293545368448453">"Телефонувати"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"Телефонувати за вибраним номером"</string>
+    <string name="dial" msgid="7317293545368448453">"Виклик"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"Набрати вибраний номер телефону"</string>
     <string name="browse" msgid="3733970143542020945">"Відкрити"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Відкрити вибрану URL-адресу"</string>
-    <string name="sms" msgid="5495416906312064886">"Написати SMS"</string>
+    <string name="sms" msgid="5495416906312064886">"Повідомлення"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Надіслати повідомлення за вибраним номером телефону"</string>
     <string name="add_contact" msgid="9005634177208282449">"Додати"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Додати в контакти"</string>
diff --git a/textclassifier/src/main/res/values-ur/strings.xml b/textclassifier/src/main/res/values-ur/strings.xml
index acdb424..55b2e6d 100644
--- a/textclassifier/src/main/res/values-ur/strings.xml
+++ b/textclassifier/src/main/res/values-ur/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"ای میل بھیجیں"</string>
+    <string name="email" msgid="5568050657313893478">"ای میل"</string>
     <string name="email_desc" msgid="6941280589171810022">"منتخب کردہ پتے پر ای میل کریں"</string>
-    <string name="dial" msgid="7317293545368448453">"کال کریں"</string>
+    <string name="dial" msgid="7317293545368448453">"کال"</string>
     <string name="dial_desc" msgid="5129451396208040332">"منتخب کردہ فون نمبر پر کال کریں"</string>
     <string name="browse" msgid="3733970143542020945">"کھولیں"</string>
     <string name="browse_desc" msgid="3898254913938219011">"‏منتخب کردہ URL کھولیں"</string>
-    <string name="sms" msgid="5495416906312064886">"پیغام بھیجیں"</string>
+    <string name="sms" msgid="5495416906312064886">"پیغام"</string>
     <string name="sms_desc" msgid="8293660783374489324">"منتخب کردہ فون نمبر پر پیغام بھیجیں"</string>
     <string name="add_contact" msgid="9005634177208282449">"شامل کریں"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"رابطوں میں شامل کریں"</string>
diff --git a/textclassifier/src/main/res/values-uz/strings.xml b/textclassifier/src/main/res/values-uz/strings.xml
index 3f6b781..74d563d 100644
--- a/textclassifier/src/main/res/values-uz/strings.xml
+++ b/textclassifier/src/main/res/values-uz/strings.xml
@@ -17,14 +17,14 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="email" msgid="5568050657313893478">"E-pochta"</string>
-    <string name="email_desc" msgid="6941280589171810022">"Belgilangan e-pochta manziliga xat yuborish"</string>
+    <string name="email_desc" msgid="6941280589171810022">"Belgilangan manzilga xat yuborish"</string>
     <string name="dial" msgid="7317293545368448453">"Chaqiruv"</string>
     <string name="dial_desc" msgid="5129451396208040332">"Belgilangan raqamga telefon qilish"</string>
     <string name="browse" msgid="3733970143542020945">"Ochish"</string>
     <string name="browse_desc" msgid="3898254913938219011">"Belgilangan URL manzilini ochish"</string>
-    <string name="sms" msgid="5495416906312064886">"SMS yozish"</string>
+    <string name="sms" msgid="5495416906312064886">"Xabar"</string>
     <string name="sms_desc" msgid="8293660783374489324">"Belgilangan telefon raqamiga SMS yuborish"</string>
-    <string name="add_contact" msgid="9005634177208282449">"Saqlab olish"</string>
+    <string name="add_contact" msgid="9005634177208282449">"Qo‘shish"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"Kontaktlarga saqlash"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"Yana"</string>
     <string name="floating_toolbar_close_overflow_description" msgid="6243666280435354232">"Qalqib turuvchi asboblar panelini yopish"</string>
diff --git a/textclassifier/src/main/res/values-zh-rCN/strings.xml b/textclassifier/src/main/res/values-zh-rCN/strings.xml
index afa89ca..a990409 100644
--- a/textclassifier/src/main/res/values-zh-rCN/strings.xml
+++ b/textclassifier/src/main/res/values-zh-rCN/strings.xml
@@ -16,13 +16,13 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"发送电子邮件"</string>
+    <string name="email" msgid="5568050657313893478">"电子邮件"</string>
     <string name="email_desc" msgid="6941280589171810022">"将电子邮件发送至所选地址"</string>
     <string name="dial" msgid="7317293545368448453">"通话"</string>
     <string name="dial_desc" msgid="5129451396208040332">"拨打所选电话号码"</string>
     <string name="browse" msgid="3733970143542020945">"打开"</string>
     <string name="browse_desc" msgid="3898254913938219011">"打开所选网址"</string>
-    <string name="sms" msgid="5495416906312064886">"发短信"</string>
+    <string name="sms" msgid="5495416906312064886">"短信"</string>
     <string name="sms_desc" msgid="8293660783374489324">"将短信发送至所选电话号码"</string>
     <string name="add_contact" msgid="9005634177208282449">"添加"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"添加到通讯录"</string>
diff --git a/textclassifier/src/main/res/values-zh-rHK/strings.xml b/textclassifier/src/main/res/values-zh-rHK/strings.xml
index 2ada5ec..946fd6c 100644
--- a/textclassifier/src/main/res/values-zh-rHK/strings.xml
+++ b/textclassifier/src/main/res/values-zh-rHK/strings.xml
@@ -22,7 +22,7 @@
     <string name="dial_desc" msgid="5129451396208040332">"打指定嘅電話號碼"</string>
     <string name="browse" msgid="3733970143542020945">"開啟"</string>
     <string name="browse_desc" msgid="3898254913938219011">"打開指定網址"</string>
-    <string name="sms" msgid="5495416906312064886">"發短訊"</string>
+    <string name="sms" msgid="5495416906312064886">"訊息"</string>
     <string name="sms_desc" msgid="8293660783374489324">"傳短訊去指定電話號碼"</string>
     <string name="add_contact" msgid="9005634177208282449">"新增"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"加入聯絡人"</string>
diff --git a/textclassifier/src/main/res/values-zh-rTW/strings.xml b/textclassifier/src/main/res/values-zh-rTW/strings.xml
index d2bd3ab..4fc184f 100644
--- a/textclassifier/src/main/res/values-zh-rTW/strings.xml
+++ b/textclassifier/src/main/res/values-zh-rTW/strings.xml
@@ -16,14 +16,14 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="email" msgid="5568050657313893478">"發送電子郵件"</string>
-    <string name="email_desc" msgid="6941280589171810022">"將電子郵件寄到選取的地址"</string>
-    <string name="dial" msgid="7317293545368448453">"撥號通話"</string>
-    <string name="dial_desc" msgid="5129451396208040332">"撥打選取的電話號碼"</string>
+    <string name="email" msgid="5568050657313893478">"電子郵件"</string>
+    <string name="email_desc" msgid="6941280589171810022">"將電子郵件寄到所選地址"</string>
+    <string name="dial" msgid="7317293545368448453">"撥號"</string>
+    <string name="dial_desc" msgid="5129451396208040332">"撥打所選電話號碼"</string>
     <string name="browse" msgid="3733970143542020945">"開啟"</string>
-    <string name="browse_desc" msgid="3898254913938219011">"開啟選取的網址"</string>
-    <string name="sms" msgid="5495416906312064886">"發送訊息"</string>
-    <string name="sms_desc" msgid="8293660783374489324">"將訊息傳送到選取的電話號碼"</string>
+    <string name="browse_desc" msgid="3898254913938219011">"開啟所選網址"</string>
+    <string name="sms" msgid="5495416906312064886">"訊息"</string>
+    <string name="sms_desc" msgid="8293660783374489324">"將訊息傳送到所選電話號碼"</string>
     <string name="add_contact" msgid="9005634177208282449">"新增"</string>
     <string name="add_contact_desc" msgid="2475604767309086575">"新增至聯絡人"</string>
     <string name="floating_toolbar_open_overflow_description" msgid="1187148927509077545">"更多選項"</string>
diff --git a/wear/res/values-bs/strings.xml b/wear/res/values-bs/strings.xml
index cf30702..82c990c 100644
--- a/wear/res/values-bs/strings.xml
+++ b/wear/res/values-bs/strings.xml
@@ -16,6 +16,6 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="ws_navigation_drawer_content_description" msgid="7216697245762194759">"Ladica za navigaciju"</string>
-    <string name="ws_action_drawer_content_description" msgid="1837365417701148489">"Ladica za radnju"</string>
+    <string name="ws_navigation_drawer_content_description" msgid="7216697245762194759">"Panel za navigaciju"</string>
+    <string name="ws_action_drawer_content_description" msgid="1837365417701148489">"Panel za radnju"</string>
 </resources>