Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
72f4cad
Refactor settings storage to Preferences DataStore to remove Protobuf
davidjiagoogle May 28, 2026
4279a6b
Address code review comments on safe string parsing & apply spotless …
davidjiagoogle May 28, 2026
cdcb36c
Remove unused androidx.datastore dependency from feature/settings and…
davidjiagoogle Jun 1, 2026
2e7792f
Clean up unused datastore and protobuf version and plugin declaration…
davidjiagoogle Jun 1, 2026
d6d18ad
Merge branch 'main' into david/protobufRemoval
davidjiagoogle Jun 1, 2026
beae310
Remove obsolete protobuf files and ProtoConversionTest from data:sett…
davidjiagoogle Jun 1, 2026
f752651
Merge branch 'main' into david/protobufRemoval
davidjiagoogle Jun 4, 2026
2b6816a
Refactor persistence into standalone :data:settings-datastore module
davidjiagoogle Jun 5, 2026
2385509
Merge branch 'main' into david/protobufRemoval
davidjiagoogle Jun 5, 2026
08094c0
Refactor settings persistence using modular Preferences DataStore (Op…
davidjiagoogle Jun 5, 2026
d898370
Address PR 525 review comments: optimize enum mapping performance, ad…
davidjiagoogle Jun 8, 2026
fb28c2c
Merge branch 'main' into david/protobufRemoval
davidjiagoogle Jun 8, 2026
721af6c
Import onAllNodesWithTag in ComposeTestRuleExt.kt
davidjiagoogle Jun 8, 2026
ecf3c27
Merge branch 'main' into david/protobufRemoval and resolve conflicts
davidjiagoogle Jun 10, 2026
7386f53
Address PR #525 review feedback: rename data source to PrefsDataStore…
davidjiagoogle Jun 10, 2026
bd78d82
Merge branch 'main' into david/protobufRemoval and resolve conflicts
davidjiagoogle Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ dependencies {

// Access settings & model data
implementation(project(":data:settings"))
implementation(project(":core:settings:datastore-prefs"))
implementation(project(":core:settings"))
implementation(project(":core:model"))
implementation(libs.androidx.datastore.preferences)

// Camera Preview
implementation(project(":feature:preview"))
Expand Down
54 changes: 54 additions & 0 deletions app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.jetpackcamera

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import com.google.jetpackcamera.core.common.DefaultCaptureModeOverride
import com.google.jetpackcamera.core.settings.datastoreprefs.PrefsDataStoreSettingsDataSource
import com.google.jetpackcamera.model.CaptureMode
import com.google.jetpackcamera.settings.SettingsDataSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppSettingsModule {

@Provides
@Singleton
fun providePreferencesDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
return PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile("app_settings.preferences_pb") }
)
}

@Provides
@Singleton
fun provideSettingsDataSource(
dataStore: DataStore<Preferences>,
@DefaultCaptureModeOverride defaultCaptureMode: CaptureMode
): SettingsDataSource {
return PrefsDataStoreSettingsDataSource(dataStore, defaultCaptureMode)
}
}
1 change: 1 addition & 0 deletions core/camera/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
1 change: 1 addition & 0 deletions core/camera/testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 1 addition & 26 deletions core/model/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -70,37 +69,13 @@ android {
dependencies {
implementation(libs.kotlinx.coroutines.core)

// proto datastore
implementation(libs.androidx.datastore)
implementation(libs.protobuf.kotlin.lite)


// Testing
testImplementation(libs.junit)
testImplementation(libs.truth)
}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.21.12"
}

generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}

task.builtins {
create("kotlin") {
option("lite")
}
}
}
}
}

