diff --git a/app/build.gradle.kts b/app/build.gradle.kts index be7fcd1ed..8767a51f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -152,7 +152,10 @@ dependencies { // Access settings & model data implementation(project(":data:settings")) + implementation(project(":core:settings:datastore-prefs")) + implementation(project(":core:settings")) implementation(project(":core:model")) + implementation(libs.androidx.datastore.preferences) // Camera Preview implementation(project(":feature:preview")) diff --git a/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt b/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt new file mode 100644 index 000000000..49a9f434c --- /dev/null +++ b/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt @@ -0,0 +1,54 @@ +/* + * 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 + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride +import com.google.jetpackcamera.core.settings.datastoreprefs.PrefsDataStoreSettingsDataSource +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.settings.SettingsDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppSettingsModule { + + @Provides + @Singleton + fun providePreferencesDataStore(@ApplicationContext context: Context): DataStore { + return PreferenceDataStoreFactory.create( + produceFile = { context.preferencesDataStoreFile("app_settings.preferences_pb") } + ) + } + + @Provides + @Singleton + fun provideSettingsDataSource( + dataStore: DataStore, + @DefaultCaptureModeOverride defaultCaptureMode: CaptureMode + ): SettingsDataSource { + return PrefsDataStoreSettingsDataSource(dataStore, defaultCaptureMode) + } +} diff --git a/core/camera/build.gradle.kts b/core/camera/build.gradle.kts index 9b5df3e3d..53f866d31 100644 --- a/core/camera/build.gradle.kts +++ b/core/camera/build.gradle.kts @@ -139,6 +139,7 @@ dependencies { implementation("javax.inject:javax.inject:1") implementation(libs.androidx.core.ktx) implementation(project(":data:settings")) + implementation(project(":core:settings")) implementation(project(":core:model")) implementation(project(":core:common")) implementation(project(":core:camera:low-light")) diff --git a/core/camera/testing/build.gradle.kts b/core/camera/testing/build.gradle.kts index 80153a553..38f70711f 100644 --- a/core/camera/testing/build.gradle.kts +++ b/core/camera/testing/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":core:camera")) implementation(project(":core:model")) implementation(project(":data:settings")) + implementation(project(":core:settings")) implementation(libs.camera.core) implementation(libs.kotlinx.coroutines.core) diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index 866fb0cc9..d352566c4 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -17,7 +17,6 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.kapt) - alias(libs.plugins.google.protobuf) } android { @@ -70,37 +69,13 @@ android { dependencies { implementation(libs.kotlinx.coroutines.core) - // proto datastore - implementation(libs.androidx.datastore) - implementation(libs.protobuf.kotlin.lite) + // Testing testImplementation(libs.junit) testImplementation(libs.truth) } -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/core/model/src/main/java/com/google/jetpackcamera/model/AspectRatio.kt b/core/model/src/main/java/com/google/jetpackcamera/model/AspectRatio.kt index 8dafa4a5f..2ffda58ae 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/AspectRatio.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/AspectRatio.kt @@ -15,8 +15,10 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.AspectRatio as AspectRatioProto - +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class AspectRatio(val numerator: Int, val denominator: Int) { THREE_FOUR(3, 4), NINE_SIXTEEN(9, 16), @@ -31,21 +33,4 @@ enum class AspectRatio(val numerator: Int, val denominator: Int) { * Returns the landscape aspect ratio as a [Float]. */ fun toLandscapeFloat(): Float = denominator.toFloat() / numerator - - companion object { - - /** returns the AspectRatio enum equivalent of a provided AspectRatioProto */ - fun fromProto(aspectRatioProto: AspectRatioProto): AspectRatio { - return when (aspectRatioProto) { - AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN -> NINE_SIXTEEN - AspectRatioProto.ASPECT_RATIO_ONE_ONE -> ONE_ONE - - // defaults to 3:4 aspect ratio - AspectRatioProto.ASPECT_RATIO_THREE_FOUR, - AspectRatioProto.ASPECT_RATIO_UNDEFINED, - AspectRatioProto.UNRECOGNIZED - -> THREE_FOUR - } - } - } } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/DarkMode.kt b/core/model/src/main/java/com/google/jetpackcamera/model/DarkMode.kt index ff1ac270d..420055360 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/DarkMode.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/DarkMode.kt @@ -15,6 +15,10 @@ */ package com.google.jetpackcamera.model +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class DarkMode { SYSTEM, DARK, diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/DebugSettings.kt b/core/model/src/main/java/com/google/jetpackcamera/model/DebugSettings.kt index 25e05698e..e5bbf6751 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/DebugSettings.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/DebugSettings.kt @@ -15,12 +15,6 @@ */ package com.google.jetpackcamera.model -import android.util.Base64 -import com.google.jetpackcamera.model.LensFacing.Companion.toProto -import com.google.jetpackcamera.model.TestPattern.Companion.toProto -import com.google.jetpackcamera.model.proto.DebugSettings as DebugSettingsProto -import com.google.jetpackcamera.model.proto.debugSettings as debugSettingsProto - /** * Data class for defining settings used in debug flows within the app. * @@ -38,64 +32,81 @@ data class DebugSettings( ) { companion object { /** - * Creates a [DebugSettings] domain model from its protobuf representation. - * - * @param proto The [DebugSettingsProto] instance. - * @return The corresponding [DebugSettings] instance. + * Parses the string into a [DebugSettings] instance. */ - fun fromProto(proto: DebugSettingsProto): DebugSettings { - return DebugSettings( - isDebugModeEnabled = proto.isDebugModeEnabled, - singleLensMode = if (proto.hasSingleLensMode()) { - LensFacing.fromProto(proto.singleLensMode) - } else { - null - }, - testPattern = TestPattern.fromProto(proto.testPattern) - ) - } + fun parseFromString(value: String): DebugSettings { + val parts = value.split(";") + var isDebugModeEnabled = false + var singleLensMode: LensFacing? = null + var testPattern: TestPattern = TestPattern.Off - /** - * Converts a [DebugSettings] domain model to its protobuf representation. - * - * @receiver The [DebugSettings] instance to convert. - * @return The corresponding [DebugSettingsProto] instance. - */ - fun DebugSettings.toProto(): DebugSettingsProto = debugSettingsProto { - isDebugModeEnabled = this@toProto.isDebugModeEnabled - this@toProto.singleLensMode?.let { lensFacing -> - singleLensMode = lensFacing.toProto() + for (part in parts) { + val kv = part.split(":") + if (kv.size == 2) { + when (kv[0]) { + "debug" -> isDebugModeEnabled = kv[1].toBoolean() + "lens" -> singleLensMode = enumValues() + .firstOrNull { it.name == kv[1] } + "pattern" -> { + testPattern = when (kv[1]) { + "Off" -> TestPattern.Off + "ColorBars" -> TestPattern.ColorBars + "ColorBarsFadeToGray" -> TestPattern.ColorBarsFadeToGray + "PN9" -> TestPattern.PN9 + "Custom1" -> TestPattern.Custom1 + else -> { + if (kv[1].startsWith("SolidColor(") && + kv[1].endsWith(")") + ) { + val channels = kv[1] + .removePrefix("SolidColor(") + .removeSuffix(")") + .split(",") + if (channels.size == 4) { + val red = channels[0].toUIntOrNull() + val greenEven = channels[1].toUIntOrNull() + val greenOdd = channels[2].toUIntOrNull() + val blue = channels[3].toUIntOrNull() + if (red != null && + greenEven != null && + greenOdd != null && + blue != null + ) { + TestPattern.SolidColor( + red, + greenEven, + greenOdd, + blue + ) + } else { + TestPattern.Off + } + } else { + TestPattern.Off + } + } else { + TestPattern.Off + } + } + } + } + } + } } - testPattern = this@toProto.testPattern.toProto() - } - - /** - * Parses the encoded byte array into a [DebugSettings] instance. - */ - fun parseFromByteArray(value: ByteArray): DebugSettings { - val protoValue = DebugSettingsProto.parseFrom(value) - return fromProto(protoValue) + return DebugSettings(isDebugModeEnabled, singleLensMode, testPattern) } /** - * Parses the Base64 encoded string into a [DebugSettings] instance. - */ - fun parseFromString(value: String): DebugSettings { - val decodedBytes = Base64.decode(value, Base64.NO_WRAP) - return parseFromByteArray(decodedBytes) - } - - /** - * Encodes the [DebugSettings] data class into a byte array. - */ - fun DebugSettings.encodeAsByteArray(): ByteArray = this.toProto().toByteArray() - - /** - * Encodes the [DebugSettings] data class to a Base64 string. + * Encodes the [DebugSettings] data class to a string. */ fun DebugSettings.encodeAsString(): String { - val protoValue = this.toProto() // Data class -> Proto - return Base64.encodeToString(protoValue.toByteArray(), Base64.NO_WRAP) + val lensStr = singleLensMode?.name ?: "" + val patternStr = when (val pattern = testPattern) { + is TestPattern.SolidColor -> + "SolidColor(${pattern.red},${pattern.greenEven},${pattern.greenOdd},${pattern.blue})" + else -> pattern.toString() + } + return "debug:$isDebugModeEnabled;lens:$lensStr;pattern:$patternStr" } } } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/DynamicRange.kt b/core/model/src/main/java/com/google/jetpackcamera/model/DynamicRange.kt index ecc228741..6b5b2d5f2 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/DynamicRange.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/DynamicRange.kt @@ -14,33 +14,13 @@ * limitations under the License. */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.DynamicRange as DynamicRangeProto - val DEFAULT_HDR_DYNAMIC_RANGE = DynamicRange.HLG10 +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class DynamicRange { SDR, - HLG10; - - companion object { - - /** returns the DynamicRangeType enum equivalent of a provided DynamicRangeTypeProto */ - fun fromProto(dynamicRangeProto: DynamicRangeProto): DynamicRange { - return when (dynamicRangeProto) { - DynamicRangeProto.DYNAMIC_RANGE_HLG10 -> HLG10 - - // Treat unrecognized and unspecified as SDR as a fallback - DynamicRangeProto.DYNAMIC_RANGE_SDR, - DynamicRangeProto.DYNAMIC_RANGE_UNSPECIFIED, - DynamicRangeProto.UNRECOGNIZED -> SDR - } - } - - fun DynamicRange.toProto(): DynamicRangeProto { - return when (this) { - SDR -> DynamicRangeProto.DYNAMIC_RANGE_SDR - HLG10 -> DynamicRangeProto.DYNAMIC_RANGE_HLG10 - } - } - } + HLG10 } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/FlashMode.kt b/core/model/src/main/java/com/google/jetpackcamera/model/FlashMode.kt index f25e32eff..5ab70de3e 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/FlashMode.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/FlashMode.kt @@ -15,6 +15,10 @@ */ package com.google.jetpackcamera.model +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class FlashMode { OFF, ON, diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/ImageOutputFormat.kt b/core/model/src/main/java/com/google/jetpackcamera/model/ImageOutputFormat.kt index 8fa219ab8..5476445dc 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/ImageOutputFormat.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/ImageOutputFormat.kt @@ -15,32 +15,13 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto - val DEFAULT_HDR_IMAGE_OUTPUT = ImageOutputFormat.JPEG_ULTRA_HDR +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class ImageOutputFormat { JPEG, - JPEG_ULTRA_HDR; - - companion object { - - /** returns the DynamicRangeType enum equivalent of a provided DynamicRangeTypeProto */ - fun fromProto(imageOutputFormatProto: ImageOutputFormatProto): ImageOutputFormat { - return when (imageOutputFormatProto) { - ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR -> JPEG_ULTRA_HDR - - // Treat unrecognized as JPEG as a fallback - ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG, - ImageOutputFormatProto.UNRECOGNIZED -> JPEG - } - } - - fun ImageOutputFormat.toProto(): ImageOutputFormatProto { - return when (this) { - JPEG -> ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG - JPEG_ULTRA_HDR -> ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR - } - } - } + JPEG_ULTRA_HDR } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/LensFacing.kt b/core/model/src/main/java/com/google/jetpackcamera/model/LensFacing.kt index 4aa5a27b5..3db87876e 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/LensFacing.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/LensFacing.kt @@ -15,8 +15,10 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.LensFacing as LensFacingProto - +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class LensFacing { BACK, FRONT; @@ -27,25 +29,4 @@ enum class LensFacing { BACK -> FRONT } } - - companion object { - - /** returns the LensFacing enum equivalent of a provided LensFacingProto */ - fun fromProto(lensFacingProto: LensFacingProto): LensFacing { - return when (lensFacingProto) { - LensFacingProto.LENS_FACING_BACK -> BACK - - // Treat unrecognized as front as a fallback - LensFacingProto.LENS_FACING_FRONT, - LensFacingProto.UNRECOGNIZED -> FRONT - } - } - - fun LensFacing.toProto(): LensFacingProto { - return when (this) { - BACK -> LensFacingProto.LENS_FACING_BACK - FRONT -> LensFacingProto.LENS_FACING_FRONT - } - } - } } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/LowLightBoostPriority.kt b/core/model/src/main/java/com/google/jetpackcamera/model/LowLightBoostPriority.kt index eb6aa7367..27015f821 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/LowLightBoostPriority.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/LowLightBoostPriority.kt @@ -15,36 +15,11 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.LowLightBoostPriority as LowLightBoostPriorityProto - +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class LowLightBoostPriority { PRIORITIZE_AE_MODE, - PRIORITIZE_GOOGLE_PLAY_SERVICES; - - companion object { - /** - * Returns the [LowLightBoostPriority] enum equivalent of a provided [LowLightBoostPriorityProto]. - * - * @param lowLightBoostPriorityProto The proto to convert from. - * @return The converted [LowLightBoostPriority]. - */ - fun fromProto( - lowLightBoostPriorityProto: LowLightBoostPriorityProto - ): LowLightBoostPriority { - return when (lowLightBoostPriorityProto) { - LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE -> PRIORITIZE_AE_MODE - LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES -> - PRIORITIZE_GOOGLE_PLAY_SERVICES - LowLightBoostPriorityProto.UNRECOGNIZED -> PRIORITIZE_AE_MODE // Default to AE mode - } - } - - fun LowLightBoostPriority.toProto(): LowLightBoostPriorityProto { - return when (this) { - PRIORITIZE_AE_MODE -> LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE - PRIORITIZE_GOOGLE_PLAY_SERVICES -> - LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES - } - } - } + PRIORITIZE_GOOGLE_PLAY_SERVICES } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/StabilizationMode.kt b/core/model/src/main/java/com/google/jetpackcamera/model/StabilizationMode.kt index bf0dfbe1d..a15412a16 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/StabilizationMode.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/StabilizationMode.kt @@ -15,9 +15,10 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.StabilizationMode as StabilizationModeProto - -/** Enum class representing the device's supported stabilization configurations. */ +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class StabilizationMode { /** Stabilization off */ OFF, @@ -36,22 +37,5 @@ enum class StabilizationMode { HIGH_QUALITY, /** Optical Stabilization (OIS) */ - OPTICAL; - - companion object { - /** returns the AspectRatio enum equivalent of a provided AspectRatioProto */ - fun fromProto(stabilizationModeProto: StabilizationModeProto): StabilizationMode = - when (stabilizationModeProto) { - StabilizationModeProto.STABILIZATION_MODE_OFF -> OFF - StabilizationModeProto.STABILIZATION_MODE_ON -> ON - StabilizationModeProto.STABILIZATION_MODE_HIGH_QUALITY -> HIGH_QUALITY - StabilizationModeProto.STABILIZATION_MODE_OPTICAL -> OPTICAL - - // Default to AUTO - StabilizationModeProto.STABILIZATION_MODE_UNDEFINED, - StabilizationModeProto.UNRECOGNIZED, - StabilizationModeProto.STABILIZATION_MODE_AUTO - -> AUTO - } - } + OPTICAL } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/StreamConfig.kt b/core/model/src/main/java/com/google/jetpackcamera/model/StreamConfig.kt index c052b9a3f..e6e1d209c 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/StreamConfig.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/StreamConfig.kt @@ -15,6 +15,10 @@ */ package com.google.jetpackcamera.model +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class StreamConfig { MULTI_STREAM, SINGLE_STREAM diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/TestPattern.kt b/core/model/src/main/java/com/google/jetpackcamera/model/TestPattern.kt index 15fc3e2f4..d758db1ee 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/TestPattern.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/TestPattern.kt @@ -15,16 +15,6 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.TestPattern as ProtoTestPattern -import com.google.jetpackcamera.model.proto.TestPattern.PatternCase -import com.google.jetpackcamera.model.proto.testPattern as protoTestPattern -import com.google.jetpackcamera.model.proto.testPatternColorBars -import com.google.jetpackcamera.model.proto.testPatternColorBarsFadeToGray -import com.google.jetpackcamera.model.proto.testPatternCustom1 -import com.google.jetpackcamera.model.proto.testPatternOff -import com.google.jetpackcamera.model.proto.testPatternPN9 -import com.google.jetpackcamera.model.proto.testPatternSolidColor - /** * Represents a test pattern to replace sensor pixel data. * @@ -179,56 +169,4 @@ sealed interface TestPattern { ) } } - - companion object { - /** - * Converts a [TestPattern] sealed interface instance to its Protocol Buffer representation - * ([ProtoTestPattern]). - */ - fun TestPattern.toProto(): ProtoTestPattern { - return protoTestPattern { - when (val pattern = this@toProto) { - is Off -> off = testPatternOff {} - is ColorBars -> colorBars = testPatternColorBars {} - is ColorBarsFadeToGray -> - colorBarsFadeToGray = testPatternColorBarsFadeToGray {} - is PN9 -> pn9 = testPatternPN9 {} - is Custom1 -> custom1 = testPatternCustom1 {} - is SolidColor -> solidColor = testPatternSolidColor { - red = pattern.red.toInt() - greenEven = pattern.greenEven.toInt() - greenOdd = pattern.greenOdd.toInt() - blue = pattern.blue.toInt() - } - } - } - } - - /** - * Converts a [ProtoTestPattern] Protocol Buffer message to its Kotlin [TestPattern] sealed - * interface representation. - */ - fun fromProto(proto: ProtoTestPattern): TestPattern { - return when (proto.patternCase) { - PatternCase.OFF, - PatternCase.PATTERN_NOT_SET -> { - // Default to Off if the oneof is not set - Off - } - PatternCase.COLOR_BARS -> ColorBars - PatternCase.COLOR_BARS_FADE_TO_GRAY -> ColorBarsFadeToGray - PatternCase.PN9 -> PN9 - PatternCase.CUSTOM1 -> Custom1 - PatternCase.SOLID_COLOR -> { - val protoSolidColor = proto.solidColor - SolidColor( - red = protoSolidColor.red.toUInt(), - greenEven = protoSolidColor.greenEven.toUInt(), - greenOdd = protoSolidColor.greenOdd.toUInt(), - blue = protoSolidColor.blue.toUInt() - ) - } - } - } - } } diff --git a/core/model/src/main/java/com/google/jetpackcamera/model/VideoQuality.kt b/core/model/src/main/java/com/google/jetpackcamera/model/VideoQuality.kt index 143b7d6a2..a8b9fd82b 100644 --- a/core/model/src/main/java/com/google/jetpackcamera/model/VideoQuality.kt +++ b/core/model/src/main/java/com/google/jetpackcamera/model/VideoQuality.kt @@ -15,37 +15,14 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.VideoQuality as VideoQualityProto - +/** + * WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore. + * Renaming constants will break compatibility with existing saved settings. + */ enum class VideoQuality { UNSPECIFIED, SD, HD, FHD, - UHD; - - companion object { - /** returns the VideoQuality enum equivalent of a provided VideoQualityProto */ - fun fromProto(videoQualityProto: VideoQualityProto): VideoQuality { - return when (videoQualityProto) { - VideoQualityProto.VIDEO_QUALITY_SD -> SD - VideoQualityProto.VIDEO_QUALITY_HD -> HD - VideoQualityProto.VIDEO_QUALITY_FHD -> FHD - VideoQualityProto.VIDEO_QUALITY_UHD -> UHD - VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED, - VideoQualityProto.UNRECOGNIZED - -> UNSPECIFIED - } - } - - fun VideoQuality.toProto(): VideoQualityProto { - return when (this) { - UNSPECIFIED -> VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED - SD -> VideoQualityProto.VIDEO_QUALITY_SD - HD -> VideoQualityProto.VIDEO_QUALITY_HD - FHD -> VideoQualityProto.VIDEO_QUALITY_FHD - UHD -> VideoQualityProto.VIDEO_QUALITY_UHD - } - } - } + UHD } diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/aspect_ratio.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/aspect_ratio.proto deleted file mode 100644 index da8011c35..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/aspect_ratio.proto +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum AspectRatio { - ASPECT_RATIO_UNDEFINED = 0; - ASPECT_RATIO_THREE_FOUR = 1; - ASPECT_RATIO_NINE_SIXTEEN= 2; - ASPECT_RATIO_ONE_ONE = 3; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/dark_mode.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/dark_mode.proto deleted file mode 100644 index 8b38ef403..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/dark_mode.proto +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum DarkMode { - DARK_MODE_SYSTEM = 0; - DARK_MODE_LIGHT= 1; - DARK_MODE_DARK = 2; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/debug_settings.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/debug_settings.proto deleted file mode 100644 index 2c3633afc..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/debug_settings.proto +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -syntax = "proto3"; - -import "com/google/jetpackcamera/model/proto/lens_facing.proto"; -import "com/google/jetpackcamera/model/proto/test_pattern.proto"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -message DebugSettings { - bool is_debug_mode_enabled = 1; - optional LensFacing single_lens_mode = 2; - TestPattern test_pattern = 3; -} diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/dynamic_range.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/dynamic_range.proto deleted file mode 100644 index 6ebe80b97..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/dynamic_range.proto +++ /dev/null @@ -1,26 +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. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum DynamicRange { - DYNAMIC_RANGE_UNSPECIFIED = 0; - DYNAMIC_RANGE_SDR = 1; - DYNAMIC_RANGE_HLG10 = 2; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/flash_mode.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/flash_mode.proto deleted file mode 100644 index fc87418e7..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/flash_mode.proto +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum FlashMode{ - FLASH_MODE_AUTO = 0; - FLASH_MODE_ON = 1; - FLASH_MODE_OFF = 2; - FLASH_MODE_LOW_LIGHT_BOOST = 3; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/image_output_format.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/image_output_format.proto deleted file mode 100644 index b64556b08..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/image_output_format.proto +++ /dev/null @@ -1,25 +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. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum ImageOutputFormat { - IMAGE_OUTPUT_FORMAT_JPEG = 0; - IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR = 1; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/lens_facing.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/lens_facing.proto deleted file mode 100644 index 2e898a4eb..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/lens_facing.proto +++ /dev/null @@ -1,25 +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. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum LensFacing { - LENS_FACING_BACK = 0; - LENS_FACING_FRONT = 1; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/low_light_boost_priority.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/low_light_boost_priority.proto deleted file mode 100644 index bd8fd7dbf..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/low_light_boost_priority.proto +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum LowLightBoostPriority { - LOW_LIGHT_BOOST_PRIORITY_AE_MODE = 0; - LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES = 1; -} diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/stabilization_mode.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/stabilization_mode.proto deleted file mode 100644 index bdba01437..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/stabilization_mode.proto +++ /dev/null @@ -1,29 +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. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum StabilizationMode { - STABILIZATION_MODE_UNDEFINED = 0; - STABILIZATION_MODE_AUTO = 1; - STABILIZATION_MODE_OFF = 2; - STABILIZATION_MODE_ON = 3; - STABILIZATION_MODE_HIGH_QUALITY = 4; - STABILIZATION_MODE_OPTICAL = 5; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/stream_config.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/stream_config.proto deleted file mode 100644 index 28af05d60..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/stream_config.proto +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum StreamConfig { - STREAM_CONFIG_UNDEFINED = 0; - STREAM_CONFIG_MULTI_STREAM = 1; - STREAM_CONFIG_SINGLE_STREAM = 2; -} \ No newline at end of file diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/test_pattern.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/test_pattern.proto deleted file mode 100644 index b74d3d96d..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/test_pattern.proto +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -// Represents the TestPattern sealed interface. -message TestPattern { - oneof pattern { - TestPatternOff off = 1; - TestPatternColorBars color_bars = 2; - TestPatternColorBarsFadeToGray color_bars_fade_to_gray = 3; - TestPatternPN9 pn9 = 4; - TestPatternCustom1 custom1 = 5; - TestPatternSolidColor solid_color = 6; - } -} - -// Corresponds to TestPattern.Off -message TestPatternOff {} - -// Corresponds to TestPattern.ColorBars -message TestPatternColorBars {} - -// Corresponds to TestPattern.ColorBarsFadeToGray -message TestPatternColorBarsFadeToGray {} - -// Corresponds to TestPattern.PN9 -message TestPatternPN9 {} - -// Corresponds to TestPattern.Custom1 -message TestPatternCustom1 {} - -// Corresponds to TestPattern.SolidColor -message TestPatternSolidColor { - uint32 red = 1; - uint32 green_even = 2; - uint32 green_odd = 3; - uint32 blue = 4; -} diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/video_quality.proto b/core/model/src/main/proto/com/google/jetpackcamera/model/proto/video_quality.proto deleted file mode 100644 index b88b423cc..000000000 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/video_quality.proto +++ /dev/null @@ -1,28 +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. - */ - -syntax = "proto3"; - -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; - -enum VideoQuality { - VIDEO_QUALITY_UNSPECIFIED = 0; - VIDEO_QUALITY_SD = 1; - VIDEO_QUALITY_HD = 2; - VIDEO_QUALITY_FHD = 3; - VIDEO_QUALITY_UHD = 4; -} \ No newline at end of file diff --git a/core/settings/build.gradle.kts b/core/settings/build.gradle.kts new file mode 100644 index 000000000..35c256a56 --- /dev/null +++ b/core/settings/build.gradle.kts @@ -0,0 +1,59 @@ +/* + * 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.kotlin.kapt) + alias(libs.plugins.dagger.hilt.android) +} + +android { + namespace = "com.google.jetpackcamera.core.settings" + 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") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(libs.kotlinx.coroutines.core) + + // Hilt + implementation(libs.dagger.hilt.android) + kapt(libs.dagger.hilt.compiler) + + // Domain models + implementation(project(":core:model")) +} + +kapt { + correctErrorTypes = true +} diff --git a/core/settings/consumer-rules.pro b/core/settings/consumer-rules.pro new file mode 100644 index 000000000..bf52d0acc --- /dev/null +++ b/core/settings/consumer-rules.pro @@ -0,0 +1 @@ +# Consumer proguard rules for core:settings diff --git a/core/settings/datastore-prefs/.gitignore b/core/settings/datastore-prefs/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/core/settings/datastore-prefs/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/settings/datastore-prefs/build.gradle.kts b/core/settings/datastore-prefs/build.gradle.kts new file mode 100644 index 000000000..c0ca9f8d4 --- /dev/null +++ b/core/settings/datastore-prefs/build.gradle.kts @@ -0,0 +1,83 @@ +/* + * 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.core.settings.datastoreprefs" + 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") + } + + 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.kotlinx.coroutines.core) + + + + // Preferences DataStore (Zero Protobuf) + implementation(libs.androidx.datastore.preferences) + + // 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(":core:settings:datastore-prefs:testing")) + + // Access Model and Settings Interface + implementation(project(":core:model")) + implementation(project(":core:common")) + implementation(project(":core:settings")) +} + + diff --git a/core/settings/datastore-prefs/consumer-rules.pro b/core/settings/datastore-prefs/consumer-rules.pro new file mode 100644 index 000000000..a554da829 --- /dev/null +++ b/core/settings/datastore-prefs/consumer-rules.pro @@ -0,0 +1 @@ +# Consumer rules for settings-datastore diff --git a/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSourceInstrumentedTest.kt b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSourceInstrumentedTest.kt new file mode 100644 index 000000000..a74760915 --- /dev/null +++ b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSourceInstrumentedTest.kt @@ -0,0 +1,249 @@ +/* + * 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.settings.datastoreprefs + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.core.settings.datastoreprefs.testing.FakeDataStoreModule +import com.google.jetpackcamera.model.AspectRatio +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.model.LowLightBoostPriority +import com.google.jetpackcamera.model.StabilizationMode +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.model.UNLIMITED_VIDEO_DURATION +import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS +import java.io.File +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class PrefsDataStoreSettingsDataSourceInstrumentedTest { + @get:Rule + val tempFolder = TemporaryFolder() + private lateinit var testFile: File + + private lateinit var testDataStore: DataStore + private lateinit var datastoreScope: CoroutineScope + private lateinit var dataSource: PrefsDataStoreSettingsDataSource + + @Before + fun setup() = runTest { + Dispatchers.setMain(StandardTestDispatcher()) + testFile = tempFolder.newFile("test_settings.preferences_pb") + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + testDataStore = FakeDataStoreModule.providePreferenceDataStore( + scope = datastoreScope, + file = testFile + ) + dataSource = PrefsDataStoreSettingsDataSource( + dataStore = testDataStore, + defaultCaptureModeOverride = CaptureMode.STANDARD + ) + advanceUntilIdle() + } + + @After + fun tearDown() { + datastoreScope.cancel() + } + + @Test + fun datasource_can_fetch_initial_settings() = runTest { + val cameraAppSettings: CameraAppSettings = dataSource.getCurrentDefaultCameraAppSettings() + advanceUntilIdle() + assertThat(cameraAppSettings).isEqualTo(DEFAULT_CAMERA_APP_SETTINGS) + } + + @Test + fun can_update_default_to_front_camera() = runTest { + val initialDefaultLensFacing = + dataSource.getCurrentDefaultCameraAppSettings().cameraLensFacing + dataSource.updateDefaultLensFacing(LensFacing.FRONT) + val newDefaultLensFacing = dataSource.getCurrentDefaultCameraAppSettings().cameraLensFacing + advanceUntilIdle() + + assertThat(initialDefaultLensFacing).isEqualTo(LensFacing.BACK) + assertThat(newDefaultLensFacing).isEqualTo(LensFacing.FRONT) + } + + @Test + fun can_update_flash_mode() = runTest { + val initialFlashModeStatus = dataSource.getCurrentDefaultCameraAppSettings().flashMode + dataSource.updateFlashModeStatus(FlashMode.ON) + val newFlashModeStatus = dataSource.getCurrentDefaultCameraAppSettings().flashMode + advanceUntilIdle() + + assertThat(initialFlashModeStatus).isEqualTo(FlashMode.OFF) + assertThat(newFlashModeStatus).isEqualTo(FlashMode.ON) + } + + @Test + fun can_update_dynamic_range() = runTest { + val initialDynamicRange = dataSource.getCurrentDefaultCameraAppSettings().dynamicRange + dataSource.updateDynamicRange(dynamicRange = DynamicRange.HLG10) + advanceUntilIdle() + + val newDynamicRange = dataSource.getCurrentDefaultCameraAppSettings().dynamicRange + assertThat(initialDynamicRange).isEqualTo(DynamicRange.SDR) + assertThat(newDynamicRange).isEqualTo(DynamicRange.HLG10) + } + + @Test + fun can_update_image_format() = runTest { + val initialImageFormat = dataSource.getCurrentDefaultCameraAppSettings().imageFormat + dataSource.updateImageFormat(imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR) + advanceUntilIdle() + + val newImageFormat = dataSource.getCurrentDefaultCameraAppSettings().imageFormat + assertThat(initialImageFormat).isEqualTo(ImageOutputFormat.JPEG) + assertThat(newImageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) + } + + @Test + fun can_update_dark_mode() = runTest { + val initialDarkMode = dataSource.getCurrentDefaultCameraAppSettings().darkMode + dataSource.updateDarkModeStatus(DarkMode.LIGHT) + advanceUntilIdle() + + val newDarkMode = dataSource.getCurrentDefaultCameraAppSettings().darkMode + assertThat(initialDarkMode).isEqualTo(DarkMode.DARK) + assertThat(newDarkMode).isEqualTo(DarkMode.LIGHT) + } + + @Test + fun can_update_target_frame_rate() = runTest { + val initialFrameRate = dataSource.getCurrentDefaultCameraAppSettings().targetFrameRate + dataSource.updateTargetFrameRate(30) + advanceUntilIdle() + + val newFrameRate = dataSource.getCurrentDefaultCameraAppSettings().targetFrameRate + assertThat(initialFrameRate).isEqualTo(0) + assertThat(newFrameRate).isEqualTo(30) + } + + @Test + fun can_update_aspect_ratio() = runTest { + val initialAspectRatio = dataSource.getCurrentDefaultCameraAppSettings().aspectRatio + dataSource.updateAspectRatio(AspectRatio.THREE_FOUR) + advanceUntilIdle() + + val newAspectRatio = dataSource.getCurrentDefaultCameraAppSettings().aspectRatio + assertThat(initialAspectRatio).isEqualTo(AspectRatio.NINE_SIXTEEN) + assertThat(newAspectRatio).isEqualTo(AspectRatio.THREE_FOUR) + } + + @Test + fun can_update_stream_config() = runTest { + val initialStreamConfig = dataSource.getCurrentDefaultCameraAppSettings().streamConfig + dataSource.updateStreamConfig(StreamConfig.SINGLE_STREAM) + advanceUntilIdle() + + val newStreamConfig = dataSource.getCurrentDefaultCameraAppSettings().streamConfig + assertThat(initialStreamConfig).isEqualTo(StreamConfig.MULTI_STREAM) + assertThat(newStreamConfig).isEqualTo(StreamConfig.SINGLE_STREAM) + } + + @Test + fun can_update_stabilization_mode() = runTest { + val initialStabilizationMode = + dataSource.getCurrentDefaultCameraAppSettings().stabilizationMode + dataSource.updateStabilizationMode(StabilizationMode.HIGH_QUALITY) + advanceUntilIdle() + + val newStabilizationMode = + dataSource.getCurrentDefaultCameraAppSettings().stabilizationMode + assertThat(initialStabilizationMode).isEqualTo(StabilizationMode.AUTO) + assertThat(newStabilizationMode).isEqualTo(StabilizationMode.HIGH_QUALITY) + } + + @Test + fun can_update_max_video_duration() = runTest { + val initialDuration = + dataSource.getCurrentDefaultCameraAppSettings().maxVideoDurationMillis + dataSource.updateMaxVideoDuration(60000L) + advanceUntilIdle() + + val newDuration = + dataSource.getCurrentDefaultCameraAppSettings().maxVideoDurationMillis + assertThat(initialDuration).isEqualTo(UNLIMITED_VIDEO_DURATION) + assertThat(newDuration).isEqualTo(60000L) + } + + @Test + fun can_update_video_quality() = runTest { + val initialVideoQuality = + dataSource.getCurrentDefaultCameraAppSettings().videoQuality + dataSource.updateVideoQuality(VideoQuality.HD) + advanceUntilIdle() + + val newVideoQuality = + dataSource.getCurrentDefaultCameraAppSettings().videoQuality + assertThat(initialVideoQuality).isEqualTo(VideoQuality.UNSPECIFIED) + assertThat(newVideoQuality).isEqualTo(VideoQuality.HD) + } + + @Test + fun can_update_low_light_boost_priority() = runTest { + val initialLowLightBoostPriority = + dataSource.getCurrentDefaultCameraAppSettings().lowLightBoostPriority + dataSource.updateLowLightBoostPriority( + LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES + ) + advanceUntilIdle() + + val newLowLightBoostPriority = + dataSource.getCurrentDefaultCameraAppSettings().lowLightBoostPriority + assertThat(initialLowLightBoostPriority) + .isEqualTo(LowLightBoostPriority.PRIORITIZE_AE_MODE) + assertThat(newLowLightBoostPriority) + .isEqualTo(LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES) + } + + @Test + fun can_update_audio_enabled() = runTest { + val initialAudioEnabled = dataSource.getCurrentDefaultCameraAppSettings().audioEnabled + dataSource.updateAudioEnabled(false) + advanceUntilIdle() + + val newAudioEnabled = dataSource.getCurrentDefaultCameraAppSettings().audioEnabled + assertThat(initialAudioEnabled).isTrue() + assertThat(newAudioEnabled).isFalse() + } +} diff --git a/core/settings/datastore-prefs/src/main/AndroidManifest.xml b/core/settings/datastore-prefs/src/main/AndroidManifest.xml new file mode 100644 index 000000000..53b89b19c --- /dev/null +++ b/core/settings/datastore-prefs/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PreferenceKeys.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PreferenceKeys.kt new file mode 100644 index 000000000..9732b546e --- /dev/null +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PreferenceKeys.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.core.settings.datastoreprefs + +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey + +internal object PreferenceKeys { + val KEY_LENS_FACING = stringPreferencesKey("lens_facing") + val KEY_DARK_MODE = stringPreferencesKey("dark_mode") + val KEY_FLASH_MODE = stringPreferencesKey("flash_mode") + val KEY_ASPECT_RATIO = stringPreferencesKey("aspect_ratio") + val KEY_STREAM_CONFIG = stringPreferencesKey("stream_config") + val KEY_STABILIZATION_MODE = stringPreferencesKey("stabilization_mode") + val KEY_DYNAMIC_RANGE = stringPreferencesKey("dynamic_range") + val KEY_VIDEO_QUALITY = stringPreferencesKey("video_quality") + val KEY_IMAGE_FORMAT = stringPreferencesKey("image_format") + val KEY_MAX_VIDEO_DURATION = longPreferencesKey("max_video_duration") + val KEY_AUDIO_ENABLED = booleanPreferencesKey("audio_enabled") + val KEY_LOW_LIGHT_BOOST_PRIORITY = stringPreferencesKey("low_light_boost_priority") + val KEY_TARGET_FRAME_RATE = intPreferencesKey("target_frame_rate") + val KEY_CONCURRENT_CAMERA_MODE = stringPreferencesKey("concurrent_camera_mode") +} diff --git a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt new file mode 100644 index 000000000..c4829c6bf --- /dev/null +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt @@ -0,0 +1,177 @@ +/* + * 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.settings.datastoreprefs + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode +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.model.LowLightBoostPriority +import com.google.jetpackcamera.model.StabilizationMode +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.model.TARGET_FPS_AUTO +import com.google.jetpackcamera.model.UNLIMITED_VIDEO_DURATION +import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.settings.SettingsDataSource +import com.google.jetpackcamera.settings.model.CameraAppSettings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +/** + * Implementation of [SettingsDataSource] with locally stored Preferences DataStore. + */ +class PrefsDataStoreSettingsDataSource( + private val dataStore: DataStore, + @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode +) : SettingsDataSource { + + override val defaultCameraAppSettings: Flow = dataStore.data.map { prefs -> + CameraAppSettings( + cameraLensFacing = prefs[PreferenceKeys.KEY_LENS_FACING] + .toEnumOrDefault(LensFacing.BACK), + darkMode = prefs[PreferenceKeys.KEY_DARK_MODE].toEnumOrDefault(DarkMode.DARK), + flashMode = prefs[PreferenceKeys.KEY_FLASH_MODE].toEnumOrDefault(FlashMode.OFF), + aspectRatio = prefs[PreferenceKeys.KEY_ASPECT_RATIO] + .toEnumOrDefault(AspectRatio.NINE_SIXTEEN), + stabilizationMode = prefs[PreferenceKeys.KEY_STABILIZATION_MODE] + .toEnumOrDefault(StabilizationMode.AUTO), + targetFrameRate = prefs[PreferenceKeys.KEY_TARGET_FRAME_RATE] ?: TARGET_FPS_AUTO, + streamConfig = prefs[PreferenceKeys.KEY_STREAM_CONFIG] + .toEnumOrDefault(StreamConfig.MULTI_STREAM), + lowLightBoostPriority = prefs[PreferenceKeys.KEY_LOW_LIGHT_BOOST_PRIORITY] + .toEnumOrDefault(LowLightBoostPriority.PRIORITIZE_AE_MODE), + dynamicRange = prefs[PreferenceKeys.KEY_DYNAMIC_RANGE] + .toEnumOrDefault(DynamicRange.SDR), + imageFormat = prefs[PreferenceKeys.KEY_IMAGE_FORMAT] + .toEnumOrDefault(ImageOutputFormat.JPEG), + maxVideoDurationMillis = prefs[PreferenceKeys.KEY_MAX_VIDEO_DURATION] + ?: UNLIMITED_VIDEO_DURATION, + videoQuality = prefs[PreferenceKeys.KEY_VIDEO_QUALITY] + .toEnumOrDefault(VideoQuality.UNSPECIFIED), + audioEnabled = prefs[PreferenceKeys.KEY_AUDIO_ENABLED] ?: true, + concurrentCameraMode = prefs[PreferenceKeys.KEY_CONCURRENT_CAMERA_MODE] + .toEnumOrDefault(ConcurrentCameraMode.OFF), + captureMode = defaultCaptureModeOverride + ) + } + + override suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings = + defaultCameraAppSettings.first() + + override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_LENS_FACING] = lensFacing.name + } + } + + override suspend fun updateDarkModeStatus(darkMode: DarkMode) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_DARK_MODE] = darkMode.name + } + } + + override suspend fun updateFlashModeStatus(flashMode: FlashMode) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_FLASH_MODE] = flashMode.name + } + } + + override suspend fun updateTargetFrameRate(targetFrameRate: Int) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_TARGET_FRAME_RATE] = targetFrameRate + } + } + + override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_ASPECT_RATIO] = aspectRatio.name + } + } + + override suspend fun updateStreamConfig(streamConfig: StreamConfig) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_STREAM_CONFIG] = streamConfig.name + } + } + + override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_STABILIZATION_MODE] = stabilizationMode.name + } + } + + override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_DYNAMIC_RANGE] = dynamicRange.name + } + } + + override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_IMAGE_FORMAT] = imageFormat.name + } + } + + override suspend fun updateMaxVideoDuration(durationMillis: Long) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_MAX_VIDEO_DURATION] = durationMillis + } + } + + override suspend fun updateVideoQuality(videoQuality: VideoQuality) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_VIDEO_QUALITY] = videoQuality.name + } + } + + override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_LOW_LIGHT_BOOST_PRIORITY] = lowLightBoostPriority.name + } + } + + override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_AUDIO_ENABLED] = isAudioEnabled + } + } + + override suspend fun updateConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) { + dataStore.edit { prefs -> + prefs[PreferenceKeys.KEY_CONCURRENT_CAMERA_MODE] = concurrentCameraMode.name + } + } + + companion object { + private inline fun > String?.toEnumOrDefault(default: T): T { + if (this == null) return default + return try { + enumValueOf(this) + } catch (e: IllegalArgumentException) { + default + } + } + } +} diff --git a/core/settings/datastore-prefs/testing/build.gradle.kts b/core/settings/datastore-prefs/testing/build.gradle.kts new file mode 100644 index 000000000..0e619ddbd --- /dev/null +++ b/core/settings/datastore-prefs/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.core.settings.datastoreprefs.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(":core:settings:datastore-prefs")) + implementation(project(":data:settings")) + implementation(project(":core:settings")) + implementation(project(":core:model")) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.androidx.datastore.preferences) +} diff --git a/core/settings/datastore-prefs/testing/src/main/AndroidManifest.xml b/core/settings/datastore-prefs/testing/src/main/AndroidManifest.xml new file mode 100644 index 000000000..53b89b19c --- /dev/null +++ b/core/settings/datastore-prefs/testing/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto b/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt similarity index 53% rename from core/model/src/main/proto/com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto rename to core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt index 4f50d2524..8c87ccd56 100644 --- a/core/model/src/main/proto/com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto +++ b/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt @@ -13,12 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -syntax = "proto3"; +package com.google.jetpackcamera.core.settings.datastoreprefs.testing -option java_package = "com.google.jetpackcamera.model.proto"; -option java_multiple_files = true; +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import java.io.File +import kotlinx.coroutines.CoroutineScope -enum ConcurrentCameraMode { - CONCURRENT_CAMERA_MODE_OFF = 0; - CONCURRENT_CAMERA_MODE_DUAL = 1; +object FakeDataStoreModule { + + fun providePreferenceDataStore(scope: CoroutineScope, file: File): DataStore = + PreferenceDataStoreFactory.create( + scope = scope, + produceFile = { file } + ) } diff --git a/core/settings/src/main/AndroidManifest.xml b/core/settings/src/main/AndroidManifest.xml new file mode 100644 index 000000000..53b89b19c --- /dev/null +++ b/core/settings/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/core/settings/src/main/java/com/google/jetpackcamera/settings/SettingsDataSource.kt b/core/settings/src/main/java/com/google/jetpackcamera/settings/SettingsDataSource.kt new file mode 100644 index 000000000..cb190b607 --- /dev/null +++ b/core/settings/src/main/java/com/google/jetpackcamera/settings/SettingsDataSource.kt @@ -0,0 +1,116 @@ +/* + * 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.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.ConcurrentCameraMode +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.model.LowLightBoostPriority +import com.google.jetpackcamera.model.StabilizationMode +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.settings.model.CameraAppSettings +import kotlinx.coroutines.flow.Flow + +/** + * Data source interface for fetching and updating persistent camera application settings. + */ +interface SettingsDataSource { + + /** + * A [Flow] emitting the current default [CameraAppSettings]. + */ + val defaultCameraAppSettings: Flow + + /** + * Retrieves the current default [CameraAppSettings] as a single snapshot. + */ + suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings + + /** + * Updates the default camera lens facing selection. + */ + suspend fun updateDefaultLensFacing(lensFacing: LensFacing) + + /** + * Updates the user-preferred dark mode setting. + */ + suspend fun updateDarkModeStatus(darkMode: DarkMode) + + /** + * Updates the default flash mode selection. + */ + suspend fun updateFlashModeStatus(flashMode: FlashMode) + + /** + * Updates the default capture aspect ratio. + */ + suspend fun updateAspectRatio(aspectRatio: AspectRatio) + + /** + * Updates the default stream configuration mode. + */ + suspend fun updateStreamConfig(streamConfig: StreamConfig) + + /** + * Updates the low light boost execution priority setting. + */ + suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) + + /** + * Updates the default video stabilization mode. + */ + suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) + + /** + * Updates the default capture dynamic range format. + */ + suspend fun updateDynamicRange(dynamicRange: DynamicRange) + + /** + * Updates the target frames-per-second setting for video recording. + */ + suspend fun updateTargetFrameRate(targetFrameRate: Int) + + /** + * Updates the default image capture output format. + */ + suspend fun updateImageFormat(imageFormat: ImageOutputFormat) + + /** + * Updates the maximum video duration limit in milliseconds. + */ + suspend fun updateMaxVideoDuration(durationMillis: Long) + + /** + * Updates the default video recording output quality setting. + */ + suspend fun updateVideoQuality(videoQuality: VideoQuality) + + /** + * Updates whether audio recording is enabled by default during video capture. + */ + suspend fun updateAudioEnabled(isAudioEnabled: Boolean) + + /** + * Updates the default concurrent camera mode. + */ + suspend fun updateConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) +} diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt b/core/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt similarity index 100% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt rename to core/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt b/core/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt similarity index 100% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt rename to core/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt diff --git a/data/camera/build.gradle.kts b/data/camera/build.gradle.kts index fb059c98e..8a541423a 100644 --- a/data/camera/build.gradle.kts +++ b/data/camera/build.gradle.kts @@ -73,6 +73,7 @@ dependencies { implementation(project(":core:common")) implementation(project(":core:model")) implementation(project(":data:settings")) + implementation(project(":core:settings")) } // Allow references to generated code diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 443986762..7748c8d51 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,10 +75,6 @@ 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) @@ -92,28 +87,7 @@ dependencies { // 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") - } - } - - task.builtins { - create("kotlin") { - option("lite") - } - } - } - } + implementation(project(":core:settings")) } // Allow references to generated code diff --git a/data/settings/consumer-rules.pro b/data/settings/consumer-rules.pro index 2b9ca7556..e69de29bb 100644 --- a/data/settings/consumer-rules.pro +++ b/data/settings/consumer-rules.pro @@ -1 +0,0 @@ --keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* {;} \ No newline at end of file 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 deleted file mode 100644 index 703783b23..000000000 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 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 java.io.File -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TemporaryFolder -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) -class DataStoreModuleTest { - @get:Rule - val tempFolder = TemporaryFolder() - private lateinit var testFile: File - - @Before - fun setUp() { - testFile = tempFolder.newFile() - } - - @Test - fun dataStoreModule_read_can_handle_corrupted_file() = runTest { - // should handle exception and replace file information - val dataStore: DataStore = FakeDataStoreModule.provideDataStore( - scope = this.backgroundScope, - serializer = FakeJcaSettingsSerializer(failReadWithCorruptionException = true), - file = testFile - ) - val datastoreValue = dataStore.data.first() - advanceUntilIdle() - - assertThat(datastoreValue).isEqualTo(JcaSettings.getDefaultInstance()) - } -} diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt deleted file mode 100644 index fafb38dc3..000000000 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -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.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.model.CameraAppSettings -import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS -import java.io.File -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidJUnit4::class) -class LocalSettingsRepositoryInstrumentedTest { - private val testContext: Context = ApplicationProvider.getApplicationContext() - private lateinit var testDataStore: DataStore - private lateinit var datastoreScope: CoroutineScope - private lateinit var repository: LocalSettingsRepository - - @Before - fun setup() = runTest { - Dispatchers.setMain(StandardTestDispatcher()) - testDataStore = provideDataStore(testContext) - datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - - testDataStore = DataStoreFactory.create( - serializer = JcaSettingsSerializer, - scope = datastoreScope - ) { - testContext.dataStoreFile("test_jca_settings.pb") - } - repository = LocalSettingsRepository( - jcaSettings = testDataStore, - defaultCaptureModeOverride = CaptureMode.STANDARD - ) - advanceUntilIdle() - } - - @After - fun tearDown() { - File( - ApplicationProvider.getApplicationContext().filesDir, - "datastore" - ).deleteRecursively() - - datastoreScope.cancel() - } - - @Test - fun repository_can_fetch_initial_datastore() = runTest { - // if you've created a new setting value and this test is failing, be sure to check that - // JcaSettingsSerializer.kt defaultValue has been properly modified :) - - val cameraAppSettings: CameraAppSettings = repository.getCurrentDefaultCameraAppSettings() - - advanceUntilIdle() - assertThat(cameraAppSettings).isEqualTo(DEFAULT_CAMERA_APP_SETTINGS) - } - - @Test - fun can_update_dark_mode() = runTest { - val initialDarkModeStatus = repository.getCurrentDefaultCameraAppSettings().darkMode - repository.updateDarkModeStatus(DarkMode.LIGHT) - val newDarkModeStatus = repository.getCurrentDefaultCameraAppSettings().darkMode - - advanceUntilIdle() - assertThat(initialDarkModeStatus).isNotEqualTo(newDarkModeStatus) - assertThat(initialDarkModeStatus).isEqualTo(DarkMode.DARK) - assertThat(newDarkModeStatus).isEqualTo(DarkMode.LIGHT) - } - - @Test - fun can_update_default_to_front_camera() = runTest { - // default lens facing starts as BACK - val initialDefaultLensFacing = - repository.getCurrentDefaultCameraAppSettings().cameraLensFacing - repository.updateDefaultLensFacing(LensFacing.FRONT) - // default lens facing is now FRONT - val newDefaultLensFacing = repository.getCurrentDefaultCameraAppSettings().cameraLensFacing - advanceUntilIdle() - - assertThat(initialDefaultLensFacing).isEqualTo(LensFacing.BACK) - assertThat(newDefaultLensFacing).isEqualTo(LensFacing.FRONT) - } - - @Test - fun can_update_flash_mode() = runTest { - // default flash mode starts as OFF - val initialFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode - repository.updateFlashModeStatus(FlashMode.ON) - // default flash mode is now ON - val newFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode - advanceUntilIdle() - - assertThat(initialFlashModeStatus).isEqualTo(FlashMode.OFF) - assertThat(newFlashModeStatus).isEqualTo(FlashMode.ON) - } - - @Test - fun can_update_dynamic_range() = runTest { - val initialDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange - - repository.updateDynamicRange(dynamicRange = DynamicRange.HLG10) - - advanceUntilIdle() - - val newDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange - - assertThat(initialDynamicRange).isEqualTo(DynamicRange.SDR) - assertThat(newDynamicRange).isEqualTo(DynamicRange.HLG10) - } - - @Test - fun can_update_image_format() = runTest { - val initialImageFormat = repository.getCurrentDefaultCameraAppSettings().imageFormat - - repository.updateImageFormat(imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR) - - advanceUntilIdle() - - val newImageFormat = repository.getCurrentDefaultCameraAppSettings().imageFormat - - assertThat(initialImageFormat).isEqualTo(ImageOutputFormat.JPEG) - assertThat(newImageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) - } -} 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..9886c5842 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 @@ -37,7 +37,8 @@ interface ConstraintsModule { /** * ConstraintsRepository without setter. * - * This is the same instance as the activity-retained SettableConstraintsRepository, but does not + * This is the same instance as the activity-retained + * SettableConstraintsRepository, but does not * have the ability to update the constraints. */ @Binds diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt deleted file mode 100644 index 7f8862455..000000000 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.dataStoreFile -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton -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 { - private const val FILE_LOCATION = "app_settings.pb" - - @Provides - @Singleton - fun provideDataStore(@ApplicationContext context: Context): DataStore = - DataStoreFactory.create( - corruptionHandler = ReplaceFileCorruptionHandler { JcaSettings.getDefaultInstance() }, - // TODO(b/286245619, kimblebee@): Inject coroutine scope once module providing default IO dispatcher scope is implemented - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - serializer = JcaSettingsSerializer, - produceFile = { - context.dataStoreFile(FILE_LOCATION) - } - ) -} diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt deleted file mode 100644 index eec14b2af..000000000 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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 androidx.datastore.core.CorruptionException -import androidx.datastore.core.Serializer -import com.google.jetpackcamera.model.UNLIMITED_VIDEO_DURATION -import com.google.jetpackcamera.model.proto.AspectRatio -import com.google.jetpackcamera.model.proto.ConcurrentCameraMode as ConcurrentCameraModeProto -import com.google.jetpackcamera.model.proto.DarkMode -import com.google.jetpackcamera.model.proto.DynamicRange -import com.google.jetpackcamera.model.proto.FlashMode -import com.google.jetpackcamera.model.proto.ImageOutputFormat -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.protobuf.InvalidProtocolBufferException -import java.io.InputStream -import java.io.OutputStream -object JcaSettingsSerializer : Serializer { - - override val defaultValue: JcaSettings = JcaSettings.newBuilder() - .setDarkModeStatus(DarkMode.DARK_MODE_DARK) - .setDefaultLensFacing(LensFacing.LENS_FACING_BACK) - .setFlashModeStatus(FlashMode.FLASH_MODE_OFF) - .setAspectRatioStatus(AspectRatio.ASPECT_RATIO_NINE_SIXTEEN) - .setStreamConfigStatus(StreamConfig.STREAM_CONFIG_MULTI_STREAM) - .setStabilizationMode(StabilizationMode.STABILIZATION_MODE_AUTO) - .setDynamicRangeStatus(DynamicRange.DYNAMIC_RANGE_UNSPECIFIED) - .setImageFormatStatus(ImageOutputFormat.IMAGE_OUTPUT_FORMAT_JPEG) - .setMaxVideoDurationMillis(UNLIMITED_VIDEO_DURATION) - .setVideoQuality(VideoQuality.VIDEO_QUALITY_UNSPECIFIED) - .setAudioEnabledStatus(true) - .setConcurrentCameraModeStatus(ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_OFF) - .build() - - override suspend fun readFrom(input: InputStream): JcaSettings { - try { - return JcaSettings.parseFrom(input) - } catch (exception: InvalidProtocolBufferException) { - throw CorruptionException("Cannot read proto.", exception) - } - } - - override suspend fun writeTo(t: JcaSettings, output: OutputStream) = t.writeTo(output) -} diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt index ea5e2fb27..c5336d7b5 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/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. @@ -15,233 +15,87 @@ */ package com.google.jetpackcamera.settings -import androidx.datastore.core.DataStore -import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride import com.google.jetpackcamera.model.AspectRatio -import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DarkMode import com.google.jetpackcamera.model.DynamicRange -import com.google.jetpackcamera.model.DynamicRange.Companion.toProto import com.google.jetpackcamera.model.FlashMode 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.fromProto -import com.google.jetpackcamera.model.LowLightBoostPriority.Companion.toProto import com.google.jetpackcamera.model.StabilizationMode import com.google.jetpackcamera.model.StreamConfig 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.ConcurrentCameraMode as ConcurrentCameraModeProto -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.model.CameraAppSettings import javax.inject.Inject -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.Flow /** - * Implementation of [SettingsRepository] with locally stored settings. + * Implementation of [SettingsRepository] delegating to [SettingsDataSource]. */ class LocalSettingsRepository @Inject constructor( - private val jcaSettings: DataStore, - @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode -) : - SettingsRepository { - - override val defaultCameraAppSettings = jcaSettings.data - .map { - CameraAppSettings( - cameraLensFacing = LensFacing.fromProto(it.defaultLensFacing), - darkMode = when (it.darkModeStatus) { - DarkModeProto.DARK_MODE_DARK -> DarkMode.DARK - DarkModeProto.DARK_MODE_LIGHT -> DarkMode.LIGHT - DarkModeProto.DARK_MODE_SYSTEM -> DarkMode.SYSTEM - else -> DarkMode.DARK - }, - flashMode = when (it.flashModeStatus) { - FlashModeProto.FLASH_MODE_AUTO -> FlashMode.AUTO - FlashModeProto.FLASH_MODE_ON -> FlashMode.ON - FlashModeProto.FLASH_MODE_OFF -> FlashMode.OFF - FlashModeProto.FLASH_MODE_LOW_LIGHT_BOOST -> FlashMode.LOW_LIGHT_BOOST - else -> FlashMode.OFF - }, - aspectRatio = AspectRatio.fromProto(it.aspectRatioStatus), - stabilizationMode = StabilizationMode.fromProto(it.stabilizationMode), - targetFrameRate = it.targetFrameRate, - streamConfig = when (it.streamConfigStatus) { - StreamConfigProto.STREAM_CONFIG_SINGLE_STREAM -> StreamConfig.SINGLE_STREAM - StreamConfigProto.STREAM_CONFIG_MULTI_STREAM -> StreamConfig.MULTI_STREAM - else -> StreamConfig.MULTI_STREAM - }, - lowLightBoostPriority = fromProto(it.lowLightBoostPriority), - dynamicRange = DynamicRange.fromProto(it.dynamicRangeStatus), - imageFormat = ImageOutputFormat.fromProto(it.imageFormatStatus), - maxVideoDurationMillis = it.maxVideoDurationMillis, - videoQuality = VideoQuality.fromProto(it.videoQuality), - audioEnabled = it.audioEnabledStatus, - concurrentCameraMode = when (it.concurrentCameraModeStatus) { - ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_OFF -> - ConcurrentCameraMode.OFF - - ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL -> - ConcurrentCameraMode.DUAL - - else -> ConcurrentCameraMode.OFF - }, - captureMode = defaultCaptureModeOverride - ) - } + private val settingsDataSource: SettingsDataSource +) : SettingsRepository { + + override val defaultCameraAppSettings: Flow = + settingsDataSource.defaultCameraAppSettings override suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings = - defaultCameraAppSettings.first() + settingsDataSource.getCurrentDefaultCameraAppSettings() override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setDefaultLensFacing(lensFacing.toProto()) - .build() - } + settingsDataSource.updateDefaultLensFacing(lensFacing) } override suspend fun updateDarkModeStatus(darkMode: DarkMode) { - val newStatus = when (darkMode) { - DarkMode.DARK -> DarkModeProto.DARK_MODE_DARK - DarkMode.LIGHT -> DarkModeProto.DARK_MODE_LIGHT - DarkMode.SYSTEM -> DarkModeProto.DARK_MODE_SYSTEM - } - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setDarkModeStatus(newStatus) - .build() - } + settingsDataSource.updateDarkModeStatus(darkMode) } override suspend fun updateFlashModeStatus(flashMode: FlashMode) { - val newStatus = when (flashMode) { - FlashMode.AUTO -> FlashModeProto.FLASH_MODE_AUTO - FlashMode.ON -> FlashModeProto.FLASH_MODE_ON - FlashMode.OFF -> FlashModeProto.FLASH_MODE_OFF - FlashMode.LOW_LIGHT_BOOST -> FlashModeProto.FLASH_MODE_LOW_LIGHT_BOOST - } - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setFlashModeStatus(newStatus) - .build() - } - } - - override suspend fun updateTargetFrameRate(targetFrameRate: Int) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setTargetFrameRate(targetFrameRate) - .build() - } + settingsDataSource.updateFlashModeStatus(flashMode) } override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { - val newStatus = when (aspectRatio) { - AspectRatio.NINE_SIXTEEN -> AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN - AspectRatio.THREE_FOUR -> AspectRatioProto.ASPECT_RATIO_THREE_FOUR - AspectRatio.ONE_ONE -> AspectRatioProto.ASPECT_RATIO_ONE_ONE - } - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setAspectRatioStatus(newStatus) - .build() - } + settingsDataSource.updateAspectRatio(aspectRatio) } override suspend fun updateStreamConfig(streamConfig: StreamConfig) { - val newStatus = when (streamConfig) { - StreamConfig.MULTI_STREAM -> StreamConfigProto.STREAM_CONFIG_MULTI_STREAM - StreamConfig.SINGLE_STREAM -> StreamConfigProto.STREAM_CONFIG_SINGLE_STREAM - } - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setStreamConfigStatus(newStatus) - .build() - } + settingsDataSource.updateStreamConfig(streamConfig) + } + + override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { + settingsDataSource.updateLowLightBoostPriority(lowLightBoostPriority) } override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) { - val newStatus = when (stabilizationMode) { - StabilizationMode.OFF -> StabilizationModeProto.STABILIZATION_MODE_OFF - StabilizationMode.AUTO -> StabilizationModeProto.STABILIZATION_MODE_AUTO - StabilizationMode.ON -> StabilizationModeProto.STABILIZATION_MODE_ON - StabilizationMode.HIGH_QUALITY -> StabilizationModeProto.STABILIZATION_MODE_HIGH_QUALITY - StabilizationMode.OPTICAL -> StabilizationModeProto.STABILIZATION_MODE_OPTICAL - } - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setStabilizationMode(newStatus) - .build() - } + settingsDataSource.updateStabilizationMode(stabilizationMode) } override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setDynamicRangeStatus(dynamicRange.toProto()) - .build() - } + settingsDataSource.updateDynamicRange(dynamicRange) + } + + override suspend fun updateTargetFrameRate(targetFrameRate: Int) { + settingsDataSource.updateTargetFrameRate(targetFrameRate) } override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setImageFormatStatus(imageFormat.toProto()) - .build() - } + settingsDataSource.updateImageFormat(imageFormat) } override suspend fun updateMaxVideoDuration(durationMillis: Long) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setMaxVideoDurationMillis(durationMillis) - .build() - } + settingsDataSource.updateMaxVideoDuration(durationMillis) } override suspend fun updateVideoQuality(videoQuality: VideoQuality) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setVideoQuality(videoQuality.toProto()) - .build() - } - } - - override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setLowLightBoostPriority(lowLightBoostPriority.toProto()) - .build() - } + settingsDataSource.updateVideoQuality(videoQuality) } override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setAudioEnabledStatus(isAudioEnabled) - .build() - } + settingsDataSource.updateAudioEnabled(isAudioEnabled) } override suspend fun updateConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) { - val newStatus = when (concurrentCameraMode) { - ConcurrentCameraMode.OFF -> ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_OFF - ConcurrentCameraMode.DUAL -> ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL - } - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setConcurrentCameraModeStatus(newStatus) - .build() - } + settingsDataSource.updateConcurrentCameraMode(concurrentCameraMode) } } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepositoryModule.kt similarity index 84% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt rename to data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepositoryModule.kt index 3c9fb71ec..9648e22a7 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepositoryModule.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. @@ -20,15 +20,12 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -/** - * Dagger [Module] for settings data layer. - */ @Module @InstallIn(SingletonComponent::class) -interface SettingsModule { +abstract class SettingsRepositoryModule { @Binds - fun bindsSettingsRepository( + abstract fun bindSettingsRepository( localSettingsRepository: LocalSettingsRepository ): SettingsRepository } diff --git a/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto b/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto deleted file mode 100644 index 1a621f931..000000000 --- a/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -syntax = "proto3"; - -import "com/google/jetpackcamera/model/proto/aspect_ratio.proto"; -import "com/google/jetpackcamera/model/proto/stream_config.proto"; -import "com/google/jetpackcamera/model/proto/dark_mode.proto"; -import "com/google/jetpackcamera/model/proto/dynamic_range.proto"; -import "com/google/jetpackcamera/model/proto/flash_mode.proto"; -import "com/google/jetpackcamera/model/proto/image_output_format.proto"; -import "com/google/jetpackcamera/model/proto/lens_facing.proto"; -import "com/google/jetpackcamera/model/proto/stabilization_mode.proto"; -import "com/google/jetpackcamera/model/proto/video_quality.proto"; -import "com/google/jetpackcamera/model/proto/low_light_boost_priority.proto"; -import "com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto"; - - -option java_package = "com.google.jetpackcamera.settings"; -option java_multiple_files = true; - -message JcaSettings { - // Camera settings - LensFacing default_lens_facing = 1; - FlashMode flash_mode_status = 2; - int32 target_frame_rate = 3; - AspectRatio aspect_ratio_status = 4; - StreamConfig stream_config_status = 5; - StabilizationMode stabilization_mode = 6; - DynamicRange dynamic_range_status = 8; - ImageOutputFormat image_format_status = 10; - uint64 max_video_duration_millis = 11; - VideoQuality video_quality = 12; - bool audio_enabled_status = 13; - LowLightBoostPriority low_light_boost_priority = 14; - ConcurrentCameraMode concurrent_camera_mode_status = 15; - - // Non-camera app settings - DarkMode dark_mode_status = 9; -} 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/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index 0eb033d6e..14e7b1e30 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -50,9 +50,8 @@ android { dependencies { implementation(project(":data:settings")) + implementation(project(":core: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/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt deleted file mode 100644 index 7c07edfe6..000000000 --- a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.testing - -import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import com.google.jetpackcamera.settings.JcaSettings -import java.io.File -import kotlinx.coroutines.CoroutineScope - -/** test implementation of DataStoreModule */ -object FakeDataStoreModule { - - fun provideDataStore( - scope: CoroutineScope, - serializer: FakeJcaSettingsSerializer, - file: File - ): DataStore = DataStoreFactory.create( - corruptionHandler = ReplaceFileCorruptionHandler { JcaSettings.getDefaultInstance() }, - scope = scope, - serializer = serializer, - produceFile = { file } - ) -} diff --git a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt deleted file mode 100644 index 5b319f9d4..000000000 --- a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES 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.testing - -import androidx.datastore.core.CorruptionException -import androidx.datastore.core.Serializer -import com.google.jetpackcamera.model.UNLIMITED_VIDEO_DURATION -import com.google.jetpackcamera.model.proto.AspectRatio -import com.google.jetpackcamera.model.proto.DarkMode -import com.google.jetpackcamera.model.proto.DynamicRange -import com.google.jetpackcamera.model.proto.FlashMode -import com.google.jetpackcamera.model.proto.ImageOutputFormat -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.IOException -import java.io.InputStream -import java.io.OutputStream - -class FakeJcaSettingsSerializer(var failReadWithCorruptionException: Boolean = false) : - Serializer { - - override val defaultValue: JcaSettings = JcaSettings.newBuilder() - .setDarkModeStatus(DarkMode.DARK_MODE_SYSTEM) - .setDefaultLensFacing(LensFacing.LENS_FACING_BACK) - .setFlashModeStatus(FlashMode.FLASH_MODE_OFF) - .setAspectRatioStatus(AspectRatio.ASPECT_RATIO_NINE_SIXTEEN) - .setStreamConfigStatus(StreamConfig.STREAM_CONFIG_MULTI_STREAM) - .setStabilizationMode(StabilizationMode.STABILIZATION_MODE_AUTO) - .setDynamicRangeStatus(DynamicRange.DYNAMIC_RANGE_SDR) - .setVideoQuality(VideoQuality.VIDEO_QUALITY_UNSPECIFIED) - .setImageFormatStatus(ImageOutputFormat.IMAGE_OUTPUT_FORMAT_JPEG) - .setMaxVideoDurationMillis(UNLIMITED_VIDEO_DURATION) - .build() - - override suspend fun readFrom(input: InputStream): JcaSettings { - if (failReadWithCorruptionException) { - throw CorruptionException( - "Corruption Exception", - IOException() - ) - } - try { - return JcaSettings.parseFrom(input) - } catch (exception: InvalidProtocolBufferException) { - throw CorruptionException("Cannot read proto.", exception) - } - } - - override suspend fun writeTo(t: JcaSettings, output: OutputStream) = t.writeTo(output) -} diff --git a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeSettingsRepository.kt b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeSettingsRepository.kt index 1709feafb..4e9afd2d2 100644 --- a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeSettingsRepository.kt +++ b/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeSettingsRepository.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. @@ -30,78 +30,85 @@ import com.google.jetpackcamera.settings.SettingsRepository import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow - -object FakeSettingsRepository : SettingsRepository { - private var currentCameraSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +class FakeSettingsRepository( + initialSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS +) : SettingsRepository { + private val _defaultCameraAppSettings = MutableStateFlow(initialSettings) override val defaultCameraAppSettings: Flow = - flow { emit(currentCameraSettings) } + _defaultCameraAppSettings.asStateFlow() - override suspend fun getCurrentDefaultCameraAppSettings() = defaultCameraAppSettings.first() + override suspend fun getCurrentDefaultCameraAppSettings() = _defaultCameraAppSettings.value override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { - currentCameraSettings = currentCameraSettings.copy(cameraLensFacing = lensFacing) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(cameraLensFacing = lensFacing) } override suspend fun updateDarkModeStatus(darkMode: DarkMode) { - currentCameraSettings = currentCameraSettings.copy(darkMode = darkMode) + _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(darkMode = darkMode) } override suspend fun updateFlashModeStatus(flashMode: FlashMode) { - currentCameraSettings = currentCameraSettings.copy(flashMode = flashMode) + _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy( + flashMode = flashMode + ) } override suspend fun updateStreamConfig(streamConfig: StreamConfig) { - currentCameraSettings = - currentCameraSettings.copy(streamConfig = streamConfig) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(streamConfig = streamConfig) } override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { - currentCameraSettings = - currentCameraSettings.copy(lowLightBoostPriority = lowLightBoostPriority) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(lowLightBoostPriority = lowLightBoostPriority) } override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) { - currentCameraSettings = - currentCameraSettings.copy(stabilizationMode = stabilizationMode) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(stabilizationMode = stabilizationMode) } override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { - currentCameraSettings = - currentCameraSettings.copy(dynamicRange = dynamicRange) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(dynamicRange = dynamicRange) } override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { - currentCameraSettings = - currentCameraSettings.copy(aspectRatio = aspectRatio) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(aspectRatio = aspectRatio) } override suspend fun updateTargetFrameRate(targetFrameRate: Int) { - currentCameraSettings = - currentCameraSettings.copy(targetFrameRate = targetFrameRate) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(targetFrameRate = targetFrameRate) } override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { - currentCameraSettings = currentCameraSettings.copy(imageFormat = imageFormat) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(imageFormat = imageFormat) } override suspend fun updateMaxVideoDuration(durationMillis: Long) { - currentCameraSettings = currentCameraSettings.copy(maxVideoDurationMillis = durationMillis) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(maxVideoDurationMillis = durationMillis) } override suspend fun updateVideoQuality(videoQuality: VideoQuality) { - currentCameraSettings = currentCameraSettings.copy(videoQuality = videoQuality) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(videoQuality = videoQuality) } override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { - currentCameraSettings = - currentCameraSettings.copy(audioEnabled = isAudioEnabled) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(audioEnabled = isAudioEnabled) } override suspend fun updateConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) { - currentCameraSettings = - currentCameraSettings.copy(concurrentCameraMode = concurrentCameraMode) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(concurrentCameraMode = concurrentCameraMode) } } diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index 8f1ac7d69..152c5e3bc 100644 --- a/feature/preview/build.gradle.kts +++ b/feature/preview/build.gradle.kts @@ -139,6 +139,7 @@ dependencies { implementation(project(":core:common")) implementation(project(":data:media")) implementation(project(":data:settings")) + implementation(project(":core:settings")) implementation(project(":core:model")) testImplementation(project(":core:camera:testing")) testImplementation(project(":data:settings:testing")) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/DebugSettingsNavType.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/DebugSettingsNavType.kt index d2c4c5dfa..e6be7942f 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/DebugSettingsNavType.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/DebugSettingsNavType.kt @@ -18,8 +18,10 @@ package com.google.jetpackcamera.feature.preview.navigation import android.os.Bundle import androidx.navigation.NavType import com.google.jetpackcamera.model.DebugSettings -import com.google.jetpackcamera.model.DebugSettings.Companion.encodeAsByteArray import com.google.jetpackcamera.model.DebugSettings.Companion.encodeAsString +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets /** * Custom NavType to handle DebugSettings data class. @@ -31,27 +33,32 @@ internal object DebugSettingsNavType : NavType(isNullableAllowed * Puts the [DebugSettings] value into the Bundle by converting to Proto and serializing. */ override fun put(bundle: Bundle, key: String, value: DebugSettings) { - bundle.putByteArray(key, value.encodeAsByteArray()) + bundle.putString(key, value.encodeAsString()) } /** * Gets the [DebugSettings] value from the Bundle by deserializing the Proto. */ override fun get(bundle: Bundle, key: String): DebugSettings? { - return bundle.getByteArray(key)?.let { bytes -> - DebugSettings.parseFromByteArray(bytes) + return bundle.getString(key)?.let { str -> + DebugSettings.parseFromString(str) } } /** - * Parses the Base64 encoded Proto string from the navigation route. + * Parses the URL-encoded serialized string from the navigation route. */ - override fun parseValue(value: String): DebugSettings = DebugSettings.parseFromString(value) + override fun parseValue(value: String): DebugSettings { + val decoded = URLDecoder.decode(value, StandardCharsets.UTF_8.toString()) + return DebugSettings.parseFromString(decoded) + } /** - * Encodes the [DebugSettings] data class to a Base64 string for navigation routes. + * Encodes the [DebugSettings] data class to a URL-encoded string for navigation routes. */ - override fun serializeAsValue(value: DebugSettings): String = value.encodeAsString() + override fun serializeAsValue(value: DebugSettings): String { + return URLEncoder.encode(value.encodeAsString(), StandardCharsets.UTF_8.toString()) + } override val name: String = "DebugSettingsNavType" } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/PreviewNavigation.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/PreviewNavigation.kt index 60b91ba52..1021d6e68 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/PreviewNavigation.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/navigation/PreviewNavigation.kt @@ -188,5 +188,5 @@ internal fun SavedStateHandle.getCaptureUris(defaultIfMissing: List = empty internal fun SavedStateHandle.getDebugSettings( defaultIfMissing: DebugSettings = DebugSettings() -): DebugSettings = get(ARG_DEBUG_SETTINGS)?.let(DebugSettings::parseFromByteArray) +): DebugSettings = get(ARG_DEBUG_SETTINGS)?.let(DebugSettings::parseFromString) ?: defaultIfMissing 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 5a0373996..fbb878e5c 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 @@ -64,7 +64,7 @@ class PreviewViewModelTest { previewViewModel = PreviewViewModel( cameraSystemRepository = cameraSystemRepository, constraintsRepository = constraintsRepository, - settingsRepository = FakeSettingsRepository, + settingsRepository = FakeSettingsRepository(), mediaRepository = FakeMediaRepository(), savedStateHandle = SavedStateHandle(), defaultSaveMode = SaveMode.Immediate diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 886edd072..364961cf4 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -111,11 +111,11 @@ dependencies { implementation(libs.dagger.hilt.android) kapt(libs.dagger.hilt.compiler) - // Proto Datastore - implementation(libs.androidx.datastore) - implementation(libs.protobuf.kotlin.lite) - implementation(project(":data:settings")) + implementation(project(":core:settings")) + androidTestImplementation(project(":core:settings:datastore-prefs")) + androidTestImplementation(project(":core:settings:datastore-prefs:testing")) + androidTestImplementation(libs.androidx.datastore.preferences) 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 8909bfaab..35c8adfa5 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 @@ -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. @@ -16,25 +16,23 @@ package com.google.jetpackcamera.settings import android.Manifest -import android.content.Context import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.dataStoreFile -import androidx.test.core.app.ApplicationProvider +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.core.settings.datastoreprefs.PrefsDataStoreSettingsDataSource +import com.google.jetpackcamera.core.settings.datastoreprefs.testing.FakeDataStoreModule import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DarkMode import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StabilizationMode -import com.google.jetpackcamera.model.proto.ConcurrentCameraMode as ConcurrentCameraModeProto -import com.google.jetpackcamera.model.proto.FlashMode as FlashModeProto -import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto -import com.google.jetpackcamera.model.proto.StabilizationMode as StabilizationModeProto -import com.google.jetpackcamera.model.proto.StreamConfig as StreamConfigProto +import com.google.jetpackcamera.model.StreamConfig import com.google.jetpackcamera.settings.model.CameraSystemConstraints import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS import java.io.File @@ -50,7 +48,9 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith private val STABILIZATION_SUPPORTED_CONSTRAINTS = TYPICAL_SYSTEM_CONSTRAINTS.copy( @@ -89,26 +89,32 @@ private val LLB_SUPPORTED_CONSTRAINTS = TYPICAL_SYSTEM_CONSTRAINTS.copy( @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) internal class CameraAppSettingsViewModelTest { - private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - private lateinit var testDataStore: DataStore + @get:Rule + val tempFolder = TemporaryFolder() + + private lateinit var testFile: File + private lateinit var testDataStore: DataStore private lateinit var datastoreScope: CoroutineScope private lateinit var settingsViewModel: SettingsViewModel @Before fun setup() = runTest(StandardTestDispatcher()) { Dispatchers.setMain(StandardTestDispatcher()) + testFile = tempFolder.newFile("test_settings.preferences_pb") datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - testDataStore = DataStoreFactory.create( - serializer = JcaSettingsSerializer, - scope = datastoreScope - ) { - testContext.dataStoreFile("test_jca_settings.pb") - } - val settingsRepository = LocalSettingsRepository( - jcaSettings = testDataStore, + testDataStore = FakeDataStoreModule.providePreferenceDataStore( + scope = datastoreScope, + file = testFile + ) + + val settingsDataSource = PrefsDataStoreSettingsDataSource( + dataStore = testDataStore, defaultCaptureModeOverride = CaptureMode.STANDARD ) + val settingsRepository = LocalSettingsRepository( + settingsDataSource = settingsDataSource + ) val constraintsRepository = SettableConstraintsRepositoryImpl().apply { updateSystemConstraints(TYPICAL_SYSTEM_CONSTRAINTS) } @@ -121,11 +127,6 @@ internal class CameraAppSettingsViewModelTest { @After fun tearDown() { - File( - ApplicationProvider.getApplicationContext().filesDir, - "datastore" - ).deleteRecursively() - datastoreScope.cancel() } @@ -242,10 +243,13 @@ internal class CameraAppSettingsViewModelTest { systemConstraints: CameraSystemConstraints = TYPICAL_SYSTEM_CONSTRAINTS, defaultCaptureMode: CaptureMode = CaptureMode.VIDEO_ONLY ): SettingsViewModel { - val settingsRepository = LocalSettingsRepository( - jcaSettings = testDataStore, + val settingsDataSource = PrefsDataStoreSettingsDataSource( + dataStore = testDataStore, defaultCaptureModeOverride = defaultCaptureMode ) + val settingsRepository = LocalSettingsRepository( + settingsDataSource = settingsDataSource + ) val constraintsRepository = SettableConstraintsRepositoryImpl().apply { updateSystemConstraints(systemConstraints) } @@ -287,10 +291,8 @@ internal class CameraAppSettingsViewModelTest { fun concurrentCamera_whenStreamConfigIsSingleStream_isDisabled() = runTest(StandardTestDispatcher()) { // Set StreamConfig to SINGLE_STREAM first - testDataStore.updateData { - it.toBuilder() - .setStreamConfigStatus(StreamConfigProto.STREAM_CONFIG_SINGLE_STREAM) - .build() + testDataStore.edit { prefs -> + prefs[stringPreferencesKey("stream_config")] = StreamConfig.SINGLE_STREAM.name } val customViewModel = createViewModelWithConstraints( @@ -323,8 +325,8 @@ internal class CameraAppSettingsViewModelTest { @Test fun concurrentCamera_whenFlashLlbIsActive_isDisabled() = runTest(StandardTestDispatcher()) { // Set FlashMode to LOW_LIGHT_BOOST first - testDataStore.updateData { - it.toBuilder().setFlashModeStatus(FlashModeProto.FLASH_MODE_LOW_LIGHT_BOOST).build() + testDataStore.edit { prefs -> + prefs[stringPreferencesKey("flash_mode")] = FlashMode.LOW_LIGHT_BOOST.name } val customViewModel = createViewModelWithConstraints( @@ -354,9 +356,8 @@ internal class CameraAppSettingsViewModelTest { fun concurrentCamera_whenStabilizationIsActive_isDisabled() = runTest(StandardTestDispatcher()) { // Set StabilizationMode to ON first - testDataStore.updateData { - it.toBuilder().setStabilizationMode(StabilizationModeProto.STABILIZATION_MODE_ON) - .build() + testDataStore.edit { prefs -> + prefs[stringPreferencesKey("stabilization_mode")] = StabilizationMode.ON.name } val customViewModel = createViewModelWithConstraints( @@ -388,7 +389,9 @@ internal class CameraAppSettingsViewModelTest { @Test fun concurrentCamera_whenFixedFpsIsActive_isDisabled() = runTest(StandardTestDispatcher()) { // Set targetFrameRate to fixed 30 FPS first - testDataStore.updateData { it.toBuilder().setTargetFrameRate(30).build() } + testDataStore.edit { prefs -> + prefs[intPreferencesKey("target_frame_rate")] = 30 + } val customViewModel = createViewModelWithConstraints( systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS.copy(concurrentCamerasSupported = true) @@ -417,12 +420,12 @@ internal class CameraAppSettingsViewModelTest { fun streamConfig_whenConcurrentCameraIsEnabled_isDisabled() = runTest(StandardTestDispatcher()) { // Set ConcurrentCameraMode to DUAL first - testDataStore.updateData { - it.toBuilder() - .setConcurrentCameraModeStatus( - ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL + testDataStore.edit { prefs -> + prefs[ + stringPreferencesKey( + "concurrent_camera_mode" ) - .build() + ] = ConcurrentCameraMode.DUAL.name } val customViewModel = createViewModelWithConstraints( @@ -451,12 +454,8 @@ internal class CameraAppSettingsViewModelTest { @Test fun flashLlb_whenConcurrentCameraIsEnabled_isDisabled() = runTest(StandardTestDispatcher()) { // Set ConcurrentCameraMode to DUAL first - testDataStore.updateData { - it.toBuilder() - .setConcurrentCameraModeStatus( - ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL - ) - .build() + testDataStore.edit { prefs -> + prefs[stringPreferencesKey("concurrent_camera_mode")] = ConcurrentCameraMode.DUAL.name } val customViewModel = createViewModelWithConstraints( @@ -484,12 +483,12 @@ internal class CameraAppSettingsViewModelTest { fun stabilization_whenConcurrentCameraIsEnabled_isDisabled() = runTest(StandardTestDispatcher()) { // Set ConcurrentCameraMode to DUAL first - testDataStore.updateData { - it.toBuilder() - .setConcurrentCameraModeStatus( - ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL + testDataStore.edit { prefs -> + prefs[ + stringPreferencesKey( + "concurrent_camera_mode" ) - .build() + ] = ConcurrentCameraMode.DUAL.name } val customViewModel = createViewModelWithConstraints( @@ -514,12 +513,8 @@ internal class CameraAppSettingsViewModelTest { @Test fun fps_whenConcurrentCameraIsEnabled_isDisabled() = runTest(StandardTestDispatcher()) { // Set ConcurrentCameraMode to DUAL first - testDataStore.updateData { - it.toBuilder() - .setConcurrentCameraModeStatus( - ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL - ) - .build() + testDataStore.edit { prefs -> + prefs[stringPreferencesKey("concurrent_camera_mode")] = ConcurrentCameraMode.DUAL.name } val customViewModel = createViewModelWithConstraints( @@ -539,12 +534,8 @@ internal class CameraAppSettingsViewModelTest { @Test fun streamConfigDisabled_whenUltraHdrEnabled() = runTest(StandardTestDispatcher()) { // Set image format to Ultra HDR in datastore - testDataStore.updateData { currentSettings -> - currentSettings.toBuilder() - .setImageFormatStatus( - ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR - ) - .build() + testDataStore.edit { prefs -> + prefs[stringPreferencesKey("image_format")] = ImageOutputFormat.JPEG_ULTRA_HDR.name } advanceUntilIdle() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8388c4f43..a9e1c72fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ accompanist = "0.37.3" # See https://developer.android.com/jetpack/androidx/releases/compose-kotlin kotlinPlugin = "2.2.0" androidGradlePlugin = "8.10.1" -protobufPlugin = "0.9.5" + androidxActivityCompose = "1.10.1" androidxAppCompat = "1.7.1" @@ -25,6 +25,7 @@ androidxCamera = "1.5.0-SNAPSHOT" androidxConcurrentFutures = "1.3.0" androidxCoreKtx = "1.16.0" androidxDatastore = "1.1.7" + androidxGraphicsCore = "1.0.3" androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.9.2" @@ -44,7 +45,7 @@ kotlinxAtomicfu = "0.29.0" kotlinxCoroutines = "1.10.2" hilt = "2.57" junit = "4.13.2" -protobuf = "4.31.1" + robolectric = "4.15.1" truth = "1.4.4" testParameterInjector = "1.21" @@ -62,7 +63,8 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidxAnnotation" } androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidxBenchmark" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreKtx" } -androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidxDatastore" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidxDatastore" } + androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" } androidx-graphics-core = { module = "androidx.graphics:graphics-core", version.ref = "androidxGraphicsCore" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestJunit" } @@ -104,7 +106,7 @@ kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-cor play-services-camera-low-light-boost = { module = "com.google.android.gms:play-services-camera-low-light-boost", version.ref = "cameraLowLightBoost" } play-services-tasks = { module = "com.google.android.gms:play-services-tasks", version.ref = "playServicesTasks" } testParameterInjector = { group = "com.google.testparameterinjector", name = "test-parameter-injector", version.ref = "testParameterInjector" } -protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } + androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } @@ -118,6 +120,6 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlinPlugin" } dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -google-protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } + kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinPlugin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlinPlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e35b85ca..103a86882 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,6 +57,7 @@ include(":ui:uistateadapter:capture") include(":ui:components") include(":ui:components:capture") include(":data:model") +include(":core:settings") include(":core:model") include(":ui:uistate:postcapture") include(":ui:uistateadapter:postcapture") @@ -64,6 +65,7 @@ include(":core:camera:postprocess") include(":ui:controller") include(":ui:controller:impl") include(":ui:controller:testing") +include(":core:settings:datastore-prefs") +include(":core:settings:datastore-prefs:testing") include(":ui:debug") include(":ui:debug:testing") - diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index be8c09768..d2f9fc5fa 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -108,6 +108,7 @@ dependencies { testImplementation(project(":core:common")) testImplementation(project(":core:camera:testing")) testImplementation(project(":data:settings")) + testImplementation(project(":core:settings")) } diff --git a/ui/controller/impl/build.gradle.kts b/ui/controller/impl/build.gradle.kts index 7e4a74d36..4a867e8e0 100644 --- a/ui/controller/impl/build.gradle.kts +++ b/ui/controller/impl/build.gradle.kts @@ -87,6 +87,7 @@ dependencies { testImplementation(project(":core:common")) testImplementation(project(":core:camera:testing")) testImplementation(project(":data:settings")) + testImplementation(project(":core:settings")) } // Allow references to generated code diff --git a/ui/debug/build.gradle.kts b/ui/debug/build.gradle.kts index 01b81f8d5..d62d15e16 100644 --- a/ui/debug/build.gradle.kts +++ b/ui/debug/build.gradle.kts @@ -80,6 +80,7 @@ dependencies { implementation(project(":core:camera")) implementation(project(":core:model")) implementation(project(":core:common")) + implementation(project(":core:settings")) implementation(project(":data:settings")) implementation(project(":ui:uistate:capture")) implementation(project(":ui:controller")) diff --git a/ui/uistateadapter/capture/build.gradle.kts b/ui/uistateadapter/capture/build.gradle.kts index c26f63359..592a0a777 100644 --- a/ui/uistateadapter/capture/build.gradle.kts +++ b/ui/uistateadapter/capture/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(libs.compose.material3) implementation(project(":data:settings")) + implementation(project(":core:settings")) implementation(project(":core:model")) implementation(project(":data:media")) implementation(project(":core:camera"))