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><owner>.[optional.subCategories.]<optionId></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><owner>.[optional.subCategories.]<optionId></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>
+ * <owner>.[optional.subCategories.]<optionId>
+ * </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<Integer> intToken = new TypeReference<Integer>() {{ }};
+ *
+ * // using named classes
+ * class IntTypeReference extends TypeReference<Integer> {...}
+ * TypeReference<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>