// Allow references to generated code
kapt {
correctErrorTypes = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
*/
package com.google.jetpackcamera.model

import com.google.jetpackcamera.model.proto.AspectRatio as AspectRatioProto

/**
* WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore.
* Renaming constants will break compatibility with existing saved settings.
*/
enum class AspectRatio(val numerator: Int, val denominator: Int) {
THREE_FOUR(3, 4),
NINE_SIXTEEN(9, 16),
Expand All @@ -31,21 +33,4 @@ enum class AspectRatio(val numerator: Int, val denominator: Int) {
* Returns the landscape aspect ratio as a [Float].
*/
fun toLandscapeFloat(): Float = denominator.toFloat() / numerator

companion object {

/** returns the AspectRatio enum equivalent of a provided AspectRatioProto */
fun fromProto(aspectRatioProto: AspectRatioProto): AspectRatio {
return when (aspectRatioProto) {
AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN -> NINE_SIXTEEN
AspectRatioProto.ASPECT_RATIO_ONE_ONE -> ONE_ONE

// defaults to 3:4 aspect ratio
AspectRatioProto.ASPECT_RATIO_THREE_FOUR,
AspectRatioProto.ASPECT_RATIO_UNDEFINED,
AspectRatioProto.UNRECOGNIZED
-> THREE_FOUR
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@
*/
package com.google.jetpackcamera.model

import android.util.Base64
import com.google.jetpackcamera.model.LensFacing.Companion.toProto
import com.google.jetpackcamera.model.TestPattern.Companion.toProto
import com.google.jetpackcamera.model.proto.DebugSettings as DebugSettingsProto
import com.google.jetpackcamera.model.proto.debugSettings as debugSettingsProto

/**
* Data class for defining settings used in debug flows within the app.
*
Expand All @@ -38,64 +32,81 @@ data class DebugSettings(
) {
companion object {
/**
* Creates a [DebugSettings] domain model from its protobuf representation.
*
* @param proto The [DebugSettingsProto] instance.
* @return The corresponding [DebugSettings] instance.
* Parses the string into a [DebugSettings] instance.
*/
fun fromProto(proto: DebugSettingsProto): DebugSettings {
return DebugSettings(
isDebugModeEnabled = proto.isDebugModeEnabled,
singleLensMode = if (proto.hasSingleLensMode()) {
LensFacing.fromProto(proto.singleLensMode)
} else {
null
},
testPattern = TestPattern.fromProto(proto.testPattern)
)
}
fun parseFromString(value: String): DebugSettings {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are replacing Protobuf with a custom, hand-rolled string parser, we need unit tests to ensure this logic is robust. String parsing based on delimiters (:, ;, ,) and specific string formats (like SolidColor(...)) is prone to regressions if the model changes in the future.

Could you please add a DebugSettingsTest.kt in the core:model module? It should verify that parseFromString(encodeAsString()) correctly rebuilds the object. Specifically, we should test:

  1. Default values (all fields empty/off).
  2. Simple TestPattern variants (like ColorBars).
  3. The complex TestPattern.SolidColor variant to ensure the regex/splitting of the 4 color channels works correctly.
  4. Edge cases like missing delimiters or malformed strings (to ensure it falls back gracefully).

val parts = value.split(";")
var isDebugModeEnabled = false
var singleLensMode: LensFacing? = null
var testPattern: TestPattern = TestPattern.Off

/**
* Converts a [DebugSettings] domain model to its protobuf representation.
*
* @receiver The [DebugSettings] instance to convert.
* @return The corresponding [DebugSettingsProto] instance.
*/
fun DebugSettings.toProto(): DebugSettingsProto = debugSettingsProto {
isDebugModeEnabled = this@toProto.isDebugModeEnabled
this@toProto.singleLensMode?.let { lensFacing ->
singleLensMode = lensFacing.toProto()
for (part in parts) {
val kv = part.split(":")
if (kv.size == 2) {
when (kv[0]) {
"debug" -> isDebugModeEnabled = kv[1].toBoolean()
"lens" -> singleLensMode = enumValues<LensFacing>()
.firstOrNull { it.name == kv[1] }
"pattern" -> {
testPattern = when (kv[1]) {
"Off" -> TestPattern.Off
"ColorBars" -> TestPattern.ColorBars
"ColorBarsFadeToGray" -> TestPattern.ColorBarsFadeToGray
"PN9" -> TestPattern.PN9
"Custom1" -> TestPattern.Custom1
else -> {
if (kv[1].startsWith("SolidColor(") &&
kv[1].endsWith(")")
) {
val channels = kv[1]
.removePrefix("SolidColor(")
.removeSuffix(")")
.split(",")
if (channels.size == 4) {
val red = channels[0].toUIntOrNull()
val greenEven = channels[1].toUIntOrNull()
val greenOdd = channels[2].toUIntOrNull()
val blue = channels[3].toUIntOrNull()
if (red != null &&
greenEven != null &&
greenOdd != null &&
blue != null
) {
TestPattern.SolidColor(
red,
greenEven,
greenOdd,
blue
)
} else {
TestPattern.Off
}
} else {
TestPattern.Off
}
} else {
TestPattern.Off
}
}
}
}
}
}
}
testPattern = this@toProto.testPattern.toProto()
}

/**
* Parses the encoded byte array into a [DebugSettings] instance.
*/
fun parseFromByteArray(value: ByteArray): DebugSettings {
val protoValue = DebugSettingsProto.parseFrom(value)
return fromProto(protoValue)
return DebugSettings(isDebugModeEnabled, singleLensMode, testPattern)
}
Comment thread
davidjiagoogle marked this conversation as resolved.

/**
* 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"
}
}
Comment thread
davidjiagoogle marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,13 @@
* limitations under the License.
*/
package com.google.jetpackcamera.model
import com.google.jetpackcamera.model.proto.DynamicRange as DynamicRangeProto

val DEFAULT_HDR_DYNAMIC_RANGE = DynamicRange.HLG10

/**
* WARNING: The string representation of this enum is serialized and persisted in Preferences DataStore.
* Renaming constants will break compatibility with existing saved settings.
*/
enum class DynamicRange {
SDR,
HLG10;

companion object {

/** returns the DynamicRangeType enum equivalent of a provided DynamicRangeTypeProto */
fun fromProto(dynamicRangeProto: DynamicRangeProto): DynamicRange {
return when (dynamicRangeProto) {
DynamicRangeProto.DYNAMIC_RANGE_HLG10 -> HLG10

// Treat unrecognized and unspecified as SDR as a fallback
DynamicRangeProto.DYNAMIC_RANGE_SDR,
DynamicRangeProto.DYNAMIC_RANGE_UNSPECIFIED,
DynamicRangeProto.UNRECOGNIZED -> SDR
}
}

fun DynamicRange.toProto(): DynamicRangeProto {
return when (this) {
SDR -> DynamicRangeProto.DYNAMIC_RANGE_SDR
HLG10 -> DynamicRangeProto.DYNAMIC_RANGE_HLG10
}
}
}
HLG10
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading