diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index dd76e4e19..5891186e5 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -16,6 +16,8 @@ When reviewing a pull request, focus on the following key areas: * Does the new code align with the existing MVVM architecture? * Are ViewModels, Repositories, and UI components used correctly? * Does it introduce any anti-patterns or deviate from established conventions in the codebase? + * **Hilt Scoping Hygiene:** Verify Hilt dependency scopes are appropriate for their lifecycle (e.g., `SingletonComponent` vs `ActivityRetainedComponent`). Avoid over-scoping (such as using `Singleton` when the component state should be isolated per activity/test session to prevent state leaks between tests). + * **Git Merge & Diff Hygiene:** During merges from `main`, verify that recent `main` changes (such as scoping fixes or newly added configuration files like module `AndroidManifest.xml`s) are not accidentally reverted or lost in conflicts. Ensure the PR diff against `main` is minimal and contains zero unrelated "merge noise" or unintended file reversions. 2. **Code Quality and Best Practices** * Check for adherence to official Kotlin style guides and Android best practices. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f889b10e..0493140ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -151,6 +151,7 @@ dependencies { // Access settings & model data implementation(project(":data:settings")) + implementation(project(":data:settings-datastore")) implementation(project(":core:model")) // Camera Preview diff --git a/data/settings-datastore/.gitignore b/data/settings-datastore/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/data/settings-datastore/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/settings-datastore/build.gradle.kts b/data/settings-datastore/build.gradle.kts new file mode 100644 index 000000000..987bfa2d4 --- /dev/null +++ b/data/settings-datastore/build.gradle.kts @@ -0,0 +1,122 @@ +/* + * 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) + alias(libs.plugins.google.protobuf) + alias(libs.plugins.kotlin.kapt) +} + +android { + namespace = "com.google.jetpackcamera.data.settingsdatastore" + 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" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } + + @Suppress("UnstableApiUsage") + testOptions { + managedDevices { + localDevices { + create("pixel2Api28") { + device = "Pixel 2" + apiLevel = 28 + } + create("pixel8Api34") { + device = "Pixel 8" + apiLevel = 34 + systemImageSource = "aosp_atd" + } + } + } + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + + // Hilt + implementation(libs.dagger.hilt.android) + kapt(libs.dagger.hilt.compiler) + + // proto datastore + implementation(libs.protobuf.kotlin.lite) + implementation(libs.androidx.datastore) + + // Access Model data + implementation(project(":core:common")) + implementation(project(":core:model")) + implementation(project(":data:settings")) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.truth) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.truth) + androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(project(":data:settings-datastore:testing")) +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.21.12" + } + + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + } + + task.builtins { + create("kotlin") { + option("lite") + } + } + } + } +} + +// Allow references to generated code +kapt { + correctErrorTypes = true +} diff --git a/data/settings-datastore/consumer-rules.pro b/data/settings-datastore/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/data/settings-datastore/proguard-rules.pro b/data/settings-datastore/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/data/settings-datastore/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt b/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModuleTest.kt similarity index 83% rename from data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt rename to data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModuleTest.kt index 703783b23..f474577e3 100644 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt +++ b/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModuleTest.kt @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore import androidx.datastore.core.DataStore import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.settings.testing.FakeDataStoreModule -import com.google.jetpackcamera.settings.testing.FakeJcaSettingsSerializer +import com.google.jetpackcamera.settings.JcaSettings +import com.google.jetpackcamera.settingsdatastore.testing.FakeDataStoreModule +import com.google.jetpackcamera.settingsdatastore.testing.FakeJcaSettingsSerializer import java.io.File +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -31,7 +33,7 @@ import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class) class DataStoreModuleTest { @get:Rule val tempFolder = TemporaryFolder() diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt b/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt similarity index 97% rename from data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt rename to data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt index fafb38dc3..f35fa1124 100644 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt +++ b/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore import android.content.Context import androidx.datastore.core.DataStore @@ -22,13 +22,14 @@ import androidx.datastore.dataStoreFile import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.data.settingsdatastore.DataStoreModule.provideDataStore import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.DarkMode 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.settings.DataStoreModule.provideDataStore +import com.google.jetpackcamera.settings.JcaSettings import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import java.io.File diff --git a/data/settings-datastore/src/main/AndroidManifest.xml b/data/settings-datastore/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8322d22ca --- /dev/null +++ b/data/settings-datastore/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt similarity index 91% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt rename to data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt index 7f8862455..806eecb3d 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt @@ -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. @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile +import com.google.jetpackcamera.settings.JcaSettings import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -30,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -// with hilt will ensure datastore instance access is unique per file @Module @InstallIn(SingletonComponent::class) object DataStoreModule { diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/JcaSettingsSerializer.kt similarity index 94% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt rename to data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/JcaSettingsSerializer.kt index f84a3248c..e47b1e9b2 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/JcaSettingsSerializer.kt @@ -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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore import androidx.datastore.core.CorruptionException import androidx.datastore.core.Serializer @@ -27,6 +27,7 @@ import com.google.jetpackcamera.model.proto.LensFacing import com.google.jetpackcamera.model.proto.StabilizationMode import com.google.jetpackcamera.model.proto.StreamConfig import com.google.jetpackcamera.model.proto.VideoQuality +import com.google.jetpackcamera.settings.JcaSettings import com.google.protobuf.InvalidProtocolBufferException import java.io.InputStream import java.io.OutputStream diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt similarity index 96% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt rename to data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt index 7034f9eba..5e90a740b 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt @@ -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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore import androidx.datastore.core.DataStore import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride @@ -39,13 +39,14 @@ import com.google.jetpackcamera.model.proto.DarkMode as DarkModeProto import com.google.jetpackcamera.model.proto.FlashMode as FlashModeProto import com.google.jetpackcamera.model.proto.StabilizationMode as StabilizationModeProto import com.google.jetpackcamera.model.proto.StreamConfig as StreamConfigProto +import com.google.jetpackcamera.settings.JcaSettings +import com.google.jetpackcamera.settings.SettingsRepository import com.google.jetpackcamera.settings.model.CameraAppSettings import javax.inject.Inject import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map - /** - * Implementation of [SettingsRepository] with locally stored settings. + * Implementation of [com.google.jetpackcamera.settings.SettingsRepository] with locally stored settings. */ class LocalSettingsRepository @Inject constructor( private val jcaSettings: DataStore, diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt similarity index 84% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt rename to data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt index 3c9fb71ec..5eef413dc 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt @@ -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. @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore +import com.google.jetpackcamera.settings.SettingsRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto b/data/settings-datastore/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto similarity index 100% rename from data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto rename to data/settings-datastore/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto diff --git a/data/settings-datastore/src/test/kotlin/ProtoConversionTest.kt b/data/settings-datastore/src/test/kotlin/ProtoConversionTest.kt new file mode 100644 index 000000000..63736c236 --- /dev/null +++ b/data/settings-datastore/src/test/kotlin/ProtoConversionTest.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.settings + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.DynamicRange.Companion.toProto +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.ImageOutputFormat.Companion.toProto +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.LensFacing.Companion.toProto +import com.google.jetpackcamera.model.LowLightBoostPriority +import com.google.jetpackcamera.model.LowLightBoostPriority.Companion.toProto +import com.google.jetpackcamera.model.StabilizationMode +import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.model.VideoQuality.Companion.toProto +import com.google.jetpackcamera.model.proto.AspectRatio as AspectRatioProto +import com.google.jetpackcamera.model.proto.DynamicRange as DynamicRangeProto +import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto +import com.google.jetpackcamera.model.proto.LensFacing as LensFacingProto +import com.google.jetpackcamera.model.proto.LowLightBoostPriority as LowLightBoostPriorityProto +import com.google.jetpackcamera.model.proto.StabilizationMode as StabilizationModeProto +import com.google.jetpackcamera.model.proto.VideoQuality as VideoQualityProto +import kotlin.enums.enumEntries +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ProtoConversionTest { + + // --- DynamicRange --- + @Test + fun dynamicRange_convertsToCorrectProto() { + val correctConversions = { dynamicRange: DynamicRange -> + when (dynamicRange) { + DynamicRange.SDR -> DynamicRangeProto.DYNAMIC_RANGE_SDR + DynamicRange.HLG10 -> DynamicRangeProto.DYNAMIC_RANGE_HLG10 + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)).isEqualTo(it.toProto()) + } + } + + @Test + fun dynamicRangeProto_convertsToCorrectDynamicRange() { + val correctConversions = { dynamicRangeProto: DynamicRangeProto -> + when (dynamicRangeProto) { + DynamicRangeProto.DYNAMIC_RANGE_SDR, + DynamicRangeProto.UNRECOGNIZED, + DynamicRangeProto.DYNAMIC_RANGE_UNSPECIFIED -> DynamicRange.SDR + + DynamicRangeProto.DYNAMIC_RANGE_HLG10 -> DynamicRange.HLG10 + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)) + .isEqualTo(DynamicRange.fromProto(it)) + } + } + + // --- ImageOutputFormat --- + @Test + fun imageOutputFormat_convertsToCorrectProto() { + val correctConversions = { imageOutputFormat: ImageOutputFormat -> + when (imageOutputFormat) { + ImageOutputFormat.JPEG -> ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG + ImageOutputFormat.JPEG_ULTRA_HDR -> + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)).isEqualTo(it.toProto()) + } + } + + @Test + fun imageOutputFormatProto_convertsToCorrectImageOutputFormat() { + val correctConversions = { imageOutputFormatProto: ImageOutputFormatProto -> + when (imageOutputFormatProto) { + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG, + ImageOutputFormatProto.UNRECOGNIZED -> ImageOutputFormat.JPEG + + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR -> + ImageOutputFormat.JPEG_ULTRA_HDR + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)) + .isEqualTo(ImageOutputFormat.fromProto(it)) + } + } + + // --- LensFacing --- + @Test + fun lensFacing_convertsToCorrectProto() { + val correctConversions = { lensFacing: LensFacing -> + when (lensFacing) { + LensFacing.BACK -> LensFacingProto.LENS_FACING_BACK + LensFacing.FRONT -> LensFacingProto.LENS_FACING_FRONT + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)).isEqualTo(it.toProto()) + } + } + + @Test + fun lensFacingProto_convertsToCorrectLensFacing() { + val correctConversions = { lensFacingProto: LensFacingProto -> + when (lensFacingProto) { + LensFacingProto.LENS_FACING_BACK -> LensFacing.BACK + LensFacingProto.LENS_FACING_FRONT, + LensFacingProto.UNRECOGNIZED -> LensFacing.FRONT + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)) + .isEqualTo(LensFacing.fromProto(it)) + } + } + + // --- LowLightBoostPriority --- + @Test + fun lowLightBoostPriority_convertsToCorrectProto() { + val correctConversions = { priority: LowLightBoostPriority -> + when (priority) { + LowLightBoostPriority.PRIORITIZE_AE_MODE -> + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE + + LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES -> + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)).isEqualTo(it.toProto()) + } + } + + @Test + fun lowLightBoostPriorityProto_convertsToCorrectPriority() { + val correctConversions = { priorityProto: LowLightBoostPriorityProto -> + when (priorityProto) { + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE, + LowLightBoostPriorityProto.UNRECOGNIZED -> LowLightBoostPriority.PRIORITIZE_AE_MODE + + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES -> + LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)) + .isEqualTo(LowLightBoostPriority.fromProto(it)) + } + } + + // --- VideoQuality --- + @Test + fun videoQuality_convertsToCorrectProto() { + val correctConversions = { videoQuality: VideoQuality -> + when (videoQuality) { + VideoQuality.UNSPECIFIED -> VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED + VideoQuality.SD -> VideoQualityProto.VIDEO_QUALITY_SD + VideoQuality.HD -> VideoQualityProto.VIDEO_QUALITY_HD + VideoQuality.FHD -> VideoQualityProto.VIDEO_QUALITY_FHD + VideoQuality.UHD -> VideoQualityProto.VIDEO_QUALITY_UHD + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)).isEqualTo(it.toProto()) + } + } + + @Test + fun videoQualityProto_convertsToCorrectVideoQuality() { + val correctConversions = { videoQualityProto: VideoQualityProto -> + when (videoQualityProto) { + VideoQualityProto.VIDEO_QUALITY_SD -> VideoQuality.SD + VideoQualityProto.VIDEO_QUALITY_HD -> VideoQuality.HD + VideoQualityProto.VIDEO_QUALITY_FHD -> VideoQuality.FHD + VideoQualityProto.VIDEO_QUALITY_UHD -> VideoQuality.UHD + VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED, + VideoQualityProto.UNRECOGNIZED -> VideoQuality.UNSPECIFIED + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)) + .isEqualTo(VideoQuality.fromProto(it)) + } + } + + // --- AspectRatio (fromProto only) --- + @Test + fun aspectRatioProto_convertsToCorrectAspectRatio() { + val correctConversions = { aspectRatioProto: AspectRatioProto -> + when (aspectRatioProto) { + AspectRatioProto.ASPECT_RATIO_THREE_FOUR, + AspectRatioProto.ASPECT_RATIO_UNDEFINED, + AspectRatioProto.UNRECOGNIZED -> AspectRatio.THREE_FOUR + + AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN -> AspectRatio.NINE_SIXTEEN + AspectRatioProto.ASPECT_RATIO_ONE_ONE -> AspectRatio.ONE_ONE + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)) + .isEqualTo(AspectRatio.fromProto(it)) + } + } + + // --- StabilizationMode (fromProto only) --- + @Test + fun stabilizationModeProto_convertsToCorrectStabilizationMode() { + val correctConversions = { stabilizationModeProto: StabilizationModeProto -> + when (stabilizationModeProto) { + StabilizationModeProto.STABILIZATION_MODE_OFF -> StabilizationMode.OFF + StabilizationModeProto.STABILIZATION_MODE_ON -> StabilizationMode.ON + StabilizationModeProto.STABILIZATION_MODE_HIGH_QUALITY -> + StabilizationMode.HIGH_QUALITY + + StabilizationModeProto.STABILIZATION_MODE_OPTICAL -> StabilizationMode.OPTICAL + StabilizationModeProto.STABILIZATION_MODE_UNDEFINED, + StabilizationModeProto.UNRECOGNIZED, + StabilizationModeProto.STABILIZATION_MODE_AUTO -> StabilizationMode.AUTO + } + } + + enumEntries().forEach { + assertThat(correctConversions(it)) + .isEqualTo(StabilizationMode.fromProto(it)) + } + } +} diff --git a/data/settings-datastore/testing/build.gradle.kts b/data/settings-datastore/testing/build.gradle.kts new file mode 100644 index 000000000..21ee6b0c3 --- /dev/null +++ b/data/settings-datastore/testing/build.gradle.kts @@ -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. + */ + +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" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } + } + + dependencies { + implementation(project(":data:settings-datastore")) + 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/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt b/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt similarity index 95% rename from data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt rename to data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt index 7c07edfe6..c9bc09803 100644 --- a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt +++ b/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/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.testing +package com.google.jetpackcamera.settingsdatastore.testing import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory diff --git a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt b/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeJcaSettingsSerializer.kt similarity index 97% rename from data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt rename to data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeJcaSettingsSerializer.kt index 5b319f9d4..84a490c4c 100644 --- a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt +++ b/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/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.testing +package com.google.jetpackcamera.settingsdatastore.testing import androidx.datastore.core.CorruptionException import androidx.datastore.core.Serializer diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 443986762..08b43c4c2 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -19,7 +19,6 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.kapt) alias(libs.plugins.dagger.hilt.android) - alias(libs.plugins.google.protobuf) } android { @@ -76,44 +75,19 @@ dependencies { implementation(libs.dagger.hilt.android) kapt(libs.dagger.hilt.compiler) - // proto datastore - implementation(libs.androidx.datastore) - implementation(libs.protobuf.kotlin.lite) - - // Testing - testImplementation(libs.junit) - testImplementation(libs.truth) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.truth) - androidTestImplementation(libs.kotlinx.coroutines.test) - androidTestImplementation(project(":data:settings:testing")) - // Access Model data implementation(project(":core:model")) implementation(project(":core:common")) -} -protobuf { - protoc { - artifact = "com.google.protobuf:protoc:3.21.12" - } - - generateProtoTasks { - all().forEach { task -> - task.builtins { - create("java") { - option("lite") - } - } + // Testing + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.truth) // For unit tests + testImplementation(project(":core:common")) // Explicitly add for unit tests - task.builtins { - create("kotlin") { - option("lite") - } - } - } - } + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.truth) // For instrumented tests } // Allow references to generated code diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsModule.kt index ed4e5b200..930a3c9da 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsModule.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsModule.kt @@ -18,8 +18,12 @@ package com.google.jetpackcamera.settings import dagger.Binds import dagger.Module import dagger.hilt.InstallIn +<<<<<<<< HEAD:data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsModule.kt import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.android.scopes.ActivityRetainedScoped +======== +import dagger.hilt.components.SingletonComponent +>>>>>>>> main:data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt /** * Dagger [Module] for constraints data layer. @@ -29,6 +33,7 @@ import dagger.hilt.android.scopes.ActivityRetainedScoped interface ConstraintsModule { @Binds +<<<<<<<< HEAD:data/settings/src/main/java/com/google/jetpackcamera/settings/ConstraintsModule.kt @ActivityRetainedScoped fun bindsSettableConstraintsRepository( settableConstraintsRepository: SettableConstraintsRepositoryImpl @@ -44,4 +49,9 @@ interface ConstraintsModule { fun bindsConstraintsRepository( constraintsRepository: SettableConstraintsRepository ): ConstraintsRepository +======== + fun bindsSettingsRepository( + localSettingsRepository: LocalSettingsRepository + ): SettingsRepository +>>>>>>>> main:data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt } diff --git a/data/settings/src/test/java/com/google/jetpackcamera/settings/ModelProtoConversionTest.kt b/data/settings/src/test/java/com/google/jetpackcamera/settings/ModelProtoConversionTest.kt new file mode 100644 index 000000000..9131dfca4 --- /dev/null +++ b/data/settings/src/test/java/com/google/jetpackcamera/settings/ModelProtoConversionTest.kt @@ -0,0 +1,91 @@ +/* + * 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.settings + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.DebugSettings +import com.google.jetpackcamera.model.DebugSettings.Companion.encodeAsByteArray +import com.google.jetpackcamera.model.DebugSettings.Companion.encodeAsString +import com.google.jetpackcamera.model.DebugSettings.Companion.toProto +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.TestPattern +import com.google.jetpackcamera.model.TestPattern.Companion.toProto +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ModelProtoConversionTest { + + // --- TestPattern --- + @Test + fun testPattern_convertsToCorrectProto() { + val patterns = listOf( + TestPattern.Off, + TestPattern.ColorBars, + TestPattern.ColorBarsFadeToGray, + TestPattern.PN9, + TestPattern.Custom1, + TestPattern.SolidColor(1u, 2u, 3u, 4u) + ) + patterns.forEach { pattern -> + val proto = pattern.toProto() + val domain = TestPattern.fromProto(proto) + assertThat(domain).isEqualTo(pattern) + } + } + + // --- DebugSettings --- + @Test + fun debugSettings_convertsToCorrectProto() { + val settings = listOf( + DebugSettings(), + DebugSettings(isDebugModeEnabled = true), + DebugSettings(singleLensMode = LensFacing.BACK), + DebugSettings(singleLensMode = LensFacing.FRONT), + DebugSettings(testPattern = TestPattern.ColorBars), + DebugSettings( + isDebugModeEnabled = true, + singleLensMode = LensFacing.FRONT, + testPattern = TestPattern.SolidColor(255u, 0u, 0u, 255u) + ) + ) + settings.forEach { setting -> + val proto = setting.toProto() + val domain = DebugSettings.fromProto(proto) + assertThat(domain).isEqualTo(setting) + } + } + + @Test + fun debugSettings_encodesAndDecodesCorrectly() { + val setting = DebugSettings( + isDebugModeEnabled = true, + singleLensMode = LensFacing.BACK, + testPattern = TestPattern.ColorBars + ) + + // Byte Array Serialization + val bytes = setting.encodeAsByteArray() + val decodedFromBytes = DebugSettings.parseFromByteArray(bytes) + assertThat(decodedFromBytes).isEqualTo(setting) + + // Base64 String Serialization (used in Navigation Routes) + val string = setting.encodeAsString() + val decodedFromString = DebugSettings.parseFromString(string) + assertThat(decodedFromString).isEqualTo(setting) + } +} diff --git a/data/settings/src/test/java/com/google/jetpackcamera/settings/ProtoConversionTest.kt b/data/settings/src/test/java/com/google/jetpackcamera/settings/ProtoConversionTest.kt deleted file mode 100644 index 5ce8f2643..000000000 --- a/data/settings/src/test/java/com/google/jetpackcamera/settings/ProtoConversionTest.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.settings - -import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.model.DynamicRange -import com.google.jetpackcamera.model.DynamicRange.Companion.toProto -import com.google.jetpackcamera.model.ImageOutputFormat -import com.google.jetpackcamera.model.ImageOutputFormat.Companion.toProto -import com.google.jetpackcamera.model.proto.DynamicRange as DynamicRangeProto -import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@RunWith(JUnit4::class) -class ProtoConversionTest { - @Test - fun dynamicRange_convertsToCorrectProto() { - val correctConversions = { dynamicRange: DynamicRange -> - when (dynamicRange) { - DynamicRange.SDR -> DynamicRangeProto.DYNAMIC_RANGE_SDR - DynamicRange.HLG10 -> DynamicRangeProto.DYNAMIC_RANGE_HLG10 - else -> TODO( - "Test does not yet contain correct conversion for dynamic range " + - "type: ${dynamicRange.name}" - ) - } - } - - enumValues().forEach { - assertThat(correctConversions(it)).isEqualTo(it.toProto()) - } - } - - @Test - fun dynamicRangeProto_convertsToCorrectDynamicRange() { - val correctConversions = { dynamicRangeProto: DynamicRangeProto -> - when (dynamicRangeProto) { - DynamicRangeProto.DYNAMIC_RANGE_SDR, - DynamicRangeProto.UNRECOGNIZED, - DynamicRangeProto.DYNAMIC_RANGE_UNSPECIFIED - -> DynamicRange.SDR - - DynamicRangeProto.DYNAMIC_RANGE_HLG10 -> DynamicRange.HLG10 - else -> TODO( - "Test does not yet contain correct conversion for dynamic range " + - "proto type: ${dynamicRangeProto.name}" - ) - } - } - - enumValues().forEach { - assertThat(correctConversions(it)).isEqualTo(DynamicRange.fromProto(it)) - } - } - - @Test - fun imageOutputFormat_convertsToCorrectProto() { - val correctConversions = { imageOutputFormat: ImageOutputFormat -> - when (imageOutputFormat) { - ImageOutputFormat.JPEG -> ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG - ImageOutputFormat.JPEG_ULTRA_HDR - -> ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR - else -> TODO( - "Test does not yet contain correct conversion for image output format " + - "type: ${imageOutputFormat.name}" - ) - } - } - - enumValues().forEach { - assertThat(correctConversions(it)).isEqualTo(it.toProto()) - } - } - - @Test - fun imageOutputFormatProto_convertsToCorrectImageOutputFormat() { - val correctConversions = { imageOutputFormatProto: ImageOutputFormatProto -> - when (imageOutputFormatProto) { - ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG, - ImageOutputFormatProto.UNRECOGNIZED - -> ImageOutputFormat.JPEG - ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR - -> ImageOutputFormat.JPEG_ULTRA_HDR - else -> TODO( - "Test does not yet contain correct conversion for image output format " + - "proto type: ${imageOutputFormatProto.name}" - ) - } - } - - enumValues().forEach { - assertThat(correctConversions(it)).isEqualTo(ImageOutputFormat.fromProto(it)) - } - } -} diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 2e4835edc..7f6789636 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -116,6 +116,7 @@ dependencies { implementation(libs.protobuf.kotlin.lite) implementation(project(":data:settings")) + androidTestImplementation(project(":data:settings-datastore")) implementation(project(":core:model")) } diff --git a/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt b/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt index 6d09cf21a..c8dc3040e 100644 --- a/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt +++ b/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt @@ -23,6 +23,8 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.data.settingsdatastore.JcaSettingsSerializer +import com.google.jetpackcamera.data.settingsdatastore.LocalSettingsRepository import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.DarkMode import com.google.jetpackcamera.model.LensFacing diff --git a/settings.gradle.kts b/settings.gradle.kts index f04820411..21666d917 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,3 +64,5 @@ include(":core:camera:postprocess") include(":ui:controller") include(":ui:controller:impl") include(":ui:controller:testing") +include(":data:settings-datastore") +include(":data:settings-datastore:testing")