From 72f4cad64ba1b876e91ea53065068b696ac009a1 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Thu, 28 May 2026 23:32:35 +0000 Subject: [PATCH 01/10] Refactor settings storage to Preferences DataStore to remove Protobuf --- core/model/build.gradle.kts | 24 --- .../google/jetpackcamera/model/AspectRatio.kt | 19 -- .../jetpackcamera/model/DebugSettings.kt | 101 +++++----- .../jetpackcamera/model/DynamicRange.kt | 24 --- .../jetpackcamera/model/ImageOutputFormat.kt | 23 --- .../google/jetpackcamera/model/LensFacing.kt | 23 --- .../model/LowLightBoostPriority.kt | 29 --- .../jetpackcamera/model/StabilizationMode.kt | 20 -- .../google/jetpackcamera/model/TestPattern.kt | 62 ------ .../jetpackcamera/model/VideoQuality.kt | 27 --- .../model/proto/aspect_ratio.proto | 27 --- .../jetpackcamera/model/proto/dark_mode.proto | 26 --- .../model/proto/debug_settings.proto | 29 --- .../model/proto/dynamic_range.proto | 26 --- .../model/proto/flash_mode.proto | 27 --- .../model/proto/image_output_format.proto | 25 --- .../model/proto/lens_facing.proto | 25 --- .../proto/low_light_boost_priority.proto | 25 --- .../model/proto/stabilization_mode.proto | 29 --- .../model/proto/stream_config.proto | 26 --- .../model/proto/test_pattern.proto | 55 ----- .../model/proto/video_quality.proto | 28 --- data/settings/build.gradle.kts | 27 +-- .../settings/DataStoreModuleTest.kt | 10 +- .../jetpackcamera/settings/DataStoreModule.kt | 17 +- .../settings/JcaSettingsSerializer.kt | 58 ------ .../settings/LocalSettingsRepository.kt | 188 ++++++------------ data/settings/testing/build.gradle.kts | 2 +- .../settings/testing/FakeDataStoreModule.kt | 10 +- .../testing/FakeJcaSettingsSerializer.kt | 67 ------- .../navigation/DebugSettingsNavType.kt | 7 +- .../preview/navigation/PreviewNavigation.kt | 2 +- gradle/libs.versions.toml | 1 + 33 files changed, 134 insertions(+), 955 deletions(-) delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/aspect_ratio.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/dark_mode.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/debug_settings.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/dynamic_range.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/flash_mode.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/image_output_format.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/lens_facing.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/low_light_boost_priority.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/stabilization_mode.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/stream_config.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/test_pattern.proto delete mode 100644 core/model/src/main/proto/com/google/jetpackcamera/model/proto/video_quality.proto delete mode 100644 data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.kt delete mode 100644 data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeJcaSettingsSerializer.kt diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index 866fb0cc9..abf84fe8f 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 { @@ -72,35 +71,12 @@ dependencies { // 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..8b96e1304 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,6 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.AspectRatio as AspectRatioProto - enum class AspectRatio(val numerator: Int, val denominator: Int) { THREE_FOUR(3, 4), NINE_SIXTEEN(9, 16), @@ -31,21 +29,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/DebugSettings.kt b/core/model/src/main/java/com/google/jetpackcamera/model/DebugSettings.kt index 25e05698e..e7cca9385 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,11 +15,8 @@ */ 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 +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.TestPattern /** * Data class for defining settings used in debug flows within the app. @@ -38,64 +35,58 @@ 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 = if (kv[1].isEmpty()) null else LensFacing.valueOf(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")) { + val channels = kv[1].removePrefix("SolidColor(").removeSuffix(")").split(",") + TestPattern.SolidColor( + red = channels[0].toUInt(), + greenEven = channels[1].toUInt(), + greenOdd = channels[2].toUInt(), + blue = channels[3].toUInt() + ) + } else { + TestPattern.Off + } + } + } + } + } + } } - testPattern = this@toProto.testPattern.toProto() + return DebugSettings(isDebugModeEnabled, singleLensMode, testPattern) } /** - * Parses the encoded byte array into a [DebugSettings] instance. - */ - fun parseFromByteArray(value: ByteArray): DebugSettings { - val protoValue = DebugSettingsProto.parseFrom(value) - return fromProto(protoValue) - } - - /** - * 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..a585c20f6 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,9 @@ * 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 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 - } - } - } } 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..abeb56f10 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,9 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto - val DEFAULT_HDR_IMAGE_OUTPUT = ImageOutputFormat.JPEG_ULTRA_HDR 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 - } - } - } } 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..b72c807b0 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,6 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.LensFacing as LensFacingProto - enum class LensFacing { BACK, FRONT; @@ -27,25 +25,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..63409ee22 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,7 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.LowLightBoostPriority as LowLightBoostPriorityProto - 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 - } - } - } } 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..bfe1109ee 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,6 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.StabilizationMode as StabilizationModeProto - -/** Enum class representing the device's supported stabilization configurations. */ enum class StabilizationMode { /** Stabilization off */ OFF, @@ -37,21 +34,4 @@ enum class StabilizationMode { /** 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 - } - } } 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..6842cf04a 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,10 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.proto.VideoQuality as VideoQualityProto - 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 - } - } - } } 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/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 443986762..241a1f1fe 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,9 +75,9 @@ dependencies { implementation(libs.dagger.hilt.android) kapt(libs.dagger.hilt.compiler) - // proto datastore + // preferences datastore implementation(libs.androidx.datastore) - implementation(libs.protobuf.kotlin.lite) + implementation(libs.androidx.datastore.preferences) // Testing testImplementation(libs.junit) @@ -94,28 +93,6 @@ dependencies { 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") - } - } - } - } -} - // Allow references to generated code kapt { correctErrorTypes = true diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt index 703783b23..e716e6663 100644 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt +++ b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt @@ -16,10 +16,10 @@ package com.google.jetpackcamera.settings 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.settings.testing.FakeDataStoreModule -import com.google.jetpackcamera.settings.testing.FakeJcaSettingsSerializer import java.io.File import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.advanceUntilIdle @@ -43,16 +43,14 @@ class DataStoreModuleTest { } @Test - fun dataStoreModule_read_can_handle_corrupted_file() = runTest { - // should handle exception and replace file information - val dataStore: DataStore = FakeDataStoreModule.provideDataStore( + fun dataStoreModule_can_provide_datastore() = runTest { + 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()) + assertThat(datastoreValue.asMap()).isEmpty() } } 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 index 7f8862455..e26b328ea 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt @@ -17,9 +17,9 @@ 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 androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,18 +34,15 @@ import kotlinx.coroutines.SupervisorJob @Module @InstallIn(SingletonComponent::class) object DataStoreModule { - private const val FILE_LOCATION = "app_settings.pb" + private const val FILE_LOCATION = "jca_settings" @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 + fun provideDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - serializer = JcaSettingsSerializer, produceFile = { - context.dataStoreFile(FILE_LOCATION) + context.preferencesDataStoreFile(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 f84a3248c..000000000 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/JcaSettingsSerializer.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.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.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) - .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 7034f9eba..7a63eb720 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 @@ -16,29 +16,25 @@ package com.google.jetpackcamera.settings import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride 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.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.UNLIMITED_VIDEO_DURATION 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.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 @@ -48,42 +44,42 @@ import kotlinx.coroutines.flow.map * Implementation of [SettingsRepository] with locally stored settings. */ class LocalSettingsRepository @Inject constructor( - private val jcaSettings: DataStore, + private val jcaSettings: DataStore, @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode -) : - SettingsRepository { +) : SettingsRepository { + + companion object { + private val KEY_LENS_FACING = stringPreferencesKey("lens_facing") + private val KEY_DARK_MODE = stringPreferencesKey("dark_mode") + private val KEY_FLASH_MODE = stringPreferencesKey("flash_mode") + private val KEY_ASPECT_RATIO = stringPreferencesKey("aspect_ratio") + private val KEY_STREAM_CONFIG = stringPreferencesKey("stream_config") + private val KEY_STABILIZATION_MODE = stringPreferencesKey("stabilization_mode") + private val KEY_DYNAMIC_RANGE = stringPreferencesKey("dynamic_range") + private val KEY_VIDEO_QUALITY = stringPreferencesKey("video_quality") + private val KEY_IMAGE_FORMAT = stringPreferencesKey("image_format") + private val KEY_MAX_VIDEO_DURATION = longPreferencesKey("max_video_duration") + private val KEY_AUDIO_ENABLED = booleanPreferencesKey("audio_enabled") + private val KEY_LOW_LIGHT_BOOST_PRIORITY = stringPreferencesKey("low_light_boost_priority") + private val KEY_TARGET_FRAME_RATE = intPreferencesKey("target_frame_rate") + } override val defaultCameraAppSettings = jcaSettings.data - .map { + .map { preferences -> 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, + cameraLensFacing = preferences[KEY_LENS_FACING]?.let { LensFacing.valueOf(it) } ?: LensFacing.BACK, + darkMode = preferences[KEY_DARK_MODE]?.let { DarkMode.valueOf(it) } ?: DarkMode.DARK, + flashMode = preferences[KEY_FLASH_MODE]?.let { FlashMode.valueOf(it) } ?: FlashMode.OFF, + aspectRatio = preferences[KEY_ASPECT_RATIO]?.let { AspectRatio.valueOf(it) } ?: AspectRatio.NINE_SIXTEEN, + stabilizationMode = preferences[KEY_STABILIZATION_MODE]?.let { StabilizationMode.valueOf(it) } ?: StabilizationMode.AUTO, + targetFrameRate = preferences[KEY_TARGET_FRAME_RATE] ?: 0, + streamConfig = preferences[KEY_STREAM_CONFIG]?.let { StreamConfig.valueOf(it) } ?: StreamConfig.MULTI_STREAM, + lowLightBoostPriority = preferences[KEY_LOW_LIGHT_BOOST_PRIORITY]?.let { LowLightBoostPriority.valueOf(it) } ?: LowLightBoostPriority.PRIORITIZE_AE_MODE, + dynamicRange = preferences[KEY_DYNAMIC_RANGE]?.let { DynamicRange.valueOf(it) } ?: DynamicRange.SDR, + imageFormat = preferences[KEY_IMAGE_FORMAT]?.let { ImageOutputFormat.valueOf(it) } ?: ImageOutputFormat.JPEG, + maxVideoDurationMillis = preferences[KEY_MAX_VIDEO_DURATION] ?: UNLIMITED_VIDEO_DURATION, + videoQuality = preferences[KEY_VIDEO_QUALITY]?.let { VideoQuality.valueOf(it) } ?: VideoQuality.UNSPECIFIED, + audioEnabled = preferences[KEY_AUDIO_ENABLED] ?: true, captureMode = defaultCaptureModeOverride ) } @@ -92,132 +88,80 @@ class LocalSettingsRepository @Inject constructor( defaultCameraAppSettings.first() override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setDefaultLensFacing(lensFacing.toProto()) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_LENS_FACING] = lensFacing.name } } 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() + jcaSettings.edit { preferences -> + preferences[KEY_DARK_MODE] = darkMode.name } } 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() + jcaSettings.edit { preferences -> + preferences[KEY_FLASH_MODE] = flashMode.name } } override suspend fun updateTargetFrameRate(targetFrameRate: Int) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setTargetFrameRate(targetFrameRate) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_TARGET_FRAME_RATE] = targetFrameRate } } 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() + jcaSettings.edit { preferences -> + preferences[KEY_ASPECT_RATIO] = aspectRatio.name } } 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() + jcaSettings.edit { preferences -> + preferences[KEY_STREAM_CONFIG] = streamConfig.name } } 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() + jcaSettings.edit { preferences -> + preferences[KEY_STABILIZATION_MODE] = stabilizationMode.name } } override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setDynamicRangeStatus(dynamicRange.toProto()) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_DYNAMIC_RANGE] = dynamicRange.name } } override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setImageFormatStatus(imageFormat.toProto()) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_IMAGE_FORMAT] = imageFormat.name } } + override suspend fun updateMaxVideoDuration(durationMillis: Long) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setMaxVideoDurationMillis(durationMillis) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_MAX_VIDEO_DURATION] = durationMillis } } override suspend fun updateVideoQuality(videoQuality: VideoQuality) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setVideoQuality(videoQuality.toProto()) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_VIDEO_QUALITY] = videoQuality.name } } override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setLowLightBoostPriority(lowLightBoostPriority.toProto()) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_LOW_LIGHT_BOOST_PRIORITY] = lowLightBoostPriority.name } } override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { - jcaSettings.updateData { currentSettings -> - currentSettings.toBuilder() - .setAudioEnabledStatus(isAudioEnabled) - .build() + jcaSettings.edit { preferences -> + preferences[KEY_AUDIO_ENABLED] = isAudioEnabled } } } diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index 0eb033d6e..a1f1cb624 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -54,5 +54,5 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.androidx.datastore) - implementation(libs.protobuf.kotlin.lite) + implementation(libs.androidx.datastore.preferences) } 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 index 7c07edfe6..a75a74623 100644 --- 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 @@ -16,9 +16,8 @@ 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 androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences import java.io.File import kotlinx.coroutines.CoroutineScope @@ -27,12 +26,9 @@ object FakeDataStoreModule { fun provideDataStore( scope: CoroutineScope, - serializer: FakeJcaSettingsSerializer, file: File - ): DataStore = DataStoreFactory.create( - corruptionHandler = ReplaceFileCorruptionHandler { JcaSettings.getDefaultInstance() }, + ): DataStore = PreferenceDataStoreFactory.create( 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/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..18f823ffc 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,7 +18,6 @@ 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 /** @@ -31,15 +30,15 @@ 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) } } 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c4f9b336..1af439742 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -63,6 +63,7 @@ androidx-annotation = { module = "androidx.annotation:annotation", version.ref = 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" } From 4279a6bc755155f4a2c4ee9b9d520789d321395e Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Thu, 28 May 2026 23:54:49 +0000 Subject: [PATCH 02/10] Address code review comments on safe string parsing & apply spotless formatting --- .../jetpackcamera/model/DebugSettings.kt | 46 ++-- .../jetpackcamera/model/DynamicRange.kt | 2 +- .../jetpackcamera/model/ImageOutputFormat.kt | 2 +- .../model/LowLightBoostPriority.kt | 2 +- .../jetpackcamera/model/StabilizationMode.kt | 2 +- .../jetpackcamera/model/VideoQuality.kt | 2 +- data/settings/build.gradle.kts | 4 - .../settings/DataStoreModuleTest.kt | 56 ----- ...LocalSettingsRepositoryInstrumentedTest.kt | 64 +----- .../settings/LocalSettingsRepository.kt | 198 ++++++++---------- ...reModule.kt => SharedPreferencesModule.kt} | 24 +-- data/settings/testing/build.gradle.kts | 2 - .../settings/testing/FakeDataStoreModule.kt | 34 --- 13 files changed, 148 insertions(+), 290 deletions(-) delete mode 100644 data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt rename data/settings/src/main/java/com/google/jetpackcamera/settings/{DataStoreModule.kt => SharedPreferencesModule.kt} (51%) delete mode 100644 data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt 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 e7cca9385..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,9 +15,6 @@ */ package com.google.jetpackcamera.model -import com.google.jetpackcamera.model.LensFacing -import com.google.jetpackcamera.model.TestPattern - /** * Data class for defining settings used in debug flows within the app. * @@ -48,7 +45,8 @@ data class DebugSettings( if (kv.size == 2) { when (kv[0]) { "debug" -> isDebugModeEnabled = kv[1].toBoolean() - "lens" -> singleLensMode = if (kv[1].isEmpty()) null else LensFacing.valueOf(kv[1]) + "lens" -> singleLensMode = enumValues() + .firstOrNull { it.name == kv[1] } "pattern" -> { testPattern = when (kv[1]) { "Off" -> TestPattern.Off @@ -57,14 +55,35 @@ data class DebugSettings( "PN9" -> TestPattern.PN9 "Custom1" -> TestPattern.Custom1 else -> { - if (kv[1].startsWith("SolidColor")) { - val channels = kv[1].removePrefix("SolidColor(").removeSuffix(")").split(",") - TestPattern.SolidColor( - red = channels[0].toUInt(), - greenEven = channels[1].toUInt(), - greenOdd = channels[2].toUInt(), - blue = channels[3].toUInt() - ) + 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 } @@ -83,7 +102,8 @@ data class DebugSettings( fun DebugSettings.encodeAsString(): String { val lensStr = singleLensMode?.name ?: "" val patternStr = when (val pattern = testPattern) { - is TestPattern.SolidColor -> "SolidColor(${pattern.red},${pattern.greenEven},${pattern.greenOdd},${pattern.blue})" + 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 a585c20f6..391d8cb36 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 @@ -18,5 +18,5 @@ val DEFAULT_HDR_DYNAMIC_RANGE = DynamicRange.HLG10 enum class DynamicRange { SDR, - HLG10; + HLG10 } 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 abeb56f10..c19a4826e 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 @@ -19,5 +19,5 @@ val DEFAULT_HDR_IMAGE_OUTPUT = ImageOutputFormat.JPEG_ULTRA_HDR enum class ImageOutputFormat { JPEG, - JPEG_ULTRA_HDR; + JPEG_ULTRA_HDR } 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 63409ee22..38da49b84 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 @@ -17,5 +17,5 @@ package com.google.jetpackcamera.model enum class LowLightBoostPriority { PRIORITIZE_AE_MODE, - PRIORITIZE_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 bfe1109ee..9c0a45422 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 @@ -33,5 +33,5 @@ enum class StabilizationMode { HIGH_QUALITY, /** Optical Stabilization (OIS) */ - OPTICAL; + OPTICAL } 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 6842cf04a..5c52aa61b 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 @@ -20,5 +20,5 @@ enum class VideoQuality { SD, HD, FHD, - UHD; + UHD } diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 241a1f1fe..6ca73d147 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -75,10 +75,6 @@ dependencies { implementation(libs.dagger.hilt.android) kapt(libs.dagger.hilt.compiler) - // preferences datastore - implementation(libs.androidx.datastore) - implementation(libs.androidx.datastore.preferences) - // Testing testImplementation(libs.junit) testImplementation(libs.truth) 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 e716e6663..000000000 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/DataStoreModuleTest.kt +++ /dev/null @@ -1,56 +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.datastore.preferences.core.Preferences -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.settings.testing.FakeDataStoreModule -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_can_provide_datastore() = runTest { - val dataStore: DataStore = FakeDataStoreModule.provideDataStore( - scope = this.backgroundScope, - file = testFile - ) - val datastoreValue = dataStore.data.first() - advanceUntilIdle() - - assertThat(datastoreValue.asMap()).isEmpty() - } -} 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 index fafb38dc3..2c6a16abd 100644 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt +++ b/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt @@ -16,9 +16,7 @@ package com.google.jetpackcamera.settings import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.dataStoreFile +import android.content.SharedPreferences import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -28,74 +26,44 @@ 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 sharedPreferences: SharedPreferences 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") - } + fun setup() { + sharedPreferences = testContext.getSharedPreferences( + "test_jca_settings", + Context.MODE_PRIVATE + ) + sharedPreferences.edit().clear().commit() repository = LocalSettingsRepository( - jcaSettings = testDataStore, + sharedPreferences = sharedPreferences, defaultCaptureModeOverride = CaptureMode.STANDARD ) - advanceUntilIdle() } @After fun tearDown() { - File( - ApplicationProvider.getApplicationContext().filesDir, - "datastore" - ).deleteRecursively() - - datastoreScope.cancel() + sharedPreferences.edit().clear().commit() } @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 :) - + fun repository_can_fetch_initial_settings() = runTest { val cameraAppSettings: CameraAppSettings = repository.getCurrentDefaultCameraAppSettings() - advanceUntilIdle() assertThat(cameraAppSettings).isEqualTo(DEFAULT_CAMERA_APP_SETTINGS) } @@ -114,11 +82,9 @@ class LocalSettingsRepositoryInstrumentedTest { @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() @@ -128,10 +94,8 @@ class LocalSettingsRepositoryInstrumentedTest { @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() @@ -142,13 +106,10 @@ class LocalSettingsRepositoryInstrumentedTest { @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) } @@ -156,13 +117,10 @@ class LocalSettingsRepositoryInstrumentedTest { @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/LocalSettingsRepository.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt index 7a63eb720..cfdc7221c 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 @@ -15,13 +15,7 @@ */ package com.google.jetpackcamera.settings -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey +import android.content.SharedPreferences import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CaptureMode @@ -37,131 +31,125 @@ import com.google.jetpackcamera.model.UNLIMITED_VIDEO_DURATION import com.google.jetpackcamera.model.VideoQuality import com.google.jetpackcamera.settings.model.CameraAppSettings import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map /** * Implementation of [SettingsRepository] with locally stored settings. */ class LocalSettingsRepository @Inject constructor( - private val jcaSettings: DataStore, + private val sharedPreferences: SharedPreferences, @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode ) : SettingsRepository { - companion object { - private val KEY_LENS_FACING = stringPreferencesKey("lens_facing") - private val KEY_DARK_MODE = stringPreferencesKey("dark_mode") - private val KEY_FLASH_MODE = stringPreferencesKey("flash_mode") - private val KEY_ASPECT_RATIO = stringPreferencesKey("aspect_ratio") - private val KEY_STREAM_CONFIG = stringPreferencesKey("stream_config") - private val KEY_STABILIZATION_MODE = stringPreferencesKey("stabilization_mode") - private val KEY_DYNAMIC_RANGE = stringPreferencesKey("dynamic_range") - private val KEY_VIDEO_QUALITY = stringPreferencesKey("video_quality") - private val KEY_IMAGE_FORMAT = stringPreferencesKey("image_format") - private val KEY_MAX_VIDEO_DURATION = longPreferencesKey("max_video_duration") - private val KEY_AUDIO_ENABLED = booleanPreferencesKey("audio_enabled") - private val KEY_LOW_LIGHT_BOOST_PRIORITY = stringPreferencesKey("low_light_boost_priority") - private val KEY_TARGET_FRAME_RATE = intPreferencesKey("target_frame_rate") + private val _defaultCameraAppSettings = MutableStateFlow(readSettings()) + override val defaultCameraAppSettings: Flow = + _defaultCameraAppSettings.asStateFlow() + + private fun readSettings(): CameraAppSettings { + return CameraAppSettings( + cameraLensFacing = sharedPreferences.getString(KEY_LENS_FACING, null) + .toEnumOrDefault(LensFacing.BACK), + darkMode = sharedPreferences.getString(KEY_DARK_MODE, null) + .toEnumOrDefault(DarkMode.DARK), + flashMode = sharedPreferences.getString(KEY_FLASH_MODE, null) + .toEnumOrDefault(FlashMode.OFF), + aspectRatio = sharedPreferences.getString(KEY_ASPECT_RATIO, null) + .toEnumOrDefault(AspectRatio.NINE_SIXTEEN), + stabilizationMode = sharedPreferences.getString(KEY_STABILIZATION_MODE, null) + .toEnumOrDefault(StabilizationMode.AUTO), + targetFrameRate = if (sharedPreferences.contains(KEY_TARGET_FRAME_RATE)) { + sharedPreferences.getInt(KEY_TARGET_FRAME_RATE, 0) + } else { + 0 + }, + streamConfig = sharedPreferences.getString(KEY_STREAM_CONFIG, null) + .toEnumOrDefault(StreamConfig.MULTI_STREAM), + lowLightBoostPriority = sharedPreferences.getString(KEY_LOW_LIGHT_BOOST_PRIORITY, null) + .toEnumOrDefault(LowLightBoostPriority.PRIORITIZE_AE_MODE), + dynamicRange = sharedPreferences.getString(KEY_DYNAMIC_RANGE, null) + .toEnumOrDefault(DynamicRange.SDR), + imageFormat = sharedPreferences.getString(KEY_IMAGE_FORMAT, null) + .toEnumOrDefault(ImageOutputFormat.JPEG), + maxVideoDurationMillis = if (sharedPreferences.contains(KEY_MAX_VIDEO_DURATION)) { + sharedPreferences.getLong(KEY_MAX_VIDEO_DURATION, UNLIMITED_VIDEO_DURATION) + } else { + UNLIMITED_VIDEO_DURATION + }, + videoQuality = sharedPreferences.getString(KEY_VIDEO_QUALITY, null) + .toEnumOrDefault(VideoQuality.UNSPECIFIED), + audioEnabled = sharedPreferences.getBoolean(KEY_AUDIO_ENABLED, true), + captureMode = defaultCaptureModeOverride + ) } - override val defaultCameraAppSettings = jcaSettings.data - .map { preferences -> - CameraAppSettings( - cameraLensFacing = preferences[KEY_LENS_FACING]?.let { LensFacing.valueOf(it) } ?: LensFacing.BACK, - darkMode = preferences[KEY_DARK_MODE]?.let { DarkMode.valueOf(it) } ?: DarkMode.DARK, - flashMode = preferences[KEY_FLASH_MODE]?.let { FlashMode.valueOf(it) } ?: FlashMode.OFF, - aspectRatio = preferences[KEY_ASPECT_RATIO]?.let { AspectRatio.valueOf(it) } ?: AspectRatio.NINE_SIXTEEN, - stabilizationMode = preferences[KEY_STABILIZATION_MODE]?.let { StabilizationMode.valueOf(it) } ?: StabilizationMode.AUTO, - targetFrameRate = preferences[KEY_TARGET_FRAME_RATE] ?: 0, - streamConfig = preferences[KEY_STREAM_CONFIG]?.let { StreamConfig.valueOf(it) } ?: StreamConfig.MULTI_STREAM, - lowLightBoostPriority = preferences[KEY_LOW_LIGHT_BOOST_PRIORITY]?.let { LowLightBoostPriority.valueOf(it) } ?: LowLightBoostPriority.PRIORITIZE_AE_MODE, - dynamicRange = preferences[KEY_DYNAMIC_RANGE]?.let { DynamicRange.valueOf(it) } ?: DynamicRange.SDR, - imageFormat = preferences[KEY_IMAGE_FORMAT]?.let { ImageOutputFormat.valueOf(it) } ?: ImageOutputFormat.JPEG, - maxVideoDurationMillis = preferences[KEY_MAX_VIDEO_DURATION] ?: UNLIMITED_VIDEO_DURATION, - videoQuality = preferences[KEY_VIDEO_QUALITY]?.let { VideoQuality.valueOf(it) } ?: VideoQuality.UNSPECIFIED, - audioEnabled = preferences[KEY_AUDIO_ENABLED] ?: true, - captureMode = defaultCaptureModeOverride - ) - } + private fun updateSetting(update: SharedPreferences.Editor.() -> SharedPreferences.Editor) { + sharedPreferences.edit().update().apply() + _defaultCameraAppSettings.value = readSettings() + } override suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings = defaultCameraAppSettings.first() - override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { - jcaSettings.edit { preferences -> - preferences[KEY_LENS_FACING] = lensFacing.name - } - } + override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) = + updateSetting { putString(KEY_LENS_FACING, lensFacing.name) } - override suspend fun updateDarkModeStatus(darkMode: DarkMode) { - jcaSettings.edit { preferences -> - preferences[KEY_DARK_MODE] = darkMode.name - } - } + override suspend fun updateDarkModeStatus(darkMode: DarkMode) = + updateSetting { putString(KEY_DARK_MODE, darkMode.name) } - override suspend fun updateFlashModeStatus(flashMode: FlashMode) { - jcaSettings.edit { preferences -> - preferences[KEY_FLASH_MODE] = flashMode.name - } - } + override suspend fun updateFlashModeStatus(flashMode: FlashMode) = + updateSetting { putString(KEY_FLASH_MODE, flashMode.name) } - override suspend fun updateTargetFrameRate(targetFrameRate: Int) { - jcaSettings.edit { preferences -> - preferences[KEY_TARGET_FRAME_RATE] = targetFrameRate - } - } + override suspend fun updateTargetFrameRate(targetFrameRate: Int) = + updateSetting { putInt(KEY_TARGET_FRAME_RATE, targetFrameRate) } - override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { - jcaSettings.edit { preferences -> - preferences[KEY_ASPECT_RATIO] = aspectRatio.name - } - } + override suspend fun updateAspectRatio(aspectRatio: AspectRatio) = + updateSetting { putString(KEY_ASPECT_RATIO, aspectRatio.name) } - override suspend fun updateStreamConfig(streamConfig: StreamConfig) { - jcaSettings.edit { preferences -> - preferences[KEY_STREAM_CONFIG] = streamConfig.name - } - } + override suspend fun updateStreamConfig(streamConfig: StreamConfig) = + updateSetting { putString(KEY_STREAM_CONFIG, streamConfig.name) } - override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) { - jcaSettings.edit { preferences -> - preferences[KEY_STABILIZATION_MODE] = stabilizationMode.name - } - } + override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) = + updateSetting { putString(KEY_STABILIZATION_MODE, stabilizationMode.name) } - override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { - jcaSettings.edit { preferences -> - preferences[KEY_DYNAMIC_RANGE] = dynamicRange.name - } - } + override suspend fun updateDynamicRange(dynamicRange: DynamicRange) = + updateSetting { putString(KEY_DYNAMIC_RANGE, dynamicRange.name) } - override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { - jcaSettings.edit { preferences -> - preferences[KEY_IMAGE_FORMAT] = imageFormat.name - } - } + override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) = + updateSetting { putString(KEY_IMAGE_FORMAT, imageFormat.name) } - override suspend fun updateMaxVideoDuration(durationMillis: Long) { - jcaSettings.edit { preferences -> - preferences[KEY_MAX_VIDEO_DURATION] = durationMillis - } - } + override suspend fun updateMaxVideoDuration(durationMillis: Long) = + updateSetting { putLong(KEY_MAX_VIDEO_DURATION, durationMillis) } - override suspend fun updateVideoQuality(videoQuality: VideoQuality) { - jcaSettings.edit { preferences -> - preferences[KEY_VIDEO_QUALITY] = videoQuality.name - } - } + override suspend fun updateVideoQuality(videoQuality: VideoQuality) = + updateSetting { putString(KEY_VIDEO_QUALITY, videoQuality.name) } - override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { - jcaSettings.edit { preferences -> - preferences[KEY_LOW_LIGHT_BOOST_PRIORITY] = lowLightBoostPriority.name - } - } + override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) = + updateSetting { putString(KEY_LOW_LIGHT_BOOST_PRIORITY, lowLightBoostPriority.name) } + + override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) = + updateSetting { putBoolean(KEY_AUDIO_ENABLED, isAudioEnabled) } - override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { - jcaSettings.edit { preferences -> - preferences[KEY_AUDIO_ENABLED] = isAudioEnabled + companion object { + private const val KEY_LENS_FACING = "lens_facing" + private const val KEY_DARK_MODE = "dark_mode" + private const val KEY_FLASH_MODE = "flash_mode" + private const val KEY_ASPECT_RATIO = "aspect_ratio" + private const val KEY_STREAM_CONFIG = "stream_config" + private const val KEY_STABILIZATION_MODE = "stabilization_mode" + private const val KEY_DYNAMIC_RANGE = "dynamic_range" + private const val KEY_VIDEO_QUALITY = "video_quality" + private const val KEY_IMAGE_FORMAT = "image_format" + private const val KEY_MAX_VIDEO_DURATION = "max_video_duration" + private const val KEY_AUDIO_ENABLED = "audio_enabled" + private const val KEY_LOW_LIGHT_BOOST_PRIORITY = "low_light_boost_priority" + private const val KEY_TARGET_FRAME_RATE = "target_frame_rate" + + private inline fun > String?.toEnumOrDefault(default: T): T { + if (this == null) return default + return enumValues().firstOrNull { it.name == this } ?: default } } } diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/SharedPreferencesModule.kt similarity index 51% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt rename to data/settings/src/main/java/com/google/jetpackcamera/settings/SharedPreferencesModule.kt index e26b328ea..4e323e53c 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/DataStoreModule.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/SharedPreferencesModule.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,33 +16,21 @@ package com.google.jetpackcamera.settings 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 android.content.SharedPreferences 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 = "jca_settings" +object SharedPreferencesModule { + private const val SHARED_PREFS_NAME = "jca_settings" @Provides @Singleton - fun provideDataStore(@ApplicationContext context: Context): DataStore = - PreferenceDataStoreFactory.create( - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - produceFile = { - context.preferencesDataStoreFile(FILE_LOCATION) - } - ) + fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) } diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index a1f1cb624..3f938b8d2 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -53,6 +53,4 @@ dependencies { implementation(project(":core:model")) implementation(libs.kotlinx.coroutines.core) - implementation(libs.androidx.datastore) - implementation(libs.androidx.datastore.preferences) } 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 a75a74623..000000000 --- a/data/settings/testing/src/main/java/com/google/jetpackcamera/settings/testing/FakeDataStoreModule.kt +++ /dev/null @@ -1,34 +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.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.core.Preferences -import java.io.File -import kotlinx.coroutines.CoroutineScope - -/** test implementation of DataStoreModule */ -object FakeDataStoreModule { - - fun provideDataStore( - scope: CoroutineScope, - file: File - ): DataStore = PreferenceDataStoreFactory.create( - scope = scope, - produceFile = { file } - ) -} From cdcb36cffd1dd953e7b053c9e89569f9d7dfb66d Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Mon, 1 Jun 2026 18:55:41 +0000 Subject: [PATCH 03/10] Remove unused androidx.datastore dependency from feature/settings and refactor tests to use SharedPreferences --- feature/settings/build.gradle.kts | 4 --- .../CameraAppSettingsViewModelTest.kt | 33 +++++-------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 2e4835edc..7b88cbc88 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -111,10 +111,6 @@ 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:model")) } diff --git a/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt b/feature/settings/src/androidTest/java/com/google/jetpackcamera/settings/CameraAppSettingsViewModelTest.kt index 6d09cf21a..01557f0d3 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 @@ -16,10 +16,7 @@ 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 android.content.SharedPreferences import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat @@ -27,12 +24,8 @@ import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.DarkMode import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS -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.flow.first import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle @@ -47,23 +40,20 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) internal class CameraAppSettingsViewModelTest { private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - private lateinit var testDataStore: DataStore - private lateinit var datastoreScope: CoroutineScope + private lateinit var sharedPreferences: SharedPreferences private lateinit var settingsViewModel: SettingsViewModel @Before fun setup() = runTest(StandardTestDispatcher()) { Dispatchers.setMain(StandardTestDispatcher()) - datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + sharedPreferences = testContext.getSharedPreferences( + "test_jca_settings", + Context.MODE_PRIVATE + ) + sharedPreferences.edit().clear().commit() - testDataStore = DataStoreFactory.create( - serializer = JcaSettingsSerializer, - scope = datastoreScope - ) { - testContext.dataStoreFile("test_jca_settings.pb") - } val settingsRepository = LocalSettingsRepository( - jcaSettings = testDataStore, + sharedPreferences = sharedPreferences, defaultCaptureModeOverride = CaptureMode.STANDARD ) val constraintsRepository = SettableConstraintsRepositoryImpl().apply { @@ -78,12 +68,7 @@ internal class CameraAppSettingsViewModelTest { @After fun tearDown() { - File( - ApplicationProvider.getApplicationContext().filesDir, - "datastore" - ).deleteRecursively() - - datastoreScope.cancel() + sharedPreferences.edit().clear().commit() } @Test From 2e7792ffcf7de6f583fde802f0559d3cfabe5b0b Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Mon, 1 Jun 2026 18:59:50 +0000 Subject: [PATCH 04/10] Clean up unused datastore and protobuf version and plugin declarations from Gradle scripts and Proguard config --- core/model/build.gradle.kts | 3 +-- data/settings/consumer-rules.pro | 1 - gradle/libs.versions.toml | 13 ++++++------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index abf84fe8f..d352566c4 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -69,8 +69,7 @@ android { dependencies { implementation(libs.kotlinx.coroutines.core) - // proto datastore - implementation(libs.androidx.datastore) + // Testing testImplementation(libs.junit) 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1af439742..c2ffbb48f 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" @@ -24,7 +24,7 @@ androidxBenchmark = "1.3.4" 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 +44,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,8 +62,7 @@ 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" } @@ -105,7 +104,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" } @@ -119,6 +118,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" } From beae310b64433a9e1cab284836d36102e03b6227 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Mon, 1 Jun 2026 22:07:06 +0000 Subject: [PATCH 05/10] Remove obsolete protobuf files and ProtoConversionTest from data:settings --- .../jetpackcamera/settings/jca_settings.proto | 51 -------- .../settings/ProtoConversionTest.kt | 110 ------------------ 2 files changed, 161 deletions(-) delete mode 100644 data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto delete mode 100644 data/settings/src/test/java/com/google/jetpackcamera/settings/ProtoConversionTest.kt 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 eec01dd22..000000000 --- a/data/settings/src/main/proto/com/google/jetpackcamera/settings/jca_settings.proto +++ /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. - */ - -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"; - - -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; - - // 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)) - } - } -} From 2b6816aa05b2fcf2026954091c0613329ace79a3 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Fri, 5 Jun 2026 18:59:26 +0000 Subject: [PATCH 06/10] Refactor persistence into standalone :data:settings-datastore module --- app/build.gradle.kts | 1 + data/settings-datastore/.gitignore | 1 + data/settings-datastore/build.gradle.kts | 90 ++++++++++ data/settings-datastore/consumer-rules.pro | 1 + ...LocalSettingsRepositoryInstrumentedTest.kt | 56 ++++--- .../src/main/AndroidManifest.xml | 19 +++ .../settingsdatastore/DataStoreModule.kt} | 23 ++- .../LocalSettingsRepository.kt | 156 ++++++++++++++++++ .../data/settingsdatastore/PreferenceKeys.kt | 37 +++++ .../data/settingsdatastore}/SettingsModule.kt | 12 +- .../testing/build.gradle.kts | 50 ++++++ .../testing/src/main/AndroidManifest.xml | 19 +++ .../testing/FakeDataStoreModule.kt | 33 ++++ .../settings/LocalSettingsRepository.kt | 155 ----------------- feature/settings/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + settings.gradle.kts | 2 + 17 files changed, 465 insertions(+), 193 deletions(-) create mode 100644 data/settings-datastore/.gitignore create mode 100644 data/settings-datastore/build.gradle.kts create mode 100644 data/settings-datastore/consumer-rules.pro rename data/{settings/src/androidTest/java/com/google/jetpackcamera/settings => settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore}/LocalSettingsRepositoryInstrumentedTest.kt (76%) create mode 100644 data/settings-datastore/src/main/AndroidManifest.xml rename data/{settings/src/main/java/com/google/jetpackcamera/settings/SharedPreferencesModule.kt => settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt} (53%) create mode 100644 data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt create mode 100644 data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/PreferenceKeys.kt rename data/{settings/src/main/java/com/google/jetpackcamera/settings => settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore}/SettingsModule.kt (77%) create mode 100644 data/settings-datastore/testing/build.gradle.kts create mode 100644 data/settings-datastore/testing/src/main/AndroidManifest.xml create mode 100644 data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt delete mode 100644 data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f889b10e..0493140ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -151,6 +151,7 @@ dependencies { // Access settings & model data implementation(project(":data:settings")) + implementation(project(":data:settings-datastore")) implementation(project(":core:model")) // Camera Preview diff --git a/data/settings-datastore/.gitignore b/data/settings-datastore/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/data/settings-datastore/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/settings-datastore/build.gradle.kts b/data/settings-datastore/build.gradle.kts new file mode 100644 index 000000000..cb99d2755 --- /dev/null +++ b/data/settings-datastore/build.gradle.kts @@ -0,0 +1,90 @@ +/* + * 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.data.settingsdatastore" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + 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) + + // Hilt + implementation(libs.dagger.hilt.android) + kapt(libs.dagger.hilt.compiler) + + // 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(":data:settings-datastore:testing")) + + // Access Model and Settings Interface + implementation(project(":core:model")) + implementation(project(":core:common")) + implementation(project(":data:settings")) +} + +// Allow references to generated code +kapt { + correctErrorTypes = true +} diff --git a/data/settings-datastore/consumer-rules.pro b/data/settings-datastore/consumer-rules.pro new file mode 100644 index 000000000..a554da829 --- /dev/null +++ b/data/settings-datastore/consumer-rules.pro @@ -0,0 +1 @@ +# Consumer rules for settings-datastore diff --git a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt b/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt similarity index 76% rename from data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt rename to data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt index 2c6a16abd..ca9724bd5 100644 --- a/data/settings/src/androidTest/java/com/google/jetpackcamera/settings/LocalSettingsRepositoryInstrumentedTest.kt +++ b/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,52 +13,70 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore import android.content.Context -import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences 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.model.CameraAppSettings import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS +import com.google.jetpackcamera.settingsdatastore.testing.FakeDataStoreModule +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 LocalSettingsRepositoryInstrumentedTest { - private val testContext: Context = ApplicationProvider.getApplicationContext() - private lateinit var sharedPreferences: SharedPreferences + @get:Rule + val tempFolder = TemporaryFolder() + private lateinit var testFile: File + + private lateinit var testDataStore: DataStore + private lateinit var datastoreScope: CoroutineScope private lateinit var repository: LocalSettingsRepository @Before - fun setup() { - sharedPreferences = testContext.getSharedPreferences( - "test_jca_settings", - Context.MODE_PRIVATE + fun setup() = runTest { + Dispatchers.setMain(StandardTestDispatcher()) + testFile = tempFolder.newFile() + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + testDataStore = FakeDataStoreModule.providePreferenceDataStore( + scope = datastoreScope, + file = testFile ) - sharedPreferences.edit().clear().commit() repository = LocalSettingsRepository( - sharedPreferences = sharedPreferences, + dataStore = testDataStore, defaultCaptureModeOverride = CaptureMode.STANDARD ) + advanceUntilIdle() } @After fun tearDown() { - sharedPreferences.edit().clear().commit() + datastoreScope.cancel() } @Test @@ -68,18 +86,6 @@ class LocalSettingsRepositoryInstrumentedTest { 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 { val initialDefaultLensFacing = diff --git a/data/settings-datastore/src/main/AndroidManifest.xml b/data/settings-datastore/src/main/AndroidManifest.xml new file mode 100644 index 000000000..53b89b19c --- /dev/null +++ b/data/settings-datastore/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SharedPreferencesModule.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt similarity index 53% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/SharedPreferencesModule.kt rename to data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt index 4e323e53c..947aab47a 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/SharedPreferencesModule.kt +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt @@ -13,24 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore import android.content.Context -import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile 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 @Module @InstallIn(SingletonComponent::class) -object SharedPreferencesModule { - private const val SHARED_PREFS_NAME = "jca_settings" +object DataStoreModule { + private const val FILE_LOCATION = "jca_app_settings.preferences_pb" @Provides @Singleton - fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + fun providePreferenceDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + produceFile = { + context.preferencesDataStoreFile(FILE_LOCATION) + } + ) } diff --git a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt new file mode 100644 index 000000000..2b477c77f --- /dev/null +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt @@ -0,0 +1,156 @@ +/* + * 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.data.settingsdatastore + +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.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.SettingsRepository +import com.google.jetpackcamera.settings.model.CameraAppSettings +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +/** + * Implementation of [SettingsRepository] with locally stored Preferences DataStore. + */ +class LocalSettingsRepository @Inject constructor( + private val dataStore: DataStore, + @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode +) : SettingsRepository { + + 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] ?: 0, + 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, + 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 + } + } + + companion object { + private inline fun > String?.toEnumOrDefault(default: T): T { + if (this == null) return default + return enumValues().firstOrNull { it.name == this } ?: default + } + } +} diff --git a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/PreferenceKeys.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/PreferenceKeys.kt new file mode 100644 index 000000000..7405d4c5b --- /dev/null +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/PreferenceKeys.kt @@ -0,0 +1,37 @@ +/* + * 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.data.settingsdatastore + +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") +} diff --git a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt similarity index 77% rename from data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt rename to data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt index 3c9fb71ec..0b326fb99 100644 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsModule.kt +++ b/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2026 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,22 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settings +package com.google.jetpackcamera.data.settingsdatastore +import com.google.jetpackcamera.settings.SettingsRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -/** - * Dagger [Module] for settings data layer. - */ @Module @InstallIn(SingletonComponent::class) -interface SettingsModule { +abstract class SettingsModule { @Binds - fun bindsSettingsRepository( + abstract fun bindSettingsRepository( localSettingsRepository: LocalSettingsRepository ): SettingsRepository } diff --git a/data/settings-datastore/testing/build.gradle.kts b/data/settings-datastore/testing/build.gradle.kts new file mode 100644 index 000000000..e565fb290 --- /dev/null +++ b/data/settings-datastore/testing/build.gradle.kts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.google.jetpackcamera.settingsdatastore.testing" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +dependencies { + implementation(project(":data:settings-datastore")) + implementation(project(":data:settings")) + implementation(project(":core:model")) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.androidx.datastore.preferences) +} diff --git a/data/settings-datastore/testing/src/main/AndroidManifest.xml b/data/settings-datastore/testing/src/main/AndroidManifest.xml new file mode 100644 index 000000000..53b89b19c --- /dev/null +++ b/data/settings-datastore/testing/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt b/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt new file mode 100644 index 000000000..67693904c --- /dev/null +++ b/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt @@ -0,0 +1,33 @@ +/* + * 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.settingsdatastore.testing + +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 + +object FakeDataStoreModule { + + fun providePreferenceDataStore( + scope: CoroutineScope, + file: File + ): DataStore = PreferenceDataStoreFactory.create( + scope = scope, + produceFile = { file } + ) +} 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 deleted file mode 100644 index cfdc7221c..000000000 --- a/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt +++ /dev/null @@ -1,155 +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.SharedPreferences -import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride -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 javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first - -/** - * Implementation of [SettingsRepository] with locally stored settings. - */ -class LocalSettingsRepository @Inject constructor( - private val sharedPreferences: SharedPreferences, - @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode -) : SettingsRepository { - - private val _defaultCameraAppSettings = MutableStateFlow(readSettings()) - override val defaultCameraAppSettings: Flow = - _defaultCameraAppSettings.asStateFlow() - - private fun readSettings(): CameraAppSettings { - return CameraAppSettings( - cameraLensFacing = sharedPreferences.getString(KEY_LENS_FACING, null) - .toEnumOrDefault(LensFacing.BACK), - darkMode = sharedPreferences.getString(KEY_DARK_MODE, null) - .toEnumOrDefault(DarkMode.DARK), - flashMode = sharedPreferences.getString(KEY_FLASH_MODE, null) - .toEnumOrDefault(FlashMode.OFF), - aspectRatio = sharedPreferences.getString(KEY_ASPECT_RATIO, null) - .toEnumOrDefault(AspectRatio.NINE_SIXTEEN), - stabilizationMode = sharedPreferences.getString(KEY_STABILIZATION_MODE, null) - .toEnumOrDefault(StabilizationMode.AUTO), - targetFrameRate = if (sharedPreferences.contains(KEY_TARGET_FRAME_RATE)) { - sharedPreferences.getInt(KEY_TARGET_FRAME_RATE, 0) - } else { - 0 - }, - streamConfig = sharedPreferences.getString(KEY_STREAM_CONFIG, null) - .toEnumOrDefault(StreamConfig.MULTI_STREAM), - lowLightBoostPriority = sharedPreferences.getString(KEY_LOW_LIGHT_BOOST_PRIORITY, null) - .toEnumOrDefault(LowLightBoostPriority.PRIORITIZE_AE_MODE), - dynamicRange = sharedPreferences.getString(KEY_DYNAMIC_RANGE, null) - .toEnumOrDefault(DynamicRange.SDR), - imageFormat = sharedPreferences.getString(KEY_IMAGE_FORMAT, null) - .toEnumOrDefault(ImageOutputFormat.JPEG), - maxVideoDurationMillis = if (sharedPreferences.contains(KEY_MAX_VIDEO_DURATION)) { - sharedPreferences.getLong(KEY_MAX_VIDEO_DURATION, UNLIMITED_VIDEO_DURATION) - } else { - UNLIMITED_VIDEO_DURATION - }, - videoQuality = sharedPreferences.getString(KEY_VIDEO_QUALITY, null) - .toEnumOrDefault(VideoQuality.UNSPECIFIED), - audioEnabled = sharedPreferences.getBoolean(KEY_AUDIO_ENABLED, true), - captureMode = defaultCaptureModeOverride - ) - } - - private fun updateSetting(update: SharedPreferences.Editor.() -> SharedPreferences.Editor) { - sharedPreferences.edit().update().apply() - _defaultCameraAppSettings.value = readSettings() - } - - override suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings = - defaultCameraAppSettings.first() - - override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) = - updateSetting { putString(KEY_LENS_FACING, lensFacing.name) } - - override suspend fun updateDarkModeStatus(darkMode: DarkMode) = - updateSetting { putString(KEY_DARK_MODE, darkMode.name) } - - override suspend fun updateFlashModeStatus(flashMode: FlashMode) = - updateSetting { putString(KEY_FLASH_MODE, flashMode.name) } - - override suspend fun updateTargetFrameRate(targetFrameRate: Int) = - updateSetting { putInt(KEY_TARGET_FRAME_RATE, targetFrameRate) } - - override suspend fun updateAspectRatio(aspectRatio: AspectRatio) = - updateSetting { putString(KEY_ASPECT_RATIO, aspectRatio.name) } - - override suspend fun updateStreamConfig(streamConfig: StreamConfig) = - updateSetting { putString(KEY_STREAM_CONFIG, streamConfig.name) } - - override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) = - updateSetting { putString(KEY_STABILIZATION_MODE, stabilizationMode.name) } - - override suspend fun updateDynamicRange(dynamicRange: DynamicRange) = - updateSetting { putString(KEY_DYNAMIC_RANGE, dynamicRange.name) } - - override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) = - updateSetting { putString(KEY_IMAGE_FORMAT, imageFormat.name) } - - override suspend fun updateMaxVideoDuration(durationMillis: Long) = - updateSetting { putLong(KEY_MAX_VIDEO_DURATION, durationMillis) } - - override suspend fun updateVideoQuality(videoQuality: VideoQuality) = - updateSetting { putString(KEY_VIDEO_QUALITY, videoQuality.name) } - - override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) = - updateSetting { putString(KEY_LOW_LIGHT_BOOST_PRIORITY, lowLightBoostPriority.name) } - - override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) = - updateSetting { putBoolean(KEY_AUDIO_ENABLED, isAudioEnabled) } - - companion object { - private const val KEY_LENS_FACING = "lens_facing" - private const val KEY_DARK_MODE = "dark_mode" - private const val KEY_FLASH_MODE = "flash_mode" - private const val KEY_ASPECT_RATIO = "aspect_ratio" - private const val KEY_STREAM_CONFIG = "stream_config" - private const val KEY_STABILIZATION_MODE = "stabilization_mode" - private const val KEY_DYNAMIC_RANGE = "dynamic_range" - private const val KEY_VIDEO_QUALITY = "video_quality" - private const val KEY_IMAGE_FORMAT = "image_format" - private const val KEY_MAX_VIDEO_DURATION = "max_video_duration" - private const val KEY_AUDIO_ENABLED = "audio_enabled" - private const val KEY_LOW_LIGHT_BOOST_PRIORITY = "low_light_boost_priority" - private const val KEY_TARGET_FRAME_RATE = "target_frame_rate" - - private inline fun > String?.toEnumOrDefault(default: T): T { - if (this == null) return default - return enumValues().firstOrNull { it.name == this } ?: default - } - } -} diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index f24bb9ac2..e64ec7c82 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -112,6 +112,7 @@ dependencies { kapt(libs.dagger.hilt.compiler) implementation(project(":data:settings")) + androidTestImplementation(project(":data:settings-datastore")) implementation(project(":core:model")) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dfa16bde5..a9e1c72fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ androidxBenchmark = "1.3.4" 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" @@ -62,6 +63,7 @@ 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-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" } diff --git a/settings.gradle.kts b/settings.gradle.kts index f04820411..21666d917 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,3 +64,5 @@ include(":core:camera:postprocess") include(":ui:controller") include(":ui:controller:impl") include(":ui:controller:testing") +include(":data:settings-datastore") +include(":data:settings-datastore:testing") From 08094c01951ecef50c3b8c2bac33670919631032 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Fri, 5 Jun 2026 22:50:43 +0000 Subject: [PATCH 07/10] Refactor settings persistence using modular Preferences DataStore (Option B) --- app/build.gradle.kts | 3 +- core/camera/build.gradle.kts | 1 + core/camera/testing/build.gradle.kts | 1 + core/settings/build.gradle.kts | 59 ++++++++++++ core/settings/consumer-rules.pro | 1 + .../settings/datastore-prefs}/.gitignore | 0 .../datastore-prefs}/build.gradle.kts | 5 +- .../datastore-prefs}/consumer-rules.pro | 0 ...LocalSettingsDataSourceInstrumentedTest.kt | 42 ++++---- .../src/main/AndroidManifest.xml | 0 .../datastoreprefs}/DataStoreModule.kt | 2 +- .../datastoreprefs/LocalSettingsDataSource.kt | 10 +- .../datastoreprefs}/PreferenceKeys.kt | 2 +- .../SettingsDataSourceModule.kt | 32 +++++++ .../datastore-prefs}/testing/build.gradle.kts | 5 +- .../testing/src/main/AndroidManifest.xml | 0 .../testing/FakeDataStoreModule.kt | 2 +- core/settings/src/main/AndroidManifest.xml | 19 ++++ .../settings/SettingsDataSource.kt | 65 +++++++++++++ .../settings/model/CameraAppSettings.kt | 0 .../settings/model/Constraints.kt | 0 data/camera/build.gradle.kts | 1 + data/settings/build.gradle.kts | 1 + .../settings/LocalSettingsRepository.kt | 96 +++++++++++++++++++ .../settings/SettingsRepositoryModule.kt} | 5 +- data/settings/testing/build.gradle.kts | 1 + .../testing/FakeSettingsRepository.kt | 51 +++++----- feature/preview/build.gradle.kts | 1 + .../feature/preview/PreviewViewModelTest.kt | 2 +- feature/settings/build.gradle.kts | 3 +- .../CameraAppSettingsViewModelTest.kt | 54 +++++++---- settings.gradle.kts | 5 +- ui/components/capture/build.gradle.kts | 1 + ui/controller/impl/build.gradle.kts | 1 + ui/uistateadapter/capture/build.gradle.kts | 1 + 35 files changed, 381 insertions(+), 91 deletions(-) create mode 100644 core/settings/build.gradle.kts create mode 100644 core/settings/consumer-rules.pro rename {data/settings-datastore => core/settings/datastore-prefs}/.gitignore (100%) rename {data/settings-datastore => core/settings/datastore-prefs}/build.gradle.kts (93%) rename {data/settings-datastore => core/settings/datastore-prefs}/consumer-rules.pro (100%) rename data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt => core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt (75%) rename {data/settings-datastore => core/settings/datastore-prefs}/src/main/AndroidManifest.xml (100%) rename {data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore => core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs}/DataStoreModule.kt (96%) rename data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt => core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt (96%) rename {data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore => core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs}/PreferenceKeys.kt (96%) create mode 100644 core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt rename {data/settings-datastore => core/settings/datastore-prefs}/testing/build.gradle.kts (88%) rename {data/settings-datastore => core/settings/datastore-prefs}/testing/src/main/AndroidManifest.xml (100%) rename {data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore => core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs}/testing/FakeDataStoreModule.kt (93%) create mode 100644 core/settings/src/main/AndroidManifest.xml create mode 100644 core/settings/src/main/java/com/google/jetpackcamera/settings/SettingsDataSource.kt rename {data => core}/settings/src/main/java/com/google/jetpackcamera/settings/model/CameraAppSettings.kt (100%) rename {data => core}/settings/src/main/java/com/google/jetpackcamera/settings/model/Constraints.kt (100%) create mode 100644 data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt rename data/{settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt => settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepositoryModule.kt} (86%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0493140ed..c6b2ac93f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -151,7 +151,8 @@ dependencies { // Access settings & model data implementation(project(":data:settings")) - implementation(project(":data:settings-datastore")) + implementation(project(":core:settings:datastore-prefs")) + implementation(project(":core:settings")) implementation(project(":core:model")) // Camera Preview 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/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/data/settings-datastore/.gitignore b/core/settings/datastore-prefs/.gitignore similarity index 100% rename from data/settings-datastore/.gitignore rename to core/settings/datastore-prefs/.gitignore diff --git a/data/settings-datastore/build.gradle.kts b/core/settings/datastore-prefs/build.gradle.kts similarity index 93% rename from data/settings-datastore/build.gradle.kts rename to core/settings/datastore-prefs/build.gradle.kts index cb99d2755..1242a076f 100644 --- a/data/settings-datastore/build.gradle.kts +++ b/core/settings/datastore-prefs/build.gradle.kts @@ -21,7 +21,7 @@ plugins { } android { - namespace = "com.google.jetpackcamera.data.settingsdatastore" + namespace = "com.google.jetpackcamera.core.settings.datastoreprefs" compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { @@ -76,11 +76,12 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.truth) androidTestImplementation(libs.kotlinx.coroutines.test) - androidTestImplementation(project(":data:settings-datastore:testing")) + androidTestImplementation(project(":core:settings:datastore-prefs:testing")) // Access Model and Settings Interface implementation(project(":core:model")) implementation(project(":core:common")) + implementation(project(":core:settings")) implementation(project(":data:settings")) } diff --git a/data/settings-datastore/consumer-rules.pro b/core/settings/datastore-prefs/consumer-rules.pro similarity index 100% rename from data/settings-datastore/consumer-rules.pro rename to core/settings/datastore-prefs/consumer-rules.pro diff --git a/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt similarity index 75% rename from data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt rename to core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt index ca9724bd5..ba50137ee 100644 --- a/data/settings-datastore/src/androidTest/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepositoryInstrumentedTest.kt +++ b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.data.settingsdatastore +package com.google.jetpackcamera.core.settings.datastoreprefs -import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences -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 @@ -28,7 +26,7 @@ import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS -import com.google.jetpackcamera.settingsdatastore.testing.FakeDataStoreModule +import com.google.jetpackcamera.core.settings.datastoreprefs.testing.FakeDataStoreModule import java.io.File import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -48,26 +46,26 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) -class LocalSettingsRepositoryInstrumentedTest { +class LocalSettingsDataSourceInstrumentedTest { @get:Rule val tempFolder = TemporaryFolder() private lateinit var testFile: File private lateinit var testDataStore: DataStore private lateinit var datastoreScope: CoroutineScope - private lateinit var repository: LocalSettingsRepository + private lateinit var dataSource: LocalSettingsDataSource @Before fun setup() = runTest { Dispatchers.setMain(StandardTestDispatcher()) - testFile = tempFolder.newFile() + testFile = tempFolder.newFile("test_settings.preferences_pb") datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) testDataStore = FakeDataStoreModule.providePreferenceDataStore( scope = datastoreScope, file = testFile ) - repository = LocalSettingsRepository( + dataSource = LocalSettingsDataSource( dataStore = testDataStore, defaultCaptureModeOverride = CaptureMode.STANDARD ) @@ -80,8 +78,8 @@ class LocalSettingsRepositoryInstrumentedTest { } @Test - fun repository_can_fetch_initial_settings() = runTest { - val cameraAppSettings: CameraAppSettings = repository.getCurrentDefaultCameraAppSettings() + fun datasource_can_fetch_initial_settings() = runTest { + val cameraAppSettings: CameraAppSettings = dataSource.getCurrentDefaultCameraAppSettings() advanceUntilIdle() assertThat(cameraAppSettings).isEqualTo(DEFAULT_CAMERA_APP_SETTINGS) } @@ -89,9 +87,9 @@ class LocalSettingsRepositoryInstrumentedTest { @Test fun can_update_default_to_front_camera() = runTest { val initialDefaultLensFacing = - repository.getCurrentDefaultCameraAppSettings().cameraLensFacing - repository.updateDefaultLensFacing(LensFacing.FRONT) - val newDefaultLensFacing = repository.getCurrentDefaultCameraAppSettings().cameraLensFacing + dataSource.getCurrentDefaultCameraAppSettings().cameraLensFacing + dataSource.updateDefaultLensFacing(LensFacing.FRONT) + val newDefaultLensFacing = dataSource.getCurrentDefaultCameraAppSettings().cameraLensFacing advanceUntilIdle() assertThat(initialDefaultLensFacing).isEqualTo(LensFacing.BACK) @@ -100,9 +98,9 @@ class LocalSettingsRepositoryInstrumentedTest { @Test fun can_update_flash_mode() = runTest { - val initialFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode - repository.updateFlashModeStatus(FlashMode.ON) - val newFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode + val initialFlashModeStatus = dataSource.getCurrentDefaultCameraAppSettings().flashMode + dataSource.updateFlashModeStatus(FlashMode.ON) + val newFlashModeStatus = dataSource.getCurrentDefaultCameraAppSettings().flashMode advanceUntilIdle() assertThat(initialFlashModeStatus).isEqualTo(FlashMode.OFF) @@ -111,22 +109,22 @@ class LocalSettingsRepositoryInstrumentedTest { @Test fun can_update_dynamic_range() = runTest { - val initialDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange - repository.updateDynamicRange(dynamicRange = DynamicRange.HLG10) + val initialDynamicRange = dataSource.getCurrentDefaultCameraAppSettings().dynamicRange + dataSource.updateDynamicRange(dynamicRange = DynamicRange.HLG10) advanceUntilIdle() - val newDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange + val newDynamicRange = dataSource.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) + val initialImageFormat = dataSource.getCurrentDefaultCameraAppSettings().imageFormat + dataSource.updateImageFormat(imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR) advanceUntilIdle() - val newImageFormat = repository.getCurrentDefaultCameraAppSettings().imageFormat + val newImageFormat = dataSource.getCurrentDefaultCameraAppSettings().imageFormat assertThat(initialImageFormat).isEqualTo(ImageOutputFormat.JPEG) assertThat(newImageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) } diff --git a/data/settings-datastore/src/main/AndroidManifest.xml b/core/settings/datastore-prefs/src/main/AndroidManifest.xml similarity index 100% rename from data/settings-datastore/src/main/AndroidManifest.xml rename to core/settings/datastore-prefs/src/main/AndroidManifest.xml diff --git a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/DataStoreModule.kt similarity index 96% rename from data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt rename to core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/DataStoreModule.kt index 947aab47a..187043a40 100644 --- a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/DataStoreModule.kt +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/DataStoreModule.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.data.settingsdatastore +package com.google.jetpackcamera.core.settings.datastoreprefs import android.content.Context import androidx.datastore.core.DataStore diff --git a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt similarity index 96% rename from data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt rename to core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt index 2b477c77f..0bc30810d 100644 --- a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/LocalSettingsRepository.kt +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.data.settingsdatastore +package com.google.jetpackcamera.core.settings.datastoreprefs import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences @@ -31,7 +31,7 @@ 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.SettingsRepository +import com.google.jetpackcamera.settings.SettingsDataSource import com.google.jetpackcamera.settings.model.CameraAppSettings import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -39,12 +39,12 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map /** - * Implementation of [SettingsRepository] with locally stored Preferences DataStore. + * Implementation of [SettingsDataSource] with locally stored Preferences DataStore. */ -class LocalSettingsRepository @Inject constructor( +class LocalSettingsDataSource @Inject constructor( private val dataStore: DataStore, @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode -) : SettingsRepository { +) : SettingsDataSource { override val defaultCameraAppSettings: Flow = dataStore.data.map { prefs -> CameraAppSettings( diff --git a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/PreferenceKeys.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PreferenceKeys.kt similarity index 96% rename from data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/PreferenceKeys.kt rename to core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PreferenceKeys.kt index 7405d4c5b..4b9d9f6ae 100644 --- a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/PreferenceKeys.kt +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PreferenceKeys.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.data.settingsdatastore +package com.google.jetpackcamera.core.settings.datastoreprefs import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey diff --git a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt new file mode 100644 index 000000000..2fc1e937c --- /dev/null +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt @@ -0,0 +1,32 @@ +/* + * 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 com.google.jetpackcamera.settings.SettingsDataSource +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class SettingsDataSourceModule { + + @Binds + abstract fun bindSettingsDataSource( + localSettingsDataSource: LocalSettingsDataSource + ): SettingsDataSource +} diff --git a/data/settings-datastore/testing/build.gradle.kts b/core/settings/datastore-prefs/testing/build.gradle.kts similarity index 88% rename from data/settings-datastore/testing/build.gradle.kts rename to core/settings/datastore-prefs/testing/build.gradle.kts index e565fb290..0e619ddbd 100644 --- a/data/settings-datastore/testing/build.gradle.kts +++ b/core/settings/datastore-prefs/testing/build.gradle.kts @@ -20,7 +20,7 @@ plugins { } android { - namespace = "com.google.jetpackcamera.settingsdatastore.testing" + namespace = "com.google.jetpackcamera.core.settings.datastoreprefs.testing" compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { @@ -41,8 +41,9 @@ android { } dependencies { - implementation(project(":data:settings-datastore")) + implementation(project(":core:settings:datastore-prefs")) implementation(project(":data:settings")) + implementation(project(":core:settings")) implementation(project(":core:model")) implementation(libs.kotlinx.coroutines.core) diff --git a/data/settings-datastore/testing/src/main/AndroidManifest.xml b/core/settings/datastore-prefs/testing/src/main/AndroidManifest.xml similarity index 100% rename from data/settings-datastore/testing/src/main/AndroidManifest.xml rename to core/settings/datastore-prefs/testing/src/main/AndroidManifest.xml diff --git a/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt b/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt similarity index 93% rename from data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt rename to core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt index 67693904c..601986856 100644 --- a/data/settings-datastore/testing/src/main/java/com/google/jetpackcamera/settingsdatastore/testing/FakeDataStoreModule.kt +++ b/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.settingsdatastore.testing +package com.google.jetpackcamera.core.settings.datastoreprefs.testing import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory 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..4ddace37b --- /dev/null +++ b/core/settings/src/main/java/com/google/jetpackcamera/settings/SettingsDataSource.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.settings + +import com.google.jetpackcamera.model.AspectRatio +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 for settings. + */ +interface SettingsDataSource { + + val defaultCameraAppSettings: Flow + + suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings + + suspend fun updateDefaultLensFacing(lensFacing: LensFacing) + + suspend fun updateDarkModeStatus(darkMode: DarkMode) + + suspend fun updateFlashModeStatus(flashMode: FlashMode) + + suspend fun updateAspectRatio(aspectRatio: AspectRatio) + + suspend fun updateStreamConfig(streamConfig: StreamConfig) + + suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) + + suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) + + suspend fun updateDynamicRange(dynamicRange: DynamicRange) + + suspend fun updateTargetFrameRate(targetFrameRate: Int) + + suspend fun updateImageFormat(imageFormat: ImageOutputFormat) + + suspend fun updateMaxVideoDuration(durationMillis: Long) + + suspend fun updateVideoQuality(videoQuality: VideoQuality) + + suspend fun updateAudioEnabled(isAudioEnabled: Boolean) +} 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 6ca73d147..7748c8d51 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -87,6 +87,7 @@ dependencies { // Access Model data implementation(project(":core:model")) implementation(project(":core:common")) + implementation(project(":core:settings")) } // Allow references to generated code 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 new file mode 100644 index 000000000..54aa14bbc --- /dev/null +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/LocalSettingsRepository.kt @@ -0,0 +1,96 @@ +/* + * 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.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 javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** + * Implementation of [SettingsRepository] delegating to [SettingsDataSource]. + */ +class LocalSettingsRepository @Inject constructor( + private val settingsDataSource: SettingsDataSource +) : SettingsRepository { + + override val defaultCameraAppSettings: Flow = + settingsDataSource.defaultCameraAppSettings + + override suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings = + settingsDataSource.getCurrentDefaultCameraAppSettings() + + override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { + settingsDataSource.updateDefaultLensFacing(lensFacing) + } + + override suspend fun updateDarkModeStatus(darkMode: DarkMode) { + settingsDataSource.updateDarkModeStatus(darkMode) + } + + override suspend fun updateFlashModeStatus(flashMode: FlashMode) { + settingsDataSource.updateFlashModeStatus(flashMode) + } + + override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { + settingsDataSource.updateAspectRatio(aspectRatio) + } + + override suspend fun updateStreamConfig(streamConfig: StreamConfig) { + settingsDataSource.updateStreamConfig(streamConfig) + } + + override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { + settingsDataSource.updateLowLightBoostPriority(lowLightBoostPriority) + } + + override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) { + settingsDataSource.updateStabilizationMode(stabilizationMode) + } + + override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { + settingsDataSource.updateDynamicRange(dynamicRange) + } + + override suspend fun updateTargetFrameRate(targetFrameRate: Int) { + settingsDataSource.updateTargetFrameRate(targetFrameRate) + } + + override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { + settingsDataSource.updateImageFormat(imageFormat) + } + + override suspend fun updateMaxVideoDuration(durationMillis: Long) { + settingsDataSource.updateMaxVideoDuration(durationMillis) + } + + override suspend fun updateVideoQuality(videoQuality: VideoQuality) { + settingsDataSource.updateVideoQuality(videoQuality) + } + + override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { + settingsDataSource.updateAudioEnabled(isAudioEnabled) + } +} diff --git a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepositoryModule.kt similarity index 86% rename from data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt rename to data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepositoryModule.kt index 0b326fb99..9648e22a7 100644 --- a/data/settings-datastore/src/main/java/com/google/jetpackcamera/data/settingsdatastore/SettingsModule.kt +++ b/data/settings/src/main/java/com/google/jetpackcamera/settings/SettingsRepositoryModule.kt @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.data.settingsdatastore +package com.google.jetpackcamera.settings -import com.google.jetpackcamera.settings.SettingsRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,7 +22,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -abstract class SettingsModule { +abstract class SettingsRepositoryModule { @Binds abstract fun bindSettingsRepository( diff --git a/data/settings/testing/build.gradle.kts b/data/settings/testing/build.gradle.kts index 3f938b8d2..14e7b1e30 100644 --- a/data/settings/testing/build.gradle.kts +++ b/data/settings/testing/build.gradle.kts @@ -50,6 +50,7 @@ android { dependencies { implementation(project(":data:settings")) + implementation(project(":core:settings")) implementation(project(":core:model")) implementation(libs.kotlinx.coroutines.core) 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 b2fcb8b9d..b49da9cf8 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. @@ -29,73 +29,66 @@ 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 +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow -object FakeSettingsRepository : SettingsRepository { - private var currentCameraSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS +class FakeSettingsRepository( + initialSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS +) : SettingsRepository { + private val _defaultCameraAppSettings = MutableStateFlow(initialSettings) + override val defaultCameraAppSettings: Flow = _defaultCameraAppSettings.asStateFlow() - override val defaultCameraAppSettings: Flow = - flow { emit(currentCameraSettings) } - - 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) } } diff --git a/feature/preview/build.gradle.kts b/feature/preview/build.gradle.kts index badeea6bb..78c251fcf 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/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 e64ec7c82..736bfa6bc 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -112,7 +112,8 @@ dependencies { kapt(libs.dagger.hilt.compiler) implementation(project(":data:settings")) - androidTestImplementation(project(":data:settings-datastore")) + implementation(project(":core:settings")) + androidTestImplementation(project(":core:settings:datastore-prefs")) 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 94fcec9b7..95823b256 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. @@ -15,18 +15,25 @@ */ package com.google.jetpackcamera.settings -import android.content.Context -import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +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.LocalSettingsDataSource +import com.google.jetpackcamera.core.settings.datastoreprefs.testing.FakeDataStoreModule import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.DarkMode +import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.LensFacing -import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS +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.flow.first import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle @@ -34,29 +41,40 @@ 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) internal class CameraAppSettingsViewModelTest { - private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - private lateinit var sharedPreferences: SharedPreferences + @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()) - sharedPreferences = testContext.getSharedPreferences( - "test_jca_settings", - Context.MODE_PRIVATE + testFile = tempFolder.newFile("test_settings.preferences_pb") + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + testDataStore = FakeDataStoreModule.providePreferenceDataStore( + scope = datastoreScope, + file = testFile ) - sharedPreferences.edit().clear().commit() - val settingsRepository = LocalSettingsRepository( - sharedPreferences = sharedPreferences, + val settingsDataSource = LocalSettingsDataSource( + dataStore = testDataStore, defaultCaptureModeOverride = CaptureMode.STANDARD ) + val settingsRepository = LocalSettingsRepository( + settingsDataSource = settingsDataSource + ) val constraintsRepository = SettableConstraintsRepositoryImpl().apply { updateSystemConstraints(TYPICAL_SYSTEM_CONSTRAINTS) } @@ -69,7 +87,7 @@ internal class CameraAppSettingsViewModelTest { @After fun tearDown() { - sharedPreferences.edit().clear().commit() + datastoreScope.cancel() } @Test @@ -184,12 +202,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/settings.gradle.kts b/settings.gradle.kts index 21666d917..06d8bdf92 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,5 +65,5 @@ include(":core:camera:postprocess") include(":ui:controller") include(":ui:controller:impl") include(":ui:controller:testing") -include(":data:settings-datastore") -include(":data:settings-datastore:testing") +include(":core:settings:datastore-prefs") +include(":core:settings:datastore-prefs: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/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")) From d898370459482898a6227371a7b02f8bb157b1cd Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Mon, 8 Jun 2026 22:16:34 +0000 Subject: [PATCH 08/10] Address PR 525 review comments: optimize enum mapping performance, add rename warning comments, fix navigation encoding, and expand test coverage --- .../google/jetpackcamera/model/AspectRatio.kt | 4 + .../google/jetpackcamera/model/DarkMode.kt | 4 + .../jetpackcamera/model/DynamicRange.kt | 4 + .../google/jetpackcamera/model/FlashMode.kt | 4 + .../jetpackcamera/model/ImageOutputFormat.kt | 4 + .../google/jetpackcamera/model/LensFacing.kt | 4 + .../model/LowLightBoostPriority.kt | 4 + .../jetpackcamera/model/StabilizationMode.kt | 4 + .../jetpackcamera/model/StreamConfig.kt | 4 + .../jetpackcamera/model/VideoQuality.kt | 4 + ...LocalSettingsDataSourceInstrumentedTest.kt | 120 +++++++++++++++++- .../datastoreprefs/LocalSettingsDataSource.kt | 30 +++-- .../testing/FakeDataStoreModule.kt | 12 +- .../settings/ConstraintsModule.kt | 3 +- .../testing/FakeSettingsRepository.kt | 40 ++++-- .../navigation/DebugSettingsNavType.kt | 16 ++- feature/settings/build.gradle.kts | 2 + 17 files changed, 228 insertions(+), 35 deletions(-) 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 8b96e1304..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,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 AspectRatio(val numerator: Int, val denominator: Int) { THREE_FOUR(3, 4), NINE_SIXTEEN(9, 16), 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/DynamicRange.kt b/core/model/src/main/java/com/google/jetpackcamera/model/DynamicRange.kt index 391d8cb36..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 @@ -16,6 +16,10 @@ package com.google.jetpackcamera.model 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 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 c19a4826e..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 @@ -17,6 +17,10 @@ package com.google.jetpackcamera.model 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 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 b72c807b0..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,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 LensFacing { BACK, 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 38da49b84..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,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 LowLightBoostPriority { PRIORITIZE_AE_MODE, 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 9c0a45422..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,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 StabilizationMode { /** Stabilization off */ OFF, 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/VideoQuality.kt b/core/model/src/main/java/com/google/jetpackcamera/model/VideoQuality.kt index 5c52aa61b..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,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 VideoQuality { UNSPECIFIED, SD, diff --git a/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt index ba50137ee..d17bcb2af 100644 --- a/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt +++ b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt @@ -19,14 +19,21 @@ 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 com.google.jetpackcamera.core.settings.datastoreprefs.testing.FakeDataStoreModule import java.io.File import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -128,4 +135,115 @@ class LocalSettingsDataSourceInstrumentedTest { 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/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt index 0bc30810d..b723df692 100644 --- a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt @@ -48,19 +48,27 @@ class LocalSettingsDataSource @Inject constructor( override val defaultCameraAppSettings: Flow = dataStore.data.map { prefs -> CameraAppSettings( - cameraLensFacing = prefs[PreferenceKeys.KEY_LENS_FACING].toEnumOrDefault(LensFacing.BACK), + 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), + 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] ?: 0, - streamConfig = prefs[PreferenceKeys.KEY_STREAM_CONFIG].toEnumOrDefault(StreamConfig.MULTI_STREAM), + 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), + 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, captureMode = defaultCaptureModeOverride ) @@ -150,7 +158,11 @@ class LocalSettingsDataSource @Inject constructor( companion object { private inline fun > String?.toEnumOrDefault(default: T): T { if (this == null) return default - return enumValues().firstOrNull { it.name == this } ?: default + return try { + enumValueOf(this) + } catch (e: IllegalArgumentException) { + default + } } } } diff --git a/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt b/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt index 601986856..8c87ccd56 100644 --- a/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt +++ b/core/settings/datastore-prefs/testing/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/testing/FakeDataStoreModule.kt @@ -23,11 +23,9 @@ import kotlinx.coroutines.CoroutineScope object FakeDataStoreModule { - fun providePreferenceDataStore( - scope: CoroutineScope, - file: File - ): DataStore = PreferenceDataStoreFactory.create( - scope = scope, - produceFile = { file } - ) + fun providePreferenceDataStore(scope: CoroutineScope, file: File): DataStore = + PreferenceDataStoreFactory.create( + scope = scope, + produceFile = { file } + ) } 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/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 b49da9cf8..03aecc4a0 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 @@ -36,12 +36,14 @@ class FakeSettingsRepository( initialSettings: CameraAppSettings = DEFAULT_CAMERA_APP_SETTINGS ) : SettingsRepository { private val _defaultCameraAppSettings = MutableStateFlow(initialSettings) - override val defaultCameraAppSettings: Flow = _defaultCameraAppSettings.asStateFlow() + override val defaultCameraAppSettings: Flow = + _defaultCameraAppSettings.asStateFlow() override suspend fun getCurrentDefaultCameraAppSettings() = _defaultCameraAppSettings.value override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(cameraLensFacing = lensFacing) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(cameraLensFacing = lensFacing) } override suspend fun updateDarkModeStatus(darkMode: DarkMode) { @@ -49,46 +51,58 @@ class FakeSettingsRepository( } override suspend fun updateFlashModeStatus(flashMode: FlashMode) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(flashMode = flashMode) + _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy( + flashMode = flashMode + ) } override suspend fun updateStreamConfig(streamConfig: StreamConfig) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(streamConfig = streamConfig) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(streamConfig = streamConfig) } override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(lowLightBoostPriority = lowLightBoostPriority) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(lowLightBoostPriority = lowLightBoostPriority) } override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(stabilizationMode = stabilizationMode) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(stabilizationMode = stabilizationMode) } override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(dynamicRange = dynamicRange) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(dynamicRange = dynamicRange) } override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(aspectRatio = aspectRatio) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(aspectRatio = aspectRatio) } override suspend fun updateTargetFrameRate(targetFrameRate: Int) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(targetFrameRate = targetFrameRate) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(targetFrameRate = targetFrameRate) } override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(imageFormat = imageFormat) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(imageFormat = imageFormat) } override suspend fun updateMaxVideoDuration(durationMillis: Long) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(maxVideoDurationMillis = durationMillis) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(maxVideoDurationMillis = durationMillis) } override suspend fun updateVideoQuality(videoQuality: VideoQuality) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(videoQuality = videoQuality) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(videoQuality = videoQuality) } override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { - _defaultCameraAppSettings.value = _defaultCameraAppSettings.value.copy(audioEnabled = isAudioEnabled) + _defaultCameraAppSettings.value = + _defaultCameraAppSettings.value.copy(audioEnabled = isAudioEnabled) } } 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 18f823ffc..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 @@ -19,6 +19,9 @@ import android.os.Bundle import androidx.navigation.NavType import com.google.jetpackcamera.model.DebugSettings 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. @@ -43,14 +46,19 @@ internal object DebugSettingsNavType : NavType(isNullableAllowed } /** - * 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/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 736bfa6bc..364961cf4 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -114,6 +114,8 @@ dependencies { 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")) } From 721af6c17627f96e32a4a4bb37d0ed5389f651da Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Mon, 8 Jun 2026 22:53:22 +0000 Subject: [PATCH 09/10] Import onAllNodesWithTag in ComposeTestRuleExt.kt --- .../java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt index 2575e2566..42fe89a1c 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText From 7386f539a590403ef9040bebfad5f3e61a943f2a Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Wed, 10 Jun 2026 22:04:54 +0000 Subject: [PATCH 10/10] Address PR #525 review feedback: rename data source to PrefsDataStoreSettingsDataSource, remove Hilt dependencies from core:settings, add KDocs, and fix dependencies --- app/build.gradle.kts | 1 + .../google/jetpackcamera/AppSettingsModule.kt | 31 +++++++----- .../settings/datastore-prefs/build.gradle.kts | 12 +---- ...toreSettingsDataSourceInstrumentedTest.kt} | 6 +-- ...kt => PrefsDataStoreSettingsDataSource.kt} | 6 +-- .../SettingsDataSourceModule.kt | 32 ------------- .../settings/SettingsDataSource.kt | 47 ++++++++++++++++++- .../CameraAppSettingsViewModelTest.kt | 4 +- ui/debug/build.gradle.kts | 1 + 9 files changed, 77 insertions(+), 63 deletions(-) rename core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/DataStoreModule.kt => app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt (57%) rename core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/{LocalSettingsDataSourceInstrumentedTest.kt => PrefsDataStoreSettingsDataSourceInstrumentedTest.kt} (98%) rename core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/{LocalSettingsDataSource.kt => PrefsDataStoreSettingsDataSource.kt} (98%) delete mode 100644 core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 43568256a..8767a51f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -155,6 +155,7 @@ dependencies { 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/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/DataStoreModule.kt b/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt similarity index 57% rename from core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/DataStoreModule.kt rename to app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt index 187043a40..49a9f434c 100644 --- a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/DataStoreModule.kt +++ b/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt @@ -13,35 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.jetpackcamera.core.settings.datastoreprefs +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 -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob @Module @InstallIn(SingletonComponent::class) -object DataStoreModule { - private const val FILE_LOCATION = "jca_app_settings.preferences_pb" +object AppSettingsModule { @Provides @Singleton - fun providePreferenceDataStore(@ApplicationContext context: Context): DataStore = - PreferenceDataStoreFactory.create( - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - produceFile = { - context.preferencesDataStoreFile(FILE_LOCATION) - } + 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/settings/datastore-prefs/build.gradle.kts b/core/settings/datastore-prefs/build.gradle.kts index 1242a076f..c0ca9f8d4 100644 --- a/core/settings/datastore-prefs/build.gradle.kts +++ b/core/settings/datastore-prefs/build.gradle.kts @@ -16,8 +16,6 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) - alias(libs.plugins.dagger.hilt.android) } android { @@ -62,9 +60,7 @@ android { dependencies { implementation(libs.kotlinx.coroutines.core) - // Hilt - implementation(libs.dagger.hilt.android) - kapt(libs.dagger.hilt.compiler) + // Preferences DataStore (Zero Protobuf) implementation(libs.androidx.datastore.preferences) @@ -82,10 +78,6 @@ dependencies { implementation(project(":core:model")) implementation(project(":core:common")) implementation(project(":core:settings")) - implementation(project(":data:settings")) } -// Allow references to generated code -kapt { - correctErrorTypes = true -} + diff --git a/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSourceInstrumentedTest.kt similarity index 98% rename from core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt rename to core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSourceInstrumentedTest.kt index d17bcb2af..a74760915 100644 --- a/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSourceInstrumentedTest.kt +++ b/core/settings/datastore-prefs/src/androidTest/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSourceInstrumentedTest.kt @@ -53,14 +53,14 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) -class LocalSettingsDataSourceInstrumentedTest { +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: LocalSettingsDataSource + private lateinit var dataSource: PrefsDataStoreSettingsDataSource @Before fun setup() = runTest { @@ -72,7 +72,7 @@ class LocalSettingsDataSourceInstrumentedTest { scope = datastoreScope, file = testFile ) - dataSource = LocalSettingsDataSource( + dataSource = PrefsDataStoreSettingsDataSource( dataStore = testDataStore, defaultCaptureModeOverride = CaptureMode.STANDARD ) diff --git a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt similarity index 98% rename from core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt rename to core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt index b723df692..c40660383 100644 --- a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/LocalSettingsDataSource.kt +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt @@ -29,11 +29,11 @@ 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 javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -41,7 +41,7 @@ import kotlinx.coroutines.flow.map /** * Implementation of [SettingsDataSource] with locally stored Preferences DataStore. */ -class LocalSettingsDataSource @Inject constructor( +class PrefsDataStoreSettingsDataSource( private val dataStore: DataStore, @DefaultCaptureModeOverride private val defaultCaptureModeOverride: CaptureMode ) : SettingsDataSource { @@ -56,7 +56,7 @@ class LocalSettingsDataSource @Inject constructor( .toEnumOrDefault(AspectRatio.NINE_SIXTEEN), stabilizationMode = prefs[PreferenceKeys.KEY_STABILIZATION_MODE] .toEnumOrDefault(StabilizationMode.AUTO), - targetFrameRate = prefs[PreferenceKeys.KEY_TARGET_FRAME_RATE] ?: 0, + 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] diff --git a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt deleted file mode 100644 index 2fc1e937c..000000000 --- a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/SettingsDataSourceModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 com.google.jetpackcamera.settings.SettingsDataSource -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -abstract class SettingsDataSourceModule { - - @Binds - abstract fun bindSettingsDataSource( - localSettingsDataSource: LocalSettingsDataSource - ): SettingsDataSource -} 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 index 4ddace37b..30fa065d0 100644 --- a/core/settings/src/main/java/com/google/jetpackcamera/settings/SettingsDataSource.kt +++ b/core/settings/src/main/java/com/google/jetpackcamera/settings/SettingsDataSource.kt @@ -29,37 +29,82 @@ import com.google.jetpackcamera.settings.model.CameraAppSettings import kotlinx.coroutines.flow.Flow /** - * Data source for settings. + * 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) } 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 95823b256..0219ecb5e 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 @@ -21,7 +21,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import com.google.jetpackcamera.core.settings.datastoreprefs.LocalSettingsDataSource +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.DarkMode @@ -68,7 +68,7 @@ internal class CameraAppSettingsViewModelTest { file = testFile ) - val settingsDataSource = LocalSettingsDataSource( + val settingsDataSource = PrefsDataStoreSettingsDataSource( dataStore = testDataStore, defaultCaptureModeOverride = CaptureMode.STANDARD ) 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"))