From 29b087e5dd3fc583c92caee323ec02130502685f Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 20 Apr 2026 16:11:27 -0700 Subject: [PATCH 01/14] Refactor test fakes into dedicated testing modules --- core/camera/build.gradle.kts | 2 + .../core/camera/CameraXCameraSystemTest.kt | 2 +- core/camera/testing/build.gradle.kts | 64 +++++++++++++++++++ .../core/camera/testing}/FakeCameraSystem.kt | 2 +- .../camera/testing}/FakeCameraSystemTest.kt | 2 +- core/common/testing/build.gradle.kts | 53 +++++++++++++++ .../common/testing}/FakeFilePathGenerator.kt | 3 +- data/media/build.gradle.kts | 3 +- .../media/LocalMediaRepositoryTest.kt | 2 +- data/media/testing/build.gradle.kts | 56 ++++++++++++++++ .../media/testing}/FakeMediaRepository.kt | 5 +- data/settings/build.gradle.kts | 1 + .../settings/DataStoreModuleTest.kt | 4 +- data/settings/testing/build.gradle.kts | 58 +++++++++++++++++ .../settings/testing}/FakeDataStoreModule.kt | 2 +- .../testing}/FakeJcaSettingsSerializer.kt | 2 +- .../testing}/FakeSettingsRepository.kt | 2 +- feature/postcapture/build.gradle.kts | 1 + .../postcapture/PostCaptureViewModelTest.kt | 2 +- feature/preview/build.gradle.kts | 5 +- .../feature/preview/PreviewViewModelTest.kt | 6 +- settings.gradle.kts | 4 ++ ui/components/capture/build.gradle.kts | 1 + .../capture/capture/ScreenFlashTest.kt | 2 +- 24 files changed, 266 insertions(+), 18 deletions(-) create mode 100644 core/camera/testing/build.gradle.kts rename core/camera/{src/main/java/com/google/jetpackcamera/core/camera/test => testing/src/main/java/com/google/jetpackcamera/core/camera/testing}/FakeCameraSystem.kt (99%) rename core/camera/{src/test/java/com/google/jetpackcamera/core/camera/test => testing/src/test/java/com/google/jetpackcamera/core/camera/testing}/FakeCameraSystemTest.kt (99%) create mode 100644 core/common/testing/build.gradle.kts rename core/common/{src/main/java/com/google/jetpackcamera/core/common => testing/src/main/java/com/google/jetpackcamera/core/common/testing}/FakeFilePathGenerator.kt (95%) create mode 100644 data/media/testing/build.gradle.kts rename data/media/{src/main/kotlin/com/google/jetpackcamera/data/media => testing/src/main/kotlin/com/google/jetpackcamera/data/media/testing}/FakeMediaRepository.kt (91%) create mode 100644 data/settings/testing/build.gradle.kts rename data/settings/{src/main/java/com/google/jetpackcamera/settings/test => testing/src/main/java/com/google/jetpackcamera/settings/testing}/FakeDataStoreModule.kt (96%) rename data/settings/{src/main/java/com/google/jetpackcamera/settings/test => testing/src/main/java/com/google/jetpackcamera/settings/testing}/FakeJcaSettingsSerializer.kt (98%) rename data/settings/{src/main/java/com/google/jetpackcamera/settings/test => testing/src/main/java/com/google/jetpackcamera/settings/testing}/FakeSettingsRepository.kt (98%) diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index 1366d1309..0fe482a3b 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -114,6 +114,8 @@ dependencies { androidTestImplementation(libs.androidx.rules) androidTestImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.truth) + androidTestImplementation(project(":core:common:testing")) + testImplementation(project(":core:camera:testing")) // Futures implementation(libs.futures.ktx) diff --git a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt index 464c29055..e90320509 100644 --- a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt +++ b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt @@ -32,7 +32,7 @@ import com.google.jetpackcamera.core.camera.postprocess.ImagePostProcessorFeatur import com.google.jetpackcamera.core.camera.postprocess.PostProcessModule.Companion.provideImagePostProcessorMap import com.google.jetpackcamera.core.camera.utils.APP_REQUIRED_PERMISSIONS import com.google.jetpackcamera.core.camera.utils.provideUpdatingSurface -import com.google.jetpackcamera.core.common.FakeFilePathGenerator +import com.google.jetpackcamera.core.common.testing.FakeFilePathGenerator import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.Illuminant diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts new file mode 100644 index 000000000..e47028143 --- /dev/null +++ b/core/camera/testing/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.google.jetpackcamera.core.camera.testing" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(project(":core:camera")) + implementation(project(":core:model")) + implementation(project(":data:settings")) + + implementation(libs.camera.core) + implementation(libs.kotlinx.coroutines.core) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.robolectric) +} diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraSystem.kt b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt similarity index 99% rename from core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraSystem.kt rename to core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt index 842afcf11..5b19520ce 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/test/FakeCameraSystem.kt +++ b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.core.camera.test +package com.google.jetpackcamera.core.camera.testing import android.annotation.SuppressLint import android.content.ContentResolver diff --git a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/test/FakeCameraSystemTest.kt b/core/camera/testing/src/test/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystemTest.kt similarity index 99% rename from core/camera/src/test/java/com/google/jetpackcamera/core/camera/test/FakeCameraSystemTest.kt rename to core/camera/testing/src/test/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystemTest.kt index e489410e3..9dfd6955b 100644 --- a/core/camera/src/test/java/com/google/jetpackcamera/core/camera/test/FakeCameraSystemTest.kt +++ b/core/camera/testing/src/test/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystemTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.core.camera.test +package com.google.jetpackcamera.core.camera.testing import com.google.common.truth.Truth import com.google.jetpackcamera.core.camera.CameraSystem diff --git a/core/common/testing/build.gradle.kts b/core/common/testing/build.gradle.kts new file mode 100644 index 000000000..d14726ad6 --- /dev/null +++ b/core/common/testing/build.gradle.kts @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.google.jetpackcamera.core.common.testing" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(project(":core:common")) +} diff --git a/core/common/src/main/java/com/google/jetpackcamera/core/common/FakeFilePathGenerator.kt b/core/common/testing/src/main/java/com/google/jetpackcamera/core/common/testing/FakeFilePathGenerator.kt similarity index 95% rename from core/common/src/main/java/com/google/jetpackcamera/core/common/FakeFilePathGenerator.kt rename to core/common/testing/src/main/java/com/google/jetpackcamera/core/common/testing/FakeFilePathGenerator.kt index a2d2e960e..3c7a65097 100644 --- a/core/common/src/main/java/com/google/jetpackcamera/core/common/FakeFilePathGenerator.kt +++ b/core/common/testing/src/main/java/com/google/jetpackcamera/core/common/testing/FakeFilePathGenerator.kt @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.core.common +package com.google.jetpackcamera.core.common.testing import android.os.Environment +import com.google.jetpackcamera.core.common.FilePathGenerator import java.io.File import java.util.Date diff --git a/data/media/build.gradle.kts b/data/media/build.gradle.kts index 1c10195cf..91146f6e8 100644 --- a/data/media/build.gradle.kts +++ b/data/media/build.gradle.kts @@ -89,7 +89,8 @@ dependencies { // Project dependencies implementation(project(":core:common")) - testImplementation(project(":core:common")) + testImplementation(project(":core:common:testing")) + androidTestImplementation(project(":core:common:testing")) } diff --git a/data/media/src/test/java/com/google/jetpackcamera/media/LocalMediaRepositoryTest.kt b/data/media/src/test/java/com/google/jetpackcamera/media/LocalMediaRepositoryTest.kt index 827d3a087..6974b6ec8 100644 --- a/data/media/src/test/java/com/google/jetpackcamera/media/LocalMediaRepositoryTest.kt +++ b/data/media/src/test/java/com/google/jetpackcamera/media/LocalMediaRepositoryTest.kt @@ -25,7 +25,7 @@ import android.os.Build import android.provider.MediaStore import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.core.common.FakeFilePathGenerator +import com.google.jetpackcamera.core.common.testing.FakeFilePathGenerator import com.google.jetpackcamera.data.media.LocalMediaRepository import com.google.jetpackcamera.data.media.Media import com.google.jetpackcamera.data.media.MediaDescriptor diff --git a/data/media/testing/build.gradle.kts b/data/media/testing/build.gradle.kts new file mode 100644 index 000000000..c3864759a --- /dev/null +++ b/data/media/testing/build.gradle.kts @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.google.jetpackcamera.data.media.testing" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(project(":data:media")) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.androidx.core.ktx) +} diff --git a/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/FakeMediaRepository.kt b/data/media/testing/src/main/kotlin/com/google/jetpackcamera/data/media/testing/FakeMediaRepository.kt similarity index 91% rename from data/media/src/main/kotlin/com/google/jetpackcamera/data/media/FakeMediaRepository.kt rename to data/media/testing/src/main/kotlin/com/google/jetpackcamera/data/media/testing/FakeMediaRepository.kt index 22a95e1a1..e84203bf7 100644 --- a/data/media/src/main/kotlin/com/google/jetpackcamera/data/media/FakeMediaRepository.kt +++ b/data/media/testing/src/main/kotlin/com/google/jetpackcamera/data/media/testing/FakeMediaRepository.kt @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.data.media +package com.google.jetpackcamera.data.media.testing import android.net.Uri import androidx.core.net.toUri +import com.google.jetpackcamera.data.media.Media +import com.google.jetpackcamera.data.media.MediaDescriptor +import com.google.jetpackcamera.data.media.MediaRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 8cb89eb9d..443986762 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -87,6 +87,7 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.truth) androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(project(":data:settings:testing")) // Access Model data implementation(project(":core:model")) diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt index a6093dc8d..703783b23 100644 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt +++ b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt @@ -18,8 +18,8 @@ package com.google.jetpackcamera.settings import androidx.datastore.core.DataStore import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.settings.test.FakeDataStoreModule -import com.google.jetpackcamera.settings.test.FakeJcaSettingsSerializer +import com.google.jetpackcamera.settings.testing.FakeDataStoreModule +import com.google.jetpackcamera.settings.testing.FakeJcaSettingsSerializer import java.io.File import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.advanceUntilIdle diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts new file mode 100644 index 000000000..f6e20b350 --- /dev/null +++ b/data/settings/testing/build.gradle.kts @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.google.jetpackcamera.settings.testing" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(project(":data:settings")) + implementation(project(":core:model")) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.androidx.datastore) + implementation(libs.protobuf.kotlin.lite) +} diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeDataStoreModule.kt b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt similarity index 96% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeDataStoreModule.kt rename to data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt index 1b04f53c7..7c07edfe6 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeDataStoreModule.kt +++ b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings.test +package com.google.jetpackcamera.settings.testing import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt similarity index 98% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt rename to data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt index e9f671991..5b319f9d4 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeJcaSettingsSerializer.kt +++ b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings.test +package com.google.jetpackcamera.settings.testing import androidx.datastore.core.CorruptionException import androidx.datastore.core.Serializer diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeSettingsRepository.kt similarity index 98% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt rename to data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeSettingsRepository.kt index 31b653d11..b2fcb8b9d 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/test/FakeSettingsRepository.kt +++ b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeSettingsRepository.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings.test +package com.google.jetpackcamera.settings.testing import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.DarkMode diff --git a/feature/postcapture/build.gradle.kts b/feature/postcapture/build.gradle.kts index 0a1bca503..689b966be 100644 --- a/feature/postcapture/build.gradle.kts +++ b/feature/postcapture/build.gradle.kts @@ -144,6 +144,7 @@ dependencies { implementation(project(":ui:uistateadapter:postcapture")) testImplementation(project(":ui:uistate:postcapture")) testImplementation(project(":core:common")) + testImplementation(project(":data:media:testing")) } // Allow references to generated code diff --git a/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt b/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt index 1676da374..88f906470 100644 --- a/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt +++ b/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt @@ -21,9 +21,9 @@ import android.net.Uri import androidx.lifecycle.ViewModel import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.data.media.FakeMediaRepository import com.google.jetpackcamera.data.media.Media import com.google.jetpackcamera.data.media.MediaDescriptor +import com.google.jetpackcamera.data.media.testing.FakeMediaRepository import com.google.jetpackcamera.feature.postcapture.ui.SNACKBAR_POST_CAPTURE_IMAGE_DELETE_FAILURE import com.google.jetpackcamera.feature.postcapture.ui.SNACKBAR_POST_CAPTURE_IMAGE_SAVE_FAILURE import com.google.jetpackcamera.feature.postcapture.ui.SNACKBAR_POST_CAPTURE_IMAGE_SAVE_SUCCESS diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index 0e63717c7..a000cfcef 100644 --- a/feature/preview/build.gradle.kts +++ b/feature/preview/build.gradle.kts @@ -141,7 +141,10 @@ dependencies { implementation(project(":data:media")) implementation(project(":data:settings")) implementation(project(":core:model")) - testImplementation(project(":core:common")) + testImplementation(project(":core:camera:testing")) + testImplementation(project(":data:settings:testing")) + testImplementation(project(":data:media:testing")) + testImplementation(project(":core:common:testing")) implementation(project(":ui:components:capture")) implementation(project(":ui:controller")) implementation(project(":ui:controller:impl")) diff --git a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt index 6742a8d28..18e28026f 100644 --- a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt +++ b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt @@ -20,14 +20,14 @@ import android.content.Context import androidx.lifecycle.SavedStateHandle import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.core.camera.test.FakeCameraSystem -import com.google.jetpackcamera.data.media.FakeMediaRepository +import com.google.jetpackcamera.core.camera.testing.FakeCameraSystem +import com.google.jetpackcamera.data.media.testing.FakeMediaRepository import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.SaveMode import com.google.jetpackcamera.settings.SettableConstraintsRepositoryImpl import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS -import com.google.jetpackcamera.settings.test.FakeSettingsRepository +import com.google.jetpackcamera.settings.testing.FakeSettingsRepository import com.google.jetpackcamera.ui.uistate.capture.FlashModeUiState import com.google.jetpackcamera.ui.uistate.capture.FlipLensUiState import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState diff --git a/settings.gradle.kts b/settings.gradle.kts index 6c18183c7..89bd10d46 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,12 +36,16 @@ rootProject.name = "Jetpack Camera" include(":app") include(":feature:preview") include(":core:camera") +include(":core:camera:testing") include(":core:camera:low-light") include(":core:camera:low-light-playservices") include(":feature:settings") include(":data:settings") +include(":data:settings:testing") include(":data:media") +include(":data:media:testing") include(":core:common") +include(":core:common:testing") include(":benchmark") include(":feature:permissions") include(":feature:postcapture") diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 8e8b6faae..6591faf7a 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -108,6 +108,7 @@ dependencies { implementation(project(":data:media")) implementation(project(":core:model")) testImplementation(project(":core:common")) + testImplementation(project(":core:camera:testing")) testImplementation(project(":data:settings")) } diff --git a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/ScreenFlashTest.kt b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/ScreenFlashTest.kt index 0a0083029..3069c7f35 100644 --- a/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/ScreenFlashTest.kt +++ b/ui/components/capture/src/test/java/com/google/jetpackcamera/ui/components/capture/capture/ScreenFlashTest.kt @@ -20,7 +20,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.core.camera.CameraSystem -import com.google.jetpackcamera.core.camera.test.FakeCameraSystem +import com.google.jetpackcamera.core.camera.testing.FakeCameraSystem import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.SaveLocation From 184da5e088cfba5ab1f884d5b0ccc1650a9381ac Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 20 Apr 2026 16:16:55 -0700 Subject: [PATCH 02/14] Correct copyright year in new build files --- core/camera/testing/build.gradle.kts | 2 +- core/common/testing/build.gradle.kts | 2 +- data/media/testing/build.gradle.kts | 2 +- data/settings/testing/build.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts index e47028143..3b9e9e5d0 100644 --- a/core/camera/testing/build.gradle.kts +++ b/core/camera/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/common/testing/build.gradle.kts b/core/common/testing/build.gradle.kts index d14726ad6..c9175f6cf 100644 --- a/core/common/testing/build.gradle.kts +++ b/core/common/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/data/media/testing/build.gradle.kts b/data/media/testing/build.gradle.kts index c3864759a..721d448d9 100644 --- a/data/media/testing/build.gradle.kts +++ b/data/media/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index f6e20b350..bd406a53d 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From cc85dcf0258396cb4e47d02e58ac9206e4f605e3 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 20 Apr 2026 16:17:24 -0700 Subject: [PATCH 03/14] Correct copyright year to 2026 in new build files --- core/camera/testing/build.gradle.kts | 2 +- core/common/testing/build.gradle.kts | 2 +- data/media/testing/build.gradle.kts | 2 +- data/settings/testing/build.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts index 3b9e9e5d0..80153a553 100644 --- a/core/camera/testing/build.gradle.kts +++ b/core/camera/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/common/testing/build.gradle.kts b/core/common/testing/build.gradle.kts index c9175f6cf..57c300b0e 100644 --- a/core/common/testing/build.gradle.kts +++ b/core/common/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/data/media/testing/build.gradle.kts b/data/media/testing/build.gradle.kts index 721d448d9..f05656de1 100644 --- a/data/media/testing/build.gradle.kts +++ b/data/media/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index bd406a53d..0eb033d6e 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 3a9fc879c4fd1e1adecf90087974fab2788056b7 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 20 Apr 2026 17:01:44 -0700 Subject: [PATCH 04/14] Implement fake controllers with unit tests in dedicated testing module --- settings.gradle.kts | 1 + ui/controller/testing/build.gradle.kts | 65 ++++++++++ .../testing/FakeCameraController.kt | 50 ++++++++ .../testing/FakeCaptureController.kt | 67 +++++++++++ .../controller/testing/FakeDebugController.kt | 44 +++++++ .../testing/FakeImageWellController.kt | 38 ++++++ .../testing/FakeQuickSettingsController.kt | 94 +++++++++++++++ .../testing/FakeSnackBarController.kt | 51 ++++++++ .../controller/testing/FakeZoomController.kt | 38 ++++++ .../testing/FakeCameraControllerTest.kt | 56 +++++++++ .../testing/FakeCaptureControllerTest.kt | 84 +++++++++++++ .../testing/FakeDebugControllerTest.kt | 46 +++++++ .../testing/FakeImageWellControllerTest.kt | 45 +++++++ .../FakeQuickSettingsControllerTest.kt | 112 ++++++++++++++++++ .../testing/FakeSnackBarControllerTest.kt | 61 ++++++++++ .../testing/FakeZoomControllerTest.kt | 40 +++++++ 16 files changed, 892 insertions(+) create mode 100644 ui/controller/testing/build.gradle.kts create mode 100644 ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraController.kt create mode 100644 ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt create mode 100644 ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugController.kt create mode 100644 ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellController.kt create mode 100644 ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt create mode 100644 ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt create mode 100644 ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomController.kt create mode 100644 ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt create mode 100644 ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt create mode 100644 ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt create mode 100644 ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellControllerTest.kt create mode 100644 ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt create mode 100644 ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt create mode 100644 ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 89bd10d46..34e816d8e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -62,3 +62,4 @@ include(":ui:uistateadapter:postcapture") include(":core:camera:postprocess") include(":ui:controller") include(":ui:controller:impl") +include(":ui:controller:testing") diff --git a/ui/controller/testing/build.gradle.kts b/ui/controller/testing/build.gradle.kts new file mode 100644 index 000000000..402ca3715 --- /dev/null +++ b/ui/controller/testing/build.gradle.kts @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.google.jetpackcamera.ui.controller.testing" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(project(":ui:controller")) + implementation(project(":core:model")) + implementation(project(":data:media")) + implementation(project(":ui:uistate")) + implementation(project(":ui:uistate:capture")) + implementation(libs.kotlinx.coroutines.core) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.test.core) +} diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraController.kt new file mode 100644 index 000000000..3f437f25f --- /dev/null +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraController.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.jetpackcamera.model.DeviceRotation +import com.google.jetpackcamera.ui.controller.CameraController + +/** + * A fake implementation of [CameraController] that allows for configuring actions for its methods. + * + * @param startCameraAction The action to perform when [startCamera] is called. + * @param stopCameraAction The action to perform when [stopCamera] is called. + * @param tapToFocusAction The action to perform when [tapToFocus] is called. + * @param setDisplayRotationAction The action to perform when [setDisplayRotation] is called. + */ +class FakeCameraController( + var startCameraAction: () -> Unit = {}, + var stopCameraAction: () -> Unit = {}, + var tapToFocusAction: (x: Float, y: Float) -> Unit = { _, _ -> }, + var setDisplayRotationAction: (DeviceRotation) -> Unit = {} +) : CameraController { + override fun startCamera() { + startCameraAction() + } + + override fun stopCamera() { + stopCameraAction() + } + + override fun tapToFocus(x: Float, y: Float) { + tapToFocusAction(x, y) + } + + override fun setDisplayRotation(deviceRotation: DeviceRotation) { + setDisplayRotationAction(deviceRotation) + } +} diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt new file mode 100644 index 000000000..1bd4374da --- /dev/null +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import android.content.ContentResolver +import com.google.jetpackcamera.model.CaptureEvent +import com.google.jetpackcamera.ui.controller.CaptureController +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel + +/** + * A fake implementation of [CaptureController] that allows for configuring actions for its methods. + * + * @param captureEvents The [ReceiveChannel] of [CaptureEvent]s to be used by the controller. + * @param captureImageAction The action to perform when [captureImage] is called. + * @param startVideoRecordingAction The action to perform when [startVideoRecording] is called. + * @param stopVideoRecordingAction The action to perform when [stopVideoRecording] is called. + * @param setLockedRecordingAction The action to perform when [setLockedRecording] is called. + * @param setPausedAction The action to perform when [setPaused] is called. + * @param setAudioEnabledAction The action to perform when [setAudioEnabled] is called. + */ +class FakeCaptureController( + override val captureEvents: ReceiveChannel = Channel(), + var captureImageAction: (ContentResolver) -> Unit = {}, + var startVideoRecordingAction: () -> Unit = {}, + var stopVideoRecordingAction: () -> Unit = {}, + var setLockedRecordingAction: (Boolean) -> Unit = {}, + var setPausedAction: (Boolean) -> Unit = {}, + var setAudioEnabledAction: (Boolean) -> Unit = {} +) : CaptureController { + override fun captureImage(contentResolver: ContentResolver) { + captureImageAction(contentResolver) + } + + override fun startVideoRecording() { + startVideoRecordingAction() + } + + override fun stopVideoRecording() { + stopVideoRecordingAction() + } + + override fun setLockedRecording(isLocked: Boolean) { + setLockedRecordingAction(isLocked) + } + + override fun setPaused(shouldBePaused: Boolean) { + setPausedAction(shouldBePaused) + } + + override fun setAudioEnabled(shouldEnableAudio: Boolean) { + setAudioEnabledAction(shouldEnableAudio) + } +} diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugController.kt new file mode 100644 index 000000000..4d8b427a5 --- /dev/null +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugController.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.jetpackcamera.model.TestPattern +import com.google.jetpackcamera.ui.controller.debug.DebugController + +/** + * A fake implementation of [DebugController] that allows for configuring actions for its methods. + * + * @param toggleDebugHidingComponentsAction The action to perform when [toggleDebugHidingComponents] is called. + * @param toggleDebugOverlayAction The action to perform when [toggleDebugOverlay] is called. + * @param setTestPatternAction The action to perform when [setTestPattern] is called. + */ +class FakeDebugController( + var toggleDebugHidingComponentsAction: () -> Unit = {}, + var toggleDebugOverlayAction: () -> Unit = {}, + var setTestPatternAction: (TestPattern) -> Unit = {} +) : DebugController { + override fun toggleDebugHidingComponents() { + toggleDebugHidingComponentsAction() + } + + override fun toggleDebugOverlay() { + toggleDebugOverlayAction() + } + + override fun setTestPattern(testPattern: TestPattern) { + setTestPatternAction(testPattern) + } +} diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellController.kt new file mode 100644 index 000000000..e10c8e4b4 --- /dev/null +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellController.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.jetpackcamera.data.media.MediaDescriptor +import com.google.jetpackcamera.ui.controller.ImageWellController + +/** + * A fake implementation of [ImageWellController] that allows for configuring actions for its methods. + * + * @param imageWellToRepositoryAction The action to perform when [imageWellToRepository] is called. + * @param updateLastCapturedMediaAction The action to perform when [updateLastCapturedMedia] is called. + */ +class FakeImageWellController( + var imageWellToRepositoryAction: (MediaDescriptor) -> Unit = {}, + var updateLastCapturedMediaAction: () -> Unit = {} +) : ImageWellController { + override fun imageWellToRepository(mediaDescriptor: MediaDescriptor) { + imageWellToRepositoryAction(mediaDescriptor) + } + + override fun updateLastCapturedMedia() { + updateLastCapturedMediaAction() + } +} diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt new file mode 100644 index 000000000..a53ac0a3f --- /dev/null +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsController.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.ui.controller.quicksettings.QuickSettingsController +import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting + +/** + * A fake implementation of [QuickSettingsController] that allows for configuring actions for its methods. + * + * @param toggleQuickSettingsAction The action to perform when [toggleQuickSettings] is called. + * @param setFocusedSettingAction The action to perform when [setFocusedSetting] is called. + * @param setLensFacingAction The action to perform when [setLensFacing] is called. + * @param setFlashAction The action to perform when [setFlash] is called. + * @param setAspectRatioAction The action to perform when [setAspectRatio] is called. + * @param setStreamConfigAction The action to perform when [setStreamConfig] is called. + * @param setDynamicRangeAction The action to perform when [setDynamicRange] is called. + * @param setImageFormatAction The action to perform when [setImageFormat] is called. + * @param setConcurrentCameraModeAction The action to perform when [setConcurrentCameraMode] is called. + * @param setCaptureModeAction The action to perform when [setCaptureMode] is called. + */ +class FakeQuickSettingsController( + var toggleQuickSettingsAction: () -> Unit = {}, + var setFocusedSettingAction: (FocusedQuickSetting) -> Unit = {}, + var setLensFacingAction: (LensFacing) -> Unit = {}, + var setFlashAction: (FlashMode) -> Unit = {}, + var setAspectRatioAction: (AspectRatio) -> Unit = {}, + var setStreamConfigAction: (StreamConfig) -> Unit = {}, + var setDynamicRangeAction: (DynamicRange) -> Unit = {}, + var setImageFormatAction: (ImageOutputFormat) -> Unit = {}, + var setConcurrentCameraModeAction: (ConcurrentCameraMode) -> Unit = {}, + var setCaptureModeAction: (CaptureMode) -> Unit = {} +) : QuickSettingsController { + override fun toggleQuickSettings() { + toggleQuickSettingsAction() + } + + override fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) { + setFocusedSettingAction(focusedQuickSetting) + } + + override fun setLensFacing(lensFace: LensFacing) { + setLensFacingAction(lensFace) + } + + override fun setFlash(flashMode: FlashMode) { + setFlashAction(flashMode) + } + + override fun setAspectRatio(aspectRatio: AspectRatio) { + setAspectRatioAction(aspectRatio) + } + + override fun setStreamConfig(streamConfig: StreamConfig) { + setStreamConfigAction(streamConfig) + } + + override fun setDynamicRange(dynamicRange: DynamicRange) { + setDynamicRangeAction(dynamicRange) + } + + override fun setImageFormat(imageOutputFormat: ImageOutputFormat) { + setImageFormatAction(imageOutputFormat) + } + + override fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) { + setConcurrentCameraModeAction(concurrentCameraMode) + } + + override fun setCaptureMode(captureMode: CaptureMode) { + setCaptureModeAction(captureMode) + } +} diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt new file mode 100644 index 000000000..0edd2ac83 --- /dev/null +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.jetpackcamera.ui.controller.SnackBarController +import com.google.jetpackcamera.ui.uistate.DisableRationale +import com.google.jetpackcamera.ui.uistate.SnackbarData + +/** + * A fake implementation of [SnackBarController] that allows for configuring actions for its methods. + * + * @param enqueueDisabledHdrToggleSnackBarAction The action to perform when [enqueueDisabledHdrToggleSnackBar] is called. + * @param onSnackBarResultAction The action to perform when [onSnackBarResult] is called. + * @param incrementAndGetSnackBarCountAction The action to perform when [incrementAndGetSnackBarCount] is called. + * @param addSnackBarDataAction The action to perform when [addSnackBarData] is called. + */ +class FakeSnackBarController( + var enqueueDisabledHdrToggleSnackBarAction: (DisableRationale) -> Unit = {}, + var onSnackBarResultAction: (String) -> Unit = {}, + var incrementAndGetSnackBarCountAction: () -> Int = { 0 }, + var addSnackBarDataAction: (SnackbarData) -> Unit = {} +) : SnackBarController { + override fun enqueueDisabledHdrToggleSnackBar(disabledReason: DisableRationale) { + enqueueDisabledHdrToggleSnackBarAction(disabledReason) + } + + override fun onSnackBarResult(cookie: String) { + onSnackBarResultAction(cookie) + } + + override fun incrementAndGetSnackBarCount(): Int { + return incrementAndGetSnackBarCountAction() + } + + override fun addSnackBarData(snackBarData: SnackbarData) { + addSnackBarDataAction(snackBarData) + } +} diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomController.kt new file mode 100644 index 000000000..db5d9b2a4 --- /dev/null +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomController.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.jetpackcamera.model.CameraZoomRatio +import com.google.jetpackcamera.ui.controller.ZoomController + +/** + * A fake implementation of [ZoomController] that allows for configuring actions for its methods. + * + * @param setZoomRatioAction The action to perform when [setZoomRatio] is called. + * @param setZoomAnimationStateAction The action to perform when [setZoomAnimationState] is called. + */ +class FakeZoomController( + var setZoomRatioAction: (CameraZoomRatio) -> Unit = {}, + var setZoomAnimationStateAction: (Float?) -> Unit = {} +) : ZoomController { + override fun setZoomRatio(zoomRatio: CameraZoomRatio) { + setZoomRatioAction(zoomRatio) + } + + override fun setZoomAnimationState(targetValue: Float?) { + setZoomAnimationStateAction(targetValue) + } +} diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt new file mode 100644 index 000000000..a1ddfd3e8 --- /dev/null +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.DeviceRotation +import org.junit.Test + +class FakeCameraControllerTest { + @Test + fun startCamera_invokesAction() { + var called = false + val controller = FakeCameraController(startCameraAction = { called = true }) + controller.startCamera() + assertThat(called).isTrue() + } + + @Test + fun stopCamera_invokesAction() { + var called = false + val controller = FakeCameraController(stopCameraAction = { called = true }) + controller.stopCamera() + assertThat(called).isTrue() + } + + @Test + fun tapToFocus_invokesAction() { + var calledCoords: Pair? = null + val controller = FakeCameraController(tapToFocusAction = { x, y -> calledCoords = x to y }) + controller.tapToFocus(1f, 2f) + assertThat(calledCoords).isEqualTo(1f to 2f) + } + + @Test + fun setDisplayRotation_invokesAction() { + var calledRotation: DeviceRotation? = null + val controller = FakeCameraController( + setDisplayRotationAction = { calledRotation = it } + ) + controller.setDisplayRotation(DeviceRotation.Rotated90) + assertThat(calledRotation).isEqualTo(DeviceRotation.Rotated90) + } +} diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt new file mode 100644 index 000000000..3667e0bb2 --- /dev/null +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import android.content.ContentResolver +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.channels.Channel +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FakeCaptureControllerTest { + @Test + fun captureEvents_returnsProvidedChannel() { + val channel = Channel() + val controller = FakeCaptureController(captureEvents = channel) + assertThat(controller.captureEvents).isEqualTo(channel) + } + + @Test + fun captureImage_invokesAction() { + var calledResolver: ContentResolver? = null + val controller = FakeCaptureController(captureImageAction = { calledResolver = it }) + val resolver = ApplicationProvider.getApplicationContext().contentResolver + controller.captureImage(resolver) + assertThat(calledResolver).isEqualTo(resolver) + } + + @Test + fun startVideoRecording_invokesAction() { + var called = false + val controller = FakeCaptureController(startVideoRecordingAction = { called = true }) + controller.startVideoRecording() + assertThat(called).isTrue() + } + + @Test + fun stopVideoRecording_invokesAction() { + var called = false + val controller = FakeCaptureController(stopVideoRecordingAction = { called = true }) + controller.stopVideoRecording() + assertThat(called).isTrue() + } + + @Test + fun setLockedRecording_invokesAction() { + var calledValue: Boolean? = null + val controller = FakeCaptureController(setLockedRecordingAction = { calledValue = it }) + controller.setLockedRecording(true) + assertThat(calledValue).isTrue() + } + + @Test + fun setPaused_invokesAction() { + var calledValue: Boolean? = null + val controller = FakeCaptureController(setPausedAction = { calledValue = it }) + controller.setPaused(true) + assertThat(calledValue).isTrue() + } + + @Test + fun setAudioEnabled_invokesAction() { + var calledValue: Boolean? = null + val controller = FakeCaptureController(setAudioEnabledAction = { calledValue = it }) + controller.setAudioEnabled(true) + assertThat(calledValue).isTrue() + } +} diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt new file mode 100644 index 000000000..954c33dc9 --- /dev/null +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.TestPattern +import org.junit.Test + +class FakeDebugControllerTest { + @Test + fun toggleDebugHidingComponents_invokesAction() { + var called = false + val controller = FakeDebugController(toggleDebugHidingComponentsAction = { called = true }) + controller.toggleDebugHidingComponents() + assertThat(called).isTrue() + } + + @Test + fun toggleDebugOverlay_invokesAction() { + var called = false + val controller = FakeDebugController(toggleDebugOverlayAction = { called = true }) + controller.toggleDebugOverlay() + assertThat(called).isTrue() + } + + @Test + fun setTestPattern_invokesAction() { + var calledPattern: TestPattern? = null + val controller = FakeDebugController(setTestPatternAction = { calledPattern = it }) + controller.setTestPattern(TestPattern.ColorBars) + assertThat(calledPattern).isEqualTo(TestPattern.ColorBars) + } +} diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellControllerTest.kt new file mode 100644 index 000000000..08e8bcbee --- /dev/null +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeImageWellControllerTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import android.net.Uri +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.data.media.MediaDescriptor +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class FakeImageWellControllerTest { + @Test + fun imageWellToRepository_invokesAction() { + var calledDescriptor: MediaDescriptor? = null + val controller = FakeImageWellController( + imageWellToRepositoryAction = { calledDescriptor = it } + ) + val descriptor = MediaDescriptor.Content.Image(Uri.EMPTY, null) + controller.imageWellToRepository(descriptor) + assertThat(calledDescriptor).isEqualTo(descriptor) + } + + @Test + fun updateLastCapturedMedia_invokesAction() { + var called = false + val controller = FakeImageWellController(updateLastCapturedMediaAction = { called = true }) + controller.updateLastCapturedMedia() + assertThat(called).isTrue() + } +} diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt new file mode 100644 index 000000000..045c1482f --- /dev/null +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting +import org.junit.Test + +class FakeQuickSettingsControllerTest { + @Test + fun toggleQuickSettings_invokesAction() { + var called = false + val controller = FakeQuickSettingsController(toggleQuickSettingsAction = { called = true }) + controller.toggleQuickSettings() + assertThat(called).isTrue() + } + + @Test + fun setFocusedSetting_invokesAction() { + var calledValue: FocusedQuickSetting? = null + val controller = FakeQuickSettingsController(setFocusedSettingAction = { calledValue = it }) + controller.setFocusedSetting(FocusedQuickSetting.ASPECT_RATIO) + assertThat(calledValue).isEqualTo(FocusedQuickSetting.ASPECT_RATIO) + } + + @Test + fun setLensFacing_invokesAction() { + var calledValue: LensFacing? = null + val controller = FakeQuickSettingsController(setLensFacingAction = { calledValue = it }) + controller.setLensFacing(LensFacing.FRONT) + assertThat(calledValue).isEqualTo(LensFacing.FRONT) + } + + @Test + fun setFlash_invokesAction() { + var calledValue: FlashMode? = null + val controller = FakeQuickSettingsController(setFlashAction = { calledValue = it }) + controller.setFlash(FlashMode.ON) + assertThat(calledValue).isEqualTo(FlashMode.ON) + } + + @Test + fun setAspectRatio_invokesAction() { + var calledValue: AspectRatio? = null + val controller = FakeQuickSettingsController(setAspectRatioAction = { calledValue = it }) + controller.setAspectRatio(AspectRatio.THREE_FOUR) + assertThat(calledValue).isEqualTo(AspectRatio.THREE_FOUR) + } + + @Test + fun setStreamConfig_invokesAction() { + var calledValue: StreamConfig? = null + val controller = FakeQuickSettingsController(setStreamConfigAction = { calledValue = it }) + controller.setStreamConfig(StreamConfig.SINGLE_STREAM) + assertThat(calledValue).isEqualTo(StreamConfig.SINGLE_STREAM) + } + + @Test + fun setDynamicRange_invokesAction() { + var calledValue: DynamicRange? = null + val controller = FakeQuickSettingsController(setDynamicRangeAction = { calledValue = it }) + controller.setDynamicRange(DynamicRange.HLG10) + assertThat(calledValue).isEqualTo(DynamicRange.HLG10) + } + + @Test + fun setImageFormat_invokesAction() { + var calledValue: ImageOutputFormat? = null + val controller = FakeQuickSettingsController(setImageFormatAction = { calledValue = it }) + controller.setImageFormat(ImageOutputFormat.JPEG) + assertThat(calledValue).isEqualTo(ImageOutputFormat.JPEG) + } + + @Test + fun setConcurrentCameraMode_invokesAction() { + var calledValue: ConcurrentCameraMode? = null + val controller = FakeQuickSettingsController( + setConcurrentCameraModeAction = { calledValue = it } + ) + controller.setConcurrentCameraMode(ConcurrentCameraMode.DUAL) + assertThat(calledValue).isEqualTo(ConcurrentCameraMode.DUAL) + } + + @Test + fun setCaptureMode_invokesAction() { + var calledValue: CaptureMode? = null + val controller = FakeQuickSettingsController(setCaptureModeAction = { calledValue = it }) + controller.setCaptureMode(CaptureMode.STANDARD) + assertThat(calledValue).isEqualTo(CaptureMode.STANDARD) + } +} diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt new file mode 100644 index 000000000..03eb38f49 --- /dev/null +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.ui.uistate.DisableRationale +import com.google.jetpackcamera.ui.uistate.SnackbarData +import org.junit.Test + +class FakeSnackBarControllerTest { + private val testDisableRationale = object : DisableRationale { + override val testTag: String = "test-tag" + override val reasonTextResId: Int = 123 + } + + @Test + fun enqueueDisabledHdrToggleSnackBar_invokesAction() { + var calledValue: DisableRationale? = null + val controller = FakeSnackBarController( + enqueueDisabledHdrToggleSnackBarAction = { calledValue = it } + ) + controller.enqueueDisabledHdrToggleSnackBar(testDisableRationale) + assertThat(calledValue).isEqualTo(testDisableRationale) + } + + @Test + fun onSnackBarResult_invokesAction() { + var calledValue: String? = null + val controller = FakeSnackBarController(onSnackBarResultAction = { calledValue = it }) + controller.onSnackBarResult("test-cookie") + assertThat(calledValue).isEqualTo("test-cookie") + } + + @Test + fun incrementAndGetSnackBarCount_invokesAction() { + val controller = FakeSnackBarController(incrementAndGetSnackBarCountAction = { 42 }) + assertThat(controller.incrementAndGetSnackBarCount()).isEqualTo(42) + } + + @Test + fun addSnackBarData_invokesAction() { + var calledValue: SnackbarData? = null + val controller = FakeSnackBarController(addSnackBarDataAction = { calledValue = it }) + val data = SnackbarData(cookie = "test-cookie", stringResource = 123) + controller.addSnackBarData(data) + assertThat(calledValue).isEqualTo(data) + } +} diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt new file mode 100644 index 000000000..7a7b9f019 --- /dev/null +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.controller.testing + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.CameraZoomRatio +import com.google.jetpackcamera.model.ZoomStrategy +import org.junit.Test + +class FakeZoomControllerTest { + @Test + fun setZoomRatio_invokesAction() { + var calledValue: CameraZoomRatio? = null + val controller = FakeZoomController(setZoomRatioAction = { calledValue = it }) + val ratio = CameraZoomRatio(ZoomStrategy.Absolute(1f)) + controller.setZoomRatio(ratio) + assertThat(calledValue).isEqualTo(ratio) + } + + @Test + fun setZoomAnimationState_invokesAction() { + var calledValue: Float? = null + val controller = FakeZoomController(setZoomAnimationStateAction = { calledValue = it }) + controller.setZoomAnimationState(2.5f) + assertThat(calledValue).isEqualTo(2.5f) + } +} From 08e908467e31fd2e5735e8b72d0129f5f809a10b Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 20 Apr 2026 17:26:05 -0700 Subject: [PATCH 05/14] Address code review comments: add explicit test runners and simplify imports --- .../ui/controller/testing/FakeCameraControllerTest.kt | 3 +++ .../ui/controller/testing/FakeCaptureControllerTest.kt | 3 ++- .../ui/controller/testing/FakeDebugControllerTest.kt | 3 +++ .../ui/controller/testing/FakeQuickSettingsControllerTest.kt | 3 +++ .../ui/controller/testing/FakeSnackBarControllerTest.kt | 3 +++ .../ui/controller/testing/FakeZoomControllerTest.kt | 3 +++ 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt index a1ddfd3e8..14cad2012 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCameraControllerTest.kt @@ -18,7 +18,10 @@ package com.google.jetpackcamera.ui.controller.testing import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.model.DeviceRotation import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +@RunWith(JUnit4::class) class FakeCameraControllerTest { @Test fun startCamera_invokesAction() { diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt index 3667e0bb2..809afa5eb 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt @@ -19,6 +19,7 @@ import android.content.ContentResolver import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.CaptureEvent import kotlinx.coroutines.channels.Channel import org.junit.Test import org.junit.runner.RunWith @@ -28,7 +29,7 @@ import org.robolectric.RobolectricTestRunner class FakeCaptureControllerTest { @Test fun captureEvents_returnsProvidedChannel() { - val channel = Channel() + val channel = Channel() val controller = FakeCaptureController(captureEvents = channel) assertThat(controller.captureEvents).isEqualTo(channel) } diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt index 954c33dc9..6f7aeded3 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeDebugControllerTest.kt @@ -18,7 +18,10 @@ package com.google.jetpackcamera.ui.controller.testing import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.model.TestPattern import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +@RunWith(JUnit4::class) class FakeDebugControllerTest { @Test fun toggleDebugHidingComponents_invokesAction() { diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt index 045c1482f..5a4af8ac0 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeQuickSettingsControllerTest.kt @@ -26,7 +26,10 @@ import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +@RunWith(JUnit4::class) class FakeQuickSettingsControllerTest { @Test fun toggleQuickSettings_invokesAction() { diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt index 03eb38f49..96f71b816 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt @@ -19,7 +19,10 @@ import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.ui.uistate.DisableRationale import com.google.jetpackcamera.ui.uistate.SnackbarData import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +@RunWith(JUnit4::class) class FakeSnackBarControllerTest { private val testDisableRationale = object : DisableRationale { override val testTag: String = "test-tag" diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt index 7a7b9f019..05ba5b0e0 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeZoomControllerTest.kt @@ -19,7 +19,10 @@ import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.model.CameraZoomRatio import com.google.jetpackcamera.model.ZoomStrategy import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +@RunWith(JUnit4::class) class FakeZoomControllerTest { @Test fun setZoomRatio_invokesAction() { From b931f60948832047f333f45504107e70ecdbc54b Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 15:00:18 -0700 Subject: [PATCH 06/14] Standardize build toolchain to jvmToolchain(17) in postcapture modules --- ui/uistate/postcapture/build.gradle.kts | 4 ++-- ui/uistateadapter/postcapture/build.gradle.kts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/uistate/postcapture/build.gradle.kts b/ui/uistate/postcapture/build.gradle.kts index 1779bfab2..3c6fb311b 100644 --- a/ui/uistate/postcapture/build.gradle.kts +++ b/ui/uistate/postcapture/build.gradle.kts @@ -42,8 +42,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } diff --git a/ui/uistateadapter/postcapture/build.gradle.kts b/ui/uistateadapter/postcapture/build.gradle.kts index cb480e455..47a383364 100644 --- a/ui/uistateadapter/postcapture/build.gradle.kts +++ b/ui/uistateadapter/postcapture/build.gradle.kts @@ -42,8 +42,8 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + kotlin { + jvmToolchain(17) } } From eceb1d0dc63db78b0320000449bbe8efa1bbe39a Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 11:43:49 -0700 Subject: [PATCH 07/14] Implement high-fidelity camera startup emulation in FakeCameraSystem This change introduces a Production-grade surface request handshake mechanism to FakeCameraSystem, allowing it to accurately emulate the startup timing of a real camera session. Key changes: - Introduced PreviewSurfaceRequest sealed interface to abstract CameraX and standalone Viewfinder surface requests. - Updated CameraSystem API to use PreviewSurfaceRequest. - Modified FakeCameraSystem to wait for UI surface provision via CompletableDeferred before transitioning to the 'running' state. - Integrated the new request flow into CaptureUiStateAdapter and the PreviewDisplay composable. - Added androidx.camera.viewfinder dependencies to support the standalone Viewfinder composable in tests. - Added instrumented tests in PreviewDisplayTest to verify the handshake and rendering on real devices. - Updated FakeCameraSystemTest with robust handshake verification. - Updated Espresso and Test Core dependencies to resolve platform compatibility issues during instrumented testing. --- core/camera/build.gradle.kts | 1 + .../core/camera/CameraSession.kt | 2 +- .../core/camera/CameraSessionContext.kt | 3 +- .../jetpackcamera/core/camera/CameraSystem.kt | 7 +- .../core/camera/CameraXCameraSystem.kt | 6 +- .../core/camera/FocusMetering.kt | 4 +- .../core/camera/PreviewSurfaceRequest.kt | 46 ++++++ core/camera/testing/build.gradle.kts | 4 +- .../core/camera/testing/FakeCameraSystem.kt | 32 ++++- .../camera/testing/FakeCameraSystemTest.kt | 132 +++++++++++------- .../feature/preview/PreviewScreen.kt | 26 +--- .../feature/preview/PreviewViewModel.kt | 3 - gradle/libs.versions.toml | 8 +- ui/components/capture/build.gradle.kts | 2 + .../components/capture/PreviewDisplayTest.kt | 77 ++++++++++ .../capture/CaptureScreenComponents.kt | 89 +++++++----- .../capture/compound/PreviewDisplayUiState.kt | 5 +- .../capture/compound/CaptureUiStateAdapter.kt | 6 +- 18 files changed, 326 insertions(+), 127 deletions(-) create mode 100644 core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt create mode 100644 ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/PreviewDisplayTest.kt diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index 0fe482a3b..b03977297 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -126,6 +126,7 @@ dependencies { // CameraX implementation(libs.camera.core) implementation(libs.camera.camera2) + implementation(libs.camera.viewfinder.core) implementation(libs.camera.lifecycle) implementation(libs.camera.video) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 2e23459c4..ac16b2477 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -773,7 +773,7 @@ private fun createPreviewUseCase( }.build() .apply { setSurfaceProvider { surfaceRequest -> - surfaceRequests.update { surfaceRequest } + surfaceRequests.update { PreviewSurfaceRequest.CameraX(surfaceRequest) } } } diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt index bb9aefc42..2c7225a64 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSessionContext.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera.core.camera import android.content.Context -import androidx.camera.core.SurfaceRequest import androidx.camera.lifecycle.ProcessCameraProvider import com.google.jetpackcamera.core.camera.lowlight.LowLightBoostEffectProvider import com.google.jetpackcamera.core.common.FilePathGenerator @@ -41,7 +40,7 @@ internal data class CameraSessionContext( val focusMeteringEvents: Channel, val videoCaptureControlEvents: Channel, val currentCameraState: MutableStateFlow, - val surfaceRequests: MutableStateFlow, + val surfaceRequests: MutableStateFlow, val transientSettings: StateFlow, val lowLightBoostEffectProvider: LowLightBoostEffectProvider? = null ) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSystem.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSystem.kt index b6821300e..6e10b6982 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSystem.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSystem.kt @@ -91,7 +91,12 @@ interface CameraSystem { fun getCurrentCameraState(): StateFlow - fun getSurfaceRequest(): StateFlow + /** + * Returns the current [PreviewSurfaceRequest]. + * + * This will be null if no surface is currently requested. + */ + fun getSurfaceRequest(): StateFlow fun getScreenFlashEvents(): ReceiveChannel diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt index ca4f4f0c5..67418a097 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraXCameraSystem.kt @@ -31,7 +31,6 @@ import androidx.camera.core.CameraXConfig import androidx.camera.core.DynamicRange as CXDynamicRange import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture.OutputFileOptions -import androidx.camera.core.SurfaceRequest import androidx.camera.core.takePicture import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration import androidx.camera.lifecycle.ProcessCameraProvider @@ -135,9 +134,10 @@ constructor( private var currentCameraState = MutableStateFlow(CameraState()) override fun getCurrentCameraState(): StateFlow = currentCameraState.asStateFlow() - private val _surfaceRequest = MutableStateFlow(null) + private val _surfaceRequest = MutableStateFlow(null) - override fun getSurfaceRequest(): StateFlow = _surfaceRequest.asStateFlow() + override fun getSurfaceRequest(): StateFlow = + _surfaceRequest.asStateFlow() private val lowLightBoostAvailabilityChecker: LowLightBoostAvailabilityChecker? private val lowLightBoostEffectProvider: LowLightBoostEffectProvider? diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt index 9253b8609..16ced2ec6 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/FocusMetering.kt @@ -44,7 +44,9 @@ internal suspend fun CameraSessionContext.processFocusMeteringEvents( cameraInfo: CameraInfo, cameraControl: CameraControl ) { - surfaceRequests.flatMapLatest { surfaceRequest -> + surfaceRequests.flatMapLatest { previewSurfaceRequest -> + val surfaceRequest = + (previewSurfaceRequest as? PreviewSurfaceRequest.CameraX)?.surfaceRequest surfaceRequest?.let { request -> Log.d( TAG, diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt new file mode 100644 index 000000000..1747738f3 --- /dev/null +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.core.camera + +import android.view.Surface +import androidx.camera.core.SurfaceRequest +import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest +import kotlinx.coroutines.CompletableDeferred + +/** + * A sealed interface representing a request for a preview surface, abstracting the specific + * viewfinder implementation required. + */ +sealed interface PreviewSurfaceRequest { + /** + * Wraps a CameraX [SurfaceRequest] for the production CameraXViewfinder. + * + * @property surfaceRequest The CameraX [SurfaceRequest]. + */ + class CameraX(val surfaceRequest: SurfaceRequest) : PreviewSurfaceRequest + + /** + * Wraps a [ViewfinderSurfaceRequest] for the standalone Viewfinder composable. + * + * @property surfaceRequest The standalone [ViewfinderSurfaceRequest]. + * @property surfaceDeferred A [CompletableDeferred] that will be completed with the [Surface] + * once provided by the UI. + */ + class Viewfinder( + val surfaceRequest: ViewfinderSurfaceRequest, + val surfaceDeferred: CompletableDeferred = CompletableDeferred() + ) : PreviewSurfaceRequest +} diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts index 80153a553..10ae51ce8 100644 --- a/core/camera/testing/build.gradle.kts +++ b/core/camera/testing/build.gradle.kts @@ -52,10 +52,10 @@ dependencies { implementation(project(":core:camera")) implementation(project(":core:model")) implementation(project(":data:settings")) - + implementation(libs.camera.core) + implementation(libs.camera.viewfinder.core) implementation(libs.kotlinx.coroutines.core) - // Testing testImplementation(libs.junit) testImplementation(libs.truth) diff --git a/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt index 5b19520ce..45cc47ffe 100644 --- a/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt +++ b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt @@ -17,11 +17,14 @@ package com.google.jetpackcamera.core.camera.testing import android.annotation.SuppressLint import android.content.ContentResolver +import android.view.Surface import androidx.camera.core.ImageCapture -import androidx.camera.core.SurfaceRequest +import androidx.camera.viewfinder.core.ImplementationMode +import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest import com.google.jetpackcamera.core.camera.CameraState import com.google.jetpackcamera.core.camera.CameraSystem import com.google.jetpackcamera.core.camera.OnVideoRecordEvent +import com.google.jetpackcamera.core.camera.PreviewSurfaceRequest import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CameraZoomRatio import com.google.jetpackcamera.model.CaptureMode @@ -38,6 +41,7 @@ import com.google.jetpackcamera.model.StreamConfig import com.google.jetpackcamera.model.TestPattern import com.google.jetpackcamera.model.VideoQuality import com.google.jetpackcamera.settings.model.CameraAppSettings +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.flow.MutableStateFlow @@ -53,12 +57,19 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti private var initialized = false private var useCasesBinded = false + /** Whether the preview has started. */ var previewStarted = false + + /** Number of pictures taken. */ var numPicturesTaken = 0 + /** Whether a recording is in progress. */ var recordingInProgress = false + + /** Whether the current recording is paused. */ var isRecordingPaused = false + /** Whether the lens facing is front. */ var isLensFacingFront = false private var isScreenFlash = true @@ -85,9 +96,11 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti currentSettings .onCompletion { + _surfaceRequest.value = null useCasesBinded = false previewStarted = false recordingInProgress = false + _currentCameraState.update { CameraState() } }.collectLatest { useCasesBinded = true previewStarted = true @@ -96,6 +109,18 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti isScreenFlash = isLensFacingFront && (it.flashMode == FlashMode.AUTO || it.flashMode == FlashMode.ON) + + val request = ViewfinderSurfaceRequest( + 1920, + 1080, + ImplementationMode.EXTERNAL + ) + val deferred = CompletableDeferred() + _surfaceRequest.value = PreviewSurfaceRequest.Viewfinder(request, deferred) + deferred.await() + _currentCameraState.update { state -> + state.copy(isCameraRunning = true) + } } } @@ -163,8 +188,9 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti override fun getCurrentCameraState(): StateFlow = _currentCameraState.asStateFlow() - private val _surfaceRequest = MutableStateFlow(null) - override fun getSurfaceRequest(): StateFlow = _surfaceRequest.asStateFlow() + private val _surfaceRequest = MutableStateFlow(null) + override fun getSurfaceRequest(): StateFlow = + _surfaceRequest.asStateFlow() override fun getScreenFlashEvents() = screenFlashEvents override fun getCurrentSettings(): StateFlow = currentSettings.asStateFlow() diff --git a/core/camera/testing/src/test/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystemTest.kt b/core/camera/testing/src/test/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystemTest.kt index 9dfd6955b..39df8d487 100644 --- a/core/camera/testing/src/test/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystemTest.kt +++ b/core/camera/testing/src/test/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystemTest.kt @@ -15,14 +15,19 @@ */ package com.google.jetpackcamera.core.camera.testing -import com.google.common.truth.Truth +import android.graphics.SurfaceTexture +import android.view.Surface +import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.core.camera.PreviewSurfaceRequest import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher @@ -36,10 +41,10 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalCoroutinesApi::class) -@RunWith(JUnit4::class) +@RunWith(RobolectricTestRunner::class) class FakeCameraSystemTest { private val testScope = TestScope() private val testDispatcher = StandardTestDispatcher(testScope.testScheduler) @@ -47,7 +52,7 @@ class FakeCameraSystemTest { private val cameraSystem = FakeCameraSystem() @Before - fun setup() { + fun setUp() { Dispatchers.setMain(testDispatcher) } @@ -56,50 +61,71 @@ class FakeCameraSystemTest { Dispatchers.resetMain() } - @Test - fun canInitialize() = runTest(testDispatcher) { - cameraSystem.initialize( - cameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS - ) {} - } - @Test fun canRunCamera() = runTest(testDispatcher) { - initAndRunCamera() - Truth.assertThat(cameraSystem.isPreviewStarted()).isTrue() + cameraSystem.initialize(DEFAULT_CAMERA_APP_SETTINGS) {} + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + cameraSystem.runCamera() + } + advanceUntilIdle() + + // Fulfill surface request to let runCamera continue + val request = cameraSystem.getSurfaceRequest().filterNotNull().first() + (request as PreviewSurfaceRequest.Viewfinder).surfaceDeferred.complete( + Surface(SurfaceTexture(1)) + ) + + advanceUntilIdle() + assertThat(cameraSystem.isPreviewStarted()).isTrue() + job.cancel() } @Test - fun screenFlashDisabled_whenFlashModeOffAndFrontCamera() = runTest(testDispatcher) { - initAndRunCamera() + fun surfaceRequest_emitsAndViewfinderFulfills() = runTest(testDispatcher) { + cameraSystem.initialize(DEFAULT_CAMERA_APP_SETTINGS) {} + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + cameraSystem.runCamera() + } + advanceUntilIdle() + + val request = cameraSystem.getSurfaceRequest().filterNotNull().first() + assertThat(request).isInstanceOf(PreviewSurfaceRequest.Viewfinder::class.java) + val viewfinderRequest = request as PreviewSurfaceRequest.Viewfinder + + // Camera should not be running yet + assertThat(cameraSystem.getCurrentCameraState().value.isCameraRunning).isFalse() + + // Provide surface + val surface = Surface(SurfaceTexture(1)) + viewfinderRequest.surfaceDeferred.complete(surface) - cameraSystem.setLensFacing(lensFacing = LensFacing.FRONT) - cameraSystem.setFlashMode(flashMode = FlashMode.OFF) advanceUntilIdle() - Truth.assertThat(cameraSystem.isScreenFlashEnabled()).isFalse() + // Now camera should be running + assertThat(cameraSystem.getCurrentCameraState().value.isCameraRunning).isTrue() + job.cancel() } @Test - fun screenFlashDisabled_whenFlashModeOnAndNotFrontCamera() = runTest(testDispatcher) { + fun canSetLensFacing() = runTest(testDispatcher) { initAndRunCamera() - cameraSystem.setLensFacing(lensFacing = LensFacing.BACK) - cameraSystem.setFlashMode(flashMode = FlashMode.ON) + cameraSystem.setLensFacing(lensFacing = LensFacing.FRONT) advanceUntilIdle() - Truth.assertThat(cameraSystem.isScreenFlashEnabled()).isFalse() + assertThat(cameraSystem.getCurrentSettings().value?.cameraLensFacing) + .isEqualTo(LensFacing.FRONT) } @Test - fun screenFlashDisabled_whenFlashModeAutoAndNotFrontCamera() = runTest(testDispatcher) { + fun canSetFlashMode() = runTest(testDispatcher) { initAndRunCamera() - cameraSystem.setLensFacing(lensFacing = LensFacing.BACK) - cameraSystem.setFlashMode(flashMode = FlashMode.AUTO) + cameraSystem.setFlashMode(flashMode = FlashMode.ON) advanceUntilIdle() - Truth.assertThat(cameraSystem.isScreenFlashEnabled()).isFalse() + assertThat(cameraSystem.getCurrentSettings().value?.flashMode) + .isEqualTo(FlashMode.ON) } @Test @@ -110,51 +136,51 @@ class FakeCameraSystemTest { cameraSystem.setFlashMode(flashMode = FlashMode.ON) advanceUntilIdle() - Truth.assertThat(cameraSystem.isScreenFlashEnabled()).isTrue() + val events = mutableListOf() + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + cameraSystem.getScreenFlashEvents().consumeAsFlow().toList(events) + } + + cameraSystem.takePicture {} + advanceUntilIdle() + + assertThat(events.map { it.type }).contains(CameraSystem.ScreenFlashEvent.Type.APPLY_UI) + job.cancel() } @Test - fun screenFlashEnabled_whenFlashModeAutoAndFrontCamera() = runTest(testDispatcher) { + fun screenFlashDisabled_whenFlashModeOffAndFrontCamera() = runTest(testDispatcher) { initAndRunCamera() cameraSystem.setLensFacing(lensFacing = LensFacing.FRONT) - cameraSystem.setFlashMode(flashMode = FlashMode.AUTO) + cameraSystem.setFlashMode(flashMode = FlashMode.OFF) advanceUntilIdle() - Truth.assertThat(cameraSystem.isScreenFlashEnabled()).isTrue() - } - - @Test - fun captureScreenFlashImage_screenFlashEventsEmittedInCorrectSequence() = runTest( - testDispatcher - ) { - initAndRunCamera() val events = mutableListOf() - backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { cameraSystem.getScreenFlashEvents().consumeAsFlow().toList(events) } - // FlashMode.ON in front facing camera automatically enables screen flash - cameraSystem.setLensFacing(lensFacing = LensFacing.FRONT) - cameraSystem.setFlashMode(FlashMode.ON) + cameraSystem.takePicture {} advanceUntilIdle() - cameraSystem.takePicture() - advanceUntilIdle() - Truth.assertThat(events.map { it.type }).containsExactlyElementsIn( - listOf( - CameraSystem.ScreenFlashEvent.Type.APPLY_UI, - CameraSystem.ScreenFlashEvent.Type.CLEAR_UI - ) - ).inOrder() + assertThat(events.map { it.type }) + .doesNotContain(CameraSystem.ScreenFlashEvent.Type.APPLY_UI) + job.cancel() } - private fun TestScope.initAndRunCamera() { + private suspend fun TestScope.initAndRunCamera() { + cameraSystem.initialize( + cameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS + ) {} backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - cameraSystem.initialize( - cameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS - ) {} cameraSystem.runCamera() } + advanceUntilIdle() + val request = cameraSystem.getSurfaceRequest().filterNotNull().first() + (request as PreviewSurfaceRequest.Viewfinder).surfaceDeferred.complete( + Surface(SurfaceTexture(1)) + ) + advanceUntilIdle() } } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index 947873c5b..3ad5ad23a 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -19,7 +19,6 @@ import android.Manifest import android.os.Build import android.util.Log import android.util.Range -import androidx.camera.core.SurfaceRequest import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -142,9 +141,6 @@ fun PreviewScreen( val screenFlashUiState: ScreenFlashUiState by viewModel.screenFlash.screenFlashUiState.collectAsState() - val surfaceRequest: SurfaceRequest? - by viewModel.surfaceRequest.collectAsState() - LifecycleStartEffect(Unit) { viewModel.cameraController.startCamera() onStopOrDispose { @@ -275,7 +271,6 @@ fun PreviewScreen( modifier = modifier, captureUiState = currentUiState, screenFlashUiState = screenFlashUiState, - surfaceRequest = surfaceRequest, onNavigateToSettings = onNavigateToSettings, onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness, onAbsoluteZoom = { zoomRatio: Float, lensToZoom: LensToZoom -> @@ -341,7 +336,6 @@ fun PreviewScreen( private fun ContentScreen( captureUiState: CaptureUiState.Ready, screenFlashUiState: ScreenFlashUiState, - surfaceRequest: SurfaceRequest?, modifier: Modifier = Modifier, onNavigateToSettings: () -> Unit = {}, onClearUiScreenBrightness: (Float) -> Unit = {}, @@ -408,8 +402,8 @@ private fun ContentScreen( onFlipCamera = onFlipCamera, onTapToFocus = cameraController?.let { it::tapToFocus } ?: { _, _ -> }, onScaleZoom = { onScaleZoom(it, LensToZoom.PRIMARY) }, - surfaceRequest = surfaceRequest, onRequestWindowColorMode = onRequestWindowColorMode, + surfaceRequest = captureUiState.previewDisplayUiState.surfaceRequest, focusMeteringUiState = captureUiState.focusMeteringUiState ) }, @@ -687,8 +681,7 @@ private fun ContentScreenPreview() { MaterialTheme { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_READY, - screenFlashUiState = ScreenFlashUiState(), - surfaceRequest = null + screenFlashUiState = ScreenFlashUiState() ) } } @@ -699,8 +692,7 @@ private fun ContentScreen_Standard_Idle() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_READY.copy(), - screenFlashUiState = ScreenFlashUiState(), - surfaceRequest = null + screenFlashUiState = ScreenFlashUiState() ) } } @@ -713,8 +705,7 @@ private fun ContentScreen_ImageOnly_Idle() { captureUiState = FAKE_PREVIEW_UI_STATE_READY.copy( captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) ), - screenFlashUiState = ScreenFlashUiState(), - surfaceRequest = null + screenFlashUiState = ScreenFlashUiState() ) } } @@ -727,8 +718,7 @@ private fun ContentScreen_VideoOnly_Idle() { captureUiState = FAKE_PREVIEW_UI_STATE_READY.copy( captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) ), - screenFlashUiState = ScreenFlashUiState(), - surfaceRequest = null + screenFlashUiState = ScreenFlashUiState() ) } } @@ -739,8 +729,7 @@ private fun ContentScreen_Standard_Recording() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_PRESSED_RECORDING, - screenFlashUiState = ScreenFlashUiState(), - surfaceRequest = null + screenFlashUiState = ScreenFlashUiState() ) } } @@ -751,8 +740,7 @@ private fun ContentScreen_Locked_Recording() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_LOCKED_RECORDING, - screenFlashUiState = ScreenFlashUiState(), - surfaceRequest = null + screenFlashUiState = ScreenFlashUiState() ) } } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index 3cf0b7004..4b27217f5 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -17,7 +17,6 @@ package com.google.jetpackcamera.feature.preview import android.net.Uri import android.util.Log -import androidx.camera.core.SurfaceRequest import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -103,8 +102,6 @@ class PreviewViewModel @Inject constructor( val snackBarUiState: StateFlow = _snackBarUiState.asStateFlow() - val surfaceRequest: StateFlow = cameraSystem.getSurfaceRequest() - private val _captureEvents = Channel() val captureEvents: ReceiveChannel = _captureEvents diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3a48567b6..6d94daaa4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,8 +31,8 @@ androidxLifecycle = "2.9.2" androidxMedia3 = "1.9.0-alpha01" androidxNavigationCompose = "2.9.2" androidxProfileinstaller = "1.4.1" -androidxTestCore = "1.5.0" -androidxTestEspresso = "3.6.1" +androidxTestCore = "1.6.1" +androidxTestEspresso = "3.7.0" androidxTestJunit = "1.2.1" androidxTestMonitor = "1.7.2" androidxTestRules = "1.6.1" @@ -81,9 +81,11 @@ androidx-tracing = { module = "androidx.tracing:tracing-ktx", version.ref = "and androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidxTestUiautomator" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidxCamera" } camera-core = { module = "androidx.camera:camera-core", version.ref = "androidxCamera" } +camera-compose = { module = "androidx.camera:camera-compose", version.ref = "androidxCamera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" } camera-video = { module = "androidx.camera:camera-video", version.ref = "androidxCamera" } -camera-compose = { module = "androidx.camera:camera-compose", version.ref = "androidxCamera" } +camera-viewfinder-core = { module = "androidx.camera.viewfinder:viewfinder-core", version.ref = "androidxCamera" } +camera-viewfinder-compose = { module = "androidx.camera.viewfinder:viewfinder-compose", version.ref = "androidxCamera" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-junit = { module = "androidx.compose.ui:ui-test-junit4" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial" } diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 6591faf7a..2eb6c97f9 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -81,6 +81,8 @@ dependencies { // CameraX implementation(libs.camera.core) implementation(libs.camera.compose) + implementation(libs.camera.viewfinder.core) + implementation(libs.camera.viewfinder.compose) // Compose - Testing androidTestImplementation(libs.compose.junit) diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/PreviewDisplayTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/PreviewDisplayTest.kt new file mode 100644 index 000000000..68917e63a --- /dev/null +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/PreviewDisplayTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.ui.components.capture + +import androidx.camera.viewfinder.core.ImplementationMode +import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.jetpackcamera.core.camera.PreviewSurfaceRequest +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState +import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState +import com.google.jetpackcamera.ui.uistate.capture.FocusMeteringUiState +import com.google.jetpackcamera.ui.uistate.capture.compound.PreviewDisplayUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented tests for the [PreviewDisplay] composable. + */ +@RunWith(AndroidJUnit4::class) +class PreviewDisplayTest { + @get:Rule + val composeTestRule = createComposeRule() + + /** + * Verifies that the [PreviewDisplay] composable correctly renders when provided with a + * [PreviewSurfaceRequest.Viewfinder] and fulfills the surface request. + */ + @Test + fun previewDisplay_withViewfinderRequest_rendersWithoutError() { + val viewfinderRequest = ViewfinderSurfaceRequest( + 1920, + 1080, + ImplementationMode.EXTERNAL + ) + val surfaceRequest = PreviewSurfaceRequest.Viewfinder(viewfinderRequest) + + composeTestRule.setContent { + PreviewDisplay( + previewDisplayUiState = PreviewDisplayUiState( + aspectRatioUiState = AspectRatioUiState.Available( + availableAspectRatios = listOf( + SingleSelectableUiState.SelectableUi( + AspectRatio.THREE_FOUR + ) + ), + selectedAspectRatio = AspectRatio.THREE_FOUR + ), + surfaceRequest = surfaceRequest + ), + onTapToFocus = { _, _ -> }, + onFlipCamera = { }, + onScaleZoom = { }, + onRequestWindowColorMode = { }, + focusMeteringUiState = FocusMeteringUiState.Unspecified, + surfaceRequest = surfaceRequest + ) + } + + composeTestRule.waitForIdle() + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index a3bc22753..386571e32 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -24,6 +24,7 @@ import androidx.camera.core.DynamicRange as CXDynamicRange import androidx.camera.core.SurfaceRequest import androidx.camera.viewfinder.compose.CoordinateTransformer import androidx.camera.viewfinder.compose.MutableCoordinateTransformer +import androidx.camera.viewfinder.compose.Viewfinder import androidx.camera.viewfinder.core.ImplementationMode import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable @@ -104,6 +105,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round +import com.google.jetpackcamera.core.camera.PreviewSurfaceRequest import com.google.jetpackcamera.core.camera.VideoRecordingState import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.StabilizationMode @@ -125,6 +127,7 @@ import com.google.jetpackcamera.ui.uistate.capture.FocusMeteringUiState import com.google.jetpackcamera.ui.uistate.capture.StabilizationUiState import com.google.jetpackcamera.ui.uistate.capture.compound.PreviewDisplayUiState import kotlin.time.Duration.Companion.nanoseconds +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -401,12 +404,12 @@ fun TestableSnackbar( @Composable private fun DetectWindowColorModeChanges( - surfaceRequest: SurfaceRequest, + surfaceRequest: SurfaceRequest?, implementationMode: ImplementationMode, onRequestWindowColorMode: (Int) -> Unit ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val currentSurfaceRequest: SurfaceRequest by rememberUpdatedState(surfaceRequest) + val currentSurfaceRequest: SurfaceRequest? by rememberUpdatedState(surfaceRequest) val currentImplementationMode: ImplementationMode by rememberUpdatedState( implementationMode ) @@ -414,11 +417,14 @@ private fun DetectWindowColorModeChanges( onRequestWindowColorMode ) - LaunchedEffect(Unit) { + LaunchedEffect(currentSurfaceRequest) { + if (currentSurfaceRequest == null) { + return@LaunchedEffect + } val colorModeSnapshotFlow = snapshotFlow { Pair( - currentSurfaceRequest.dynamicRange, + currentSurfaceRequest!!.dynamicRange, currentImplementationMode ) } @@ -462,7 +468,7 @@ private fun DetectWindowColorModeChanges( * @param onFlipCamera the callback for flipping the camera. * @param onScaleZoom the callback for scaling the zoom. * @param onRequestWindowColorMode the callback for requesting a window color mode. - * @param surfaceRequest the [SurfaceRequest] for the preview. + * @param surfaceRequest the [PreviewSurfaceRequest] for the preview. * @param focusMeteringUiState the [FocusMeteringUiState] for this component. * @param modifier the modifier for this component. */ @@ -473,7 +479,7 @@ fun PreviewDisplay( onFlipCamera: () -> Unit, onScaleZoom: (Float) -> Unit, onRequestWindowColorMode: (Int) -> Unit, - surfaceRequest: SurfaceRequest?, + surfaceRequest: PreviewSurfaceRequest?, focusMeteringUiState: FocusMeteringUiState, modifier: Modifier = Modifier ) { @@ -536,39 +542,56 @@ fun PreviewDisplay( } DetectWindowColorModeChanges( - surfaceRequest = surfaceRequest, + surfaceRequest = (surfaceRequest as? PreviewSurfaceRequest.CameraX) + ?.surfaceRequest, implementationMode = implementationMode, onRequestWindowColorMode = onRequestWindowColorMode ) val coordinateTransformer = remember { MutableCoordinateTransformer() } - CameraXViewfinder( - modifier = Modifier - .fillMaxSize() - .pointerInput(onFlipCamera) { - detectTapGestures( - onDoubleTap = { offset -> - // double tap to flip camera - Log.d(TAG, "onDoubleTap $offset") - onFlipCamera() + when (surfaceRequest) { + is PreviewSurfaceRequest.CameraX -> { + CameraXViewfinder( + modifier = Modifier + .fillMaxSize() + .pointerInput(onFlipCamera) { + detectTapGestures( + onDoubleTap = { offset -> + // double tap to flip camera + Log.d(TAG, "onDoubleTap $offset") + onFlipCamera() + }, + onTap = { + with(coordinateTransformer) { + val surfaceCoords = it.transform() + Log.d( + "TAG", + "onTapToFocus: " + + "input{$it} -> surface{$surfaceCoords}" + ) + onTapToFocus(surfaceCoords.x, surfaceCoords.y) + } + } + ) }, - onTap = { - with(coordinateTransformer) { - val surfaceCoords = it.transform() - Log.d( - "TAG", - "onTapToFocus: " + - "input{$it} -> surface{$surfaceCoords}" - ) - onTapToFocus(surfaceCoords.x, surfaceCoords.y) - } - } - ) - }, - surfaceRequest = it, - implementationMode = implementationMode, - coordinateTransformer = coordinateTransformer - ) + surfaceRequest = surfaceRequest.surfaceRequest, + implementationMode = implementationMode, + coordinateTransformer = coordinateTransformer + ) + } + + is PreviewSurfaceRequest.Viewfinder -> { + Viewfinder( + surfaceRequest = surfaceRequest.surfaceRequest, + modifier = Modifier.fillMaxSize() + ) { + onSurfaceSession { + surfaceRequest.surfaceDeferred.complete(surface) + awaitCancellation() + } + } + } + } FocusMeteringIndicator( focusMeteringUiState = focusMeteringUiState, coordinateTransformer = coordinateTransformer diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/PreviewDisplayUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/PreviewDisplayUiState.kt index d6ed953c4..fe7364e11 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/PreviewDisplayUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/compound/PreviewDisplayUiState.kt @@ -15,6 +15,7 @@ */ package com.google.jetpackcamera.ui.uistate.capture.compound +import com.google.jetpackcamera.core.camera.PreviewSurfaceRequest import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState /** @@ -22,8 +23,10 @@ import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState * * @param lastBlinkTimeStamp The timestamp of the most recent capture blink animation. * @param aspectRatioUiState The UI state for the aspect ratio of the preview. + * @param surfaceRequest The current surface request for the preview display. */ data class PreviewDisplayUiState( val lastBlinkTimeStamp: Long = 0, - val aspectRatioUiState: AspectRatioUiState + val aspectRatioUiState: AspectRatioUiState, + val surfaceRequest: PreviewSurfaceRequest? = null ) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index eabc94353..ae4fe34b8 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -72,8 +72,9 @@ fun captureUiState( cameraSystem.getCurrentSettings().filterNotNull(), constraintsRepository.systemConstraints.filterNotNull(), cameraSystem.getCurrentCameraState(), + cameraSystem.getSurfaceRequest(), trackedCaptureUiState - ) { cameraAppSettings, systemConstraints, cameraState, trackedUiState -> + ) { cameraAppSettings, systemConstraints, cameraState, surfaceRequest, trackedUiState -> val captureModeUiState = CaptureModeUiState.from( systemConstraints, cameraAppSettings, @@ -112,7 +113,8 @@ fun captureUiState( aspectRatioUiState = aspectRatioUiState, previewDisplayUiState = PreviewDisplayUiState( trackedUiState.lastBlinkTimeStamp, - aspectRatioUiState + aspectRatioUiState, + surfaceRequest ), // TODO: add updateFrom() for all ui states to prevent re-updating if // values are the same From 2fb597ef0e652e2e48de00df67040ee1167993f7 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 18:26:54 -0700 Subject: [PATCH 08/14] Address code review comments for FakeCaptureController and FakeSnackBarController - Use UNLIMITED channel for capture events to prevent test hangs. - Add simulateCaptureEvent helper to FakeCaptureController. - Use lazy AtomicInteger for snackbar count in FakeSnackBarController. - Provide functional default for enqueueDisabledHdrToggleSnackBar. --- .../testing/FakeCaptureController.kt | 13 ++++++++++-- .../testing/FakeSnackBarController.kt | 20 +++++++++++++----- .../testing/FakeCaptureControllerTest.kt | 11 ++++++++++ .../testing/FakeSnackBarControllerTest.kt | 21 ++++++++++++++----- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt index 1bd4374da..99933ed5e 100644 --- a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureController.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.channels.ReceiveChannel /** * A fake implementation of [CaptureController] that allows for configuring actions for its methods. * - * @param captureEvents The [ReceiveChannel] of [CaptureEvent]s to be used by the controller. + * @param captureEvents The [ReceiveChannel] for [CaptureEvent]s. * @param captureImageAction The action to perform when [captureImage] is called. * @param startVideoRecordingAction The action to perform when [startVideoRecording] is called. * @param stopVideoRecordingAction The action to perform when [stopVideoRecording] is called. @@ -33,7 +33,7 @@ import kotlinx.coroutines.channels.ReceiveChannel * @param setAudioEnabledAction The action to perform when [setAudioEnabled] is called. */ class FakeCaptureController( - override val captureEvents: ReceiveChannel = Channel(), + override val captureEvents: ReceiveChannel = Channel(Channel.UNLIMITED), var captureImageAction: (ContentResolver) -> Unit = {}, var startVideoRecordingAction: () -> Unit = {}, var stopVideoRecordingAction: () -> Unit = {}, @@ -41,6 +41,15 @@ class FakeCaptureController( var setPausedAction: (Boolean) -> Unit = {}, var setAudioEnabledAction: (Boolean) -> Unit = {} ) : CaptureController { + /** + * Simulates a [CaptureEvent] being emitted by the controller. + * This relies on the [captureEvents] instance being a [Channel]. + */ + fun simulateCaptureEvent(event: CaptureEvent) { + (captureEvents as? Channel)?.trySend(event) + ?: throw IllegalStateException("captureEvents is not a Channel") + } + override fun captureImage(contentResolver: ContentResolver) { captureImageAction(contentResolver) } diff --git a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt index 0edd2ac83..2c73edb0f 100644 --- a/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt +++ b/ui/controller/testing/src/main/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarController.kt @@ -18,23 +18,33 @@ package com.google.jetpackcamera.ui.controller.testing import com.google.jetpackcamera.ui.controller.SnackBarController import com.google.jetpackcamera.ui.uistate.DisableRationale import com.google.jetpackcamera.ui.uistate.SnackbarData +import java.util.concurrent.atomic.AtomicInteger /** * A fake implementation of [SnackBarController] that allows for configuring actions for its methods. * - * @param enqueueDisabledHdrToggleSnackBarAction The action to perform when [enqueueDisabledHdrToggleSnackBar] is called. * @param onSnackBarResultAction The action to perform when [onSnackBarResult] is called. * @param incrementAndGetSnackBarCountAction The action to perform when [incrementAndGetSnackBarCount] is called. * @param addSnackBarDataAction The action to perform when [addSnackBarData] is called. */ class FakeSnackBarController( - var enqueueDisabledHdrToggleSnackBarAction: (DisableRationale) -> Unit = {}, var onSnackBarResultAction: (String) -> Unit = {}, - var incrementAndGetSnackBarCountAction: () -> Int = { 0 }, + var incrementAndGetSnackBarCountAction: (() -> Int)? = null, var addSnackBarDataAction: (SnackbarData) -> Unit = {} ) : SnackBarController { + private val snackBarCount by lazy { AtomicInteger(0) } + override fun enqueueDisabledHdrToggleSnackBar(disabledReason: DisableRationale) { - enqueueDisabledHdrToggleSnackBarAction(disabledReason) + val cookieInt = incrementAndGetSnackBarCount() + val cookie = "DisabledHdrToggle-$cookieInt" + addSnackBarData( + SnackbarData( + cookie = cookie, + stringResource = disabledReason.reasonTextResId, + withDismissAction = true, + testTag = disabledReason.testTag + ) + ) } override fun onSnackBarResult(cookie: String) { @@ -42,7 +52,7 @@ class FakeSnackBarController( } override fun incrementAndGetSnackBarCount(): Int { - return incrementAndGetSnackBarCountAction() + return incrementAndGetSnackBarCountAction?.invoke() ?: snackBarCount.incrementAndGet() } override fun addSnackBarData(snackBarData: SnackbarData) { diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt index 809afa5eb..218366dc8 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeCaptureControllerTest.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.model.CaptureEvent +import com.google.jetpackcamera.model.ImageCaptureEvent import kotlinx.coroutines.channels.Channel import org.junit.Test import org.junit.runner.RunWith @@ -34,6 +35,16 @@ class FakeCaptureControllerTest { assertThat(controller.captureEvents).isEqualTo(channel) } + @Test + fun simulateCaptureEvent_emitsToCaptureEventsChannel() { + val controller = FakeCaptureController() + val event = ImageCaptureEvent.SingleImageSaved() + controller.simulateCaptureEvent(event) + + val receivedEvent = controller.captureEvents.tryReceive().getOrNull() + assertThat(receivedEvent).isEqualTo(event) + } + @Test fun captureImage_invokesAction() { var calledResolver: ContentResolver? = null diff --git a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt index 96f71b816..2dc1acbc2 100644 --- a/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt +++ b/ui/controller/testing/src/test/java/com/google/jetpackcamera/ui/controller/testing/FakeSnackBarControllerTest.kt @@ -30,13 +30,17 @@ class FakeSnackBarControllerTest { } @Test - fun enqueueDisabledHdrToggleSnackBar_invokesAction() { - var calledValue: DisableRationale? = null + fun enqueueDisabledHdrToggleSnackBar_invokesAddSnackBarData() { + var calledValue: SnackbarData? = null val controller = FakeSnackBarController( - enqueueDisabledHdrToggleSnackBarAction = { calledValue = it } + addSnackBarDataAction = { calledValue = it } ) controller.enqueueDisabledHdrToggleSnackBar(testDisableRationale) - assertThat(calledValue).isEqualTo(testDisableRationale) + + assertThat(calledValue).isNotNull() + assertThat(calledValue?.cookie).isEqualTo("DisabledHdrToggle-1") + assertThat(calledValue?.stringResource).isEqualTo(testDisableRationale.reasonTextResId) + assertThat(calledValue?.testTag).isEqualTo(testDisableRationale.testTag) } @Test @@ -48,7 +52,14 @@ class FakeSnackBarControllerTest { } @Test - fun incrementAndGetSnackBarCount_invokesAction() { + fun incrementAndGetSnackBarCount_usesInternalCounterByDefault() { + val controller = FakeSnackBarController() + assertThat(controller.incrementAndGetSnackBarCount()).isEqualTo(1) + assertThat(controller.incrementAndGetSnackBarCount()).isEqualTo(2) + } + + @Test + fun incrementAndGetSnackBarCount_invokesActionIfProvided() { val controller = FakeSnackBarController(incrementAndGetSnackBarCountAction = { 42 }) assertThat(controller.incrementAndGetSnackBarCount()).isEqualTo(42) } From 5e99e5bd11e110013abb63fd329a9b523514aeac Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 18:53:20 -0700 Subject: [PATCH 09/14] Fix CameraXCameraSystemTest build error after PreviewSurfaceRequest change --- .../jetpackcamera/core/camera/CameraXCameraSystemTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt index e90320509..a523c0fca 100644 --- a/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt +++ b/core/camera/src/androidTest/java/com/google/jetpackcamera/core/camera/CameraXCameraSystemTest.kt @@ -409,7 +409,9 @@ class CameraXCameraSystemTest { private fun CameraXCameraSystem.providePreviewSurface() { cameraSystemScope.launch { getSurfaceRequest().filterNotNull().collect { - it.provideUpdatingSurface() + if (it is PreviewSurfaceRequest.CameraX) { + it.surfaceRequest.provideUpdatingSurface() + } } } } From 9d43e46135dc060135e004e80de8dc7917f4efe9 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 19:14:08 -0700 Subject: [PATCH 10/14] Address code review: Make PreviewSurfaceRequest.CameraX a data class --- .../google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt index 1747738f3..98c487fbf 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt @@ -30,7 +30,7 @@ sealed interface PreviewSurfaceRequest { * * @property surfaceRequest The CameraX [SurfaceRequest]. */ - class CameraX(val surfaceRequest: SurfaceRequest) : PreviewSurfaceRequest + data class CameraX(val surfaceRequest: SurfaceRequest) : PreviewSurfaceRequest /** * Wraps a [ViewfinderSurfaceRequest] for the standalone Viewfinder composable. From 03cccdfa1832512c96132baa7c2ec567c60e3960 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 19:14:54 -0700 Subject: [PATCH 11/14] Address code review: Make PreviewSurfaceRequest.Viewfinder a data class --- .../google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt index 98c487fbf..7d510f67c 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/PreviewSurfaceRequest.kt @@ -39,7 +39,7 @@ sealed interface PreviewSurfaceRequest { * @property surfaceDeferred A [CompletableDeferred] that will be completed with the [Surface] * once provided by the UI. */ - class Viewfinder( + data class Viewfinder( val surfaceRequest: ViewfinderSurfaceRequest, val surfaceDeferred: CompletableDeferred = CompletableDeferred() ) : PreviewSurfaceRequest From 70f407d438561cc4cba8b6ebfd9437ed780ed142 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 19:15:55 -0700 Subject: [PATCH 12/14] Address code review: Derive FakeCameraSystem surface resolution from aspect ratio --- .../jetpackcamera/core/camera/testing/FakeCameraSystem.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt index 45cc47ffe..e74e11481 100644 --- a/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt +++ b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt @@ -110,8 +110,9 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti isLensFacingFront && (it.flashMode == FlashMode.AUTO || it.flashMode == FlashMode.ON) + val aspectRatio = it.aspectRatio val request = ViewfinderSurfaceRequest( - 1920, + 1080 * aspectRatio.denominator / aspectRatio.numerator, 1080, ImplementationMode.EXTERNAL ) From ab983c71acf9b4d515d72dcacc28f2faff444c1f Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 19:16:49 -0700 Subject: [PATCH 13/14] Address code review: Set isCameraRunning to false while waiting for surface --- .../jetpackcamera/core/camera/testing/FakeCameraSystem.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt index e74e11481..1afd392c8 100644 --- a/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt +++ b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt @@ -117,6 +117,9 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti ImplementationMode.EXTERNAL ) val deferred = CompletableDeferred() + _currentCameraState.update { state -> + state.copy(isCameraRunning = false) + } _surfaceRequest.value = PreviewSurfaceRequest.Viewfinder(request, deferred) deferred.await() _currentCameraState.update { state -> From f935fa13b5c876fdb5d11db9101128d6b875ee1d Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 21 Apr 2026 19:19:18 -0700 Subject: [PATCH 14/14] Address code review: Extract preview gestures into a reusable Modifier --- .../capture/CaptureScreenComponents.kt | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index 386571e32..6a281e1b8 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -554,26 +554,11 @@ fun PreviewDisplay( CameraXViewfinder( modifier = Modifier .fillMaxSize() - .pointerInput(onFlipCamera) { - detectTapGestures( - onDoubleTap = { offset -> - // double tap to flip camera - Log.d(TAG, "onDoubleTap $offset") - onFlipCamera() - }, - onTap = { - with(coordinateTransformer) { - val surfaceCoords = it.transform() - Log.d( - "TAG", - "onTapToFocus: " + - "input{$it} -> surface{$surfaceCoords}" - ) - onTapToFocus(surfaceCoords.x, surfaceCoords.y) - } - } - ) - }, + .previewGestures( + onFlipCamera = onFlipCamera, + onTapToFocus = onTapToFocus, + coordinateTransformer = coordinateTransformer + ), surfaceRequest = surfaceRequest.surfaceRequest, implementationMode = implementationMode, coordinateTransformer = coordinateTransformer @@ -583,7 +568,14 @@ fun PreviewDisplay( is PreviewSurfaceRequest.Viewfinder -> { Viewfinder( surfaceRequest = surfaceRequest.surfaceRequest, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .previewGestures( + onFlipCamera = onFlipCamera, + onTapToFocus = onTapToFocus, + coordinateTransformer = coordinateTransformer + ), + coordinateTransformer = coordinateTransformer ) { onSurfaceSession { surfaceRequest.surfaceDeferred.complete(surface) @@ -943,3 +935,28 @@ private fun FocusMeteringIndicator( } } } + +private fun Modifier.previewGestures( + onFlipCamera: () -> Unit, + onTapToFocus: (x: Float, y: Float) -> Unit, + coordinateTransformer: CoordinateTransformer +): Modifier = pointerInput(onFlipCamera) { + detectTapGestures( + onDoubleTap = { offset -> + // double tap to flip camera + Log.d(TAG, "onDoubleTap $offset") + onFlipCamera() + }, + onTap = { + with(coordinateTransformer) { + val surfaceCoords = it.transform() + Log.d( + TAG, + "onTapToFocus: " + + "input{$it} -> surface{$surfaceCoords}" + ) + onTapToFocus(surfaceCoords.x, surfaceCoords.y) + } + } + ) +}