diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index e2bf5e9ae..c66587a0e 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -105,4 +105,6 @@ When reviewing a pull request, focus on the following key areas: * **Prioritize Impact:** Focus on the most important issues first (e.g., architectural flaws, missing tests) before minor stylistic nits. * **Indicate Low Priority:** For minor cosmetic, spacing, or simple typographical suggestions, preface the comment with `nit:` to indicate it is a low-priority polish item. * **Cite Sources:** When suggesting a change based on a best practice or API guideline, link to the relevant official documentation (e.g., developer.android.com) to support your feedback. -* **Tone:** Maintain a helpful, collaborative, concise, and professional tone. \ No newline at end of file +* **Tone:** Maintain a helpful, collaborative, concise, and professional tone. +12. **Proto Enum Best Practices** + * **Zero-Value Requirement:** In accordance with the [Proto3 Style Guide](https://protobuf.dev/programming-guides/style/#enums), the first value of any proto enum must be `0` and should be named `_UNSPECIFIED`. This zero-value acts as the default when a field is missing, preventing implicit fallbacks to a semantically valid choice. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ad359692b..baca47368 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,10 +153,9 @@ dependencies { // Access settings & model data implementation(project(":data:settings")) - implementation(project(":core:settings:datastore-prefs")) + implementation(project(":core:settings:datastore-proto")) implementation(project(":core:settings")) implementation(project(":core:model")) - implementation(libs.androidx.datastore.preferences) // Camera Preview implementation(project(":feature:preview")) diff --git a/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt b/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt index 49a9f434c..aacd9760c 100644 --- a/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt +++ b/app/src/main/java/com/google/jetpackcamera/AppSettingsModule.kt @@ -16,13 +16,10 @@ 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.core.common.IODispatcher import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.settings.ProtoDataStoreSettingsDataSource import com.google.jetpackcamera.settings.SettingsDataSource import dagger.Module import dagger.Provides @@ -30,25 +27,19 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher @Module @InstallIn(SingletonComponent::class) object AppSettingsModule { - @Provides - @Singleton - fun providePreferencesDataStore(@ApplicationContext context: Context): DataStore { - return PreferenceDataStoreFactory.create( - produceFile = { context.preferencesDataStoreFile("app_settings.preferences_pb") } - ) - } - @Provides @Singleton fun provideSettingsDataSource( - dataStore: DataStore, - @DefaultCaptureModeOverride defaultCaptureMode: CaptureMode + @ApplicationContext context: Context, + @DefaultCaptureModeOverride defaultCaptureMode: CaptureMode, + @IODispatcher ioDispatcher: CoroutineDispatcher ): SettingsDataSource { - return PrefsDataStoreSettingsDataSource(dataStore, defaultCaptureMode) + return ProtoDataStoreSettingsDataSource.create(context, defaultCaptureMode, ioDispatcher) } } diff --git a/core/model-proto/build.gradle.kts b/core/model-proto/build.gradle.kts new file mode 100644 index 000000000..c3a0d3289 --- /dev/null +++ b/core/model-proto/build.gradle.kts @@ -0,0 +1,76 @@ +/* + * 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.protobuf) +} + +android { + namespace = "com.google.jetpackcamera.model.proto" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.21.12" + } + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + create("kotlin") { + option("lite") + } + } + } + } +} + +dependencies { + api(project(":core:model")) + api(libs.protobuf.kotlin.lite) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.kotlinx.coroutines.test) +} diff --git a/core/model-proto/src/main/java/com/google/jetpackcamera/model/proto/ModelMappers.kt b/core/model-proto/src/main/java/com/google/jetpackcamera/model/proto/ModelMappers.kt new file mode 100644 index 000000000..43248ac33 --- /dev/null +++ b/core/model-proto/src/main/java/com/google/jetpackcamera/model/proto/ModelMappers.kt @@ -0,0 +1,281 @@ +/* + * 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.model.proto + +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DarkMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.LowLightBoostPriority +import com.google.jetpackcamera.model.StabilizationMode +import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.model.proto.AspectRatio as AspectRatioProto +import com.google.jetpackcamera.model.proto.ConcurrentCameraMode as ConcurrentCameraModeProto +import com.google.jetpackcamera.model.proto.DarkMode as DarkModeProto +import com.google.jetpackcamera.model.proto.DynamicRange as DynamicRangeProto +import com.google.jetpackcamera.model.proto.FlashMode as FlashModeProto +import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto +import com.google.jetpackcamera.model.proto.LensFacing as LensFacingProto +import com.google.jetpackcamera.model.proto.LowLightBoostPriority as LowLightBoostPriorityProto +import com.google.jetpackcamera.model.proto.StabilizationMode as StabilizationModeProto +import com.google.jetpackcamera.model.proto.VideoQuality as VideoQualityProto + +/** + * Maps a Proto model to a Domain model. + */ +fun AspectRatioProto.toModel(): AspectRatio { + return when (this) { + AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN -> AspectRatio.NINE_SIXTEEN + AspectRatioProto.ASPECT_RATIO_ONE_ONE -> AspectRatio.ONE_ONE + AspectRatioProto.ASPECT_RATIO_THREE_FOUR, + AspectRatioProto.ASPECT_RATIO_UNSPECIFIED, + AspectRatioProto.UNRECOGNIZED -> AspectRatio.THREE_FOUR + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun AspectRatio.toProto(): AspectRatioProto { + return when (this) { + AspectRatio.NINE_SIXTEEN -> AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN + AspectRatio.ONE_ONE -> AspectRatioProto.ASPECT_RATIO_ONE_ONE + AspectRatio.THREE_FOUR -> AspectRatioProto.ASPECT_RATIO_THREE_FOUR + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun DarkModeProto.toModel(): DarkMode { + return when (this) { + DarkModeProto.DARK_MODE_DARK -> DarkMode.DARK + DarkModeProto.DARK_MODE_LIGHT -> DarkMode.LIGHT + DarkModeProto.DARK_MODE_SYSTEM, + DarkModeProto.DARK_MODE_UNSPECIFIED, + DarkModeProto.UNRECOGNIZED -> DarkMode.SYSTEM + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun DarkMode.toProto(): DarkModeProto { + return when (this) { + DarkMode.DARK -> DarkModeProto.DARK_MODE_DARK + DarkMode.LIGHT -> DarkModeProto.DARK_MODE_LIGHT + DarkMode.SYSTEM -> DarkModeProto.DARK_MODE_SYSTEM + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun DynamicRangeProto.toModel(): DynamicRange { + return when (this) { + DynamicRangeProto.DYNAMIC_RANGE_HLG10 -> DynamicRange.HLG10 + DynamicRangeProto.DYNAMIC_RANGE_SDR, + DynamicRangeProto.DYNAMIC_RANGE_UNSPECIFIED, + DynamicRangeProto.UNRECOGNIZED -> DynamicRange.SDR + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun DynamicRange.toProto(): DynamicRangeProto { + return when (this) { + DynamicRange.HLG10 -> DynamicRangeProto.DYNAMIC_RANGE_HLG10 + DynamicRange.SDR -> DynamicRangeProto.DYNAMIC_RANGE_SDR + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun FlashModeProto.toModel(): FlashMode { + return when (this) { + FlashModeProto.FLASH_MODE_ON -> FlashMode.ON + FlashModeProto.FLASH_MODE_AUTO -> FlashMode.AUTO + FlashModeProto.FLASH_MODE_LOW_LIGHT_BOOST -> FlashMode.LOW_LIGHT_BOOST + FlashModeProto.FLASH_MODE_OFF, + FlashModeProto.FLASH_MODE_UNSPECIFIED, + FlashModeProto.UNRECOGNIZED -> FlashMode.OFF + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun FlashMode.toProto(): FlashModeProto { + return when (this) { + FlashMode.ON -> FlashModeProto.FLASH_MODE_ON + FlashMode.AUTO -> FlashModeProto.FLASH_MODE_AUTO + FlashMode.LOW_LIGHT_BOOST -> FlashModeProto.FLASH_MODE_LOW_LIGHT_BOOST + FlashMode.OFF -> FlashModeProto.FLASH_MODE_OFF + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun ImageOutputFormatProto.toModel(): ImageOutputFormat { + return when (this) { + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR -> + ImageOutputFormat.JPEG_ULTRA_HDR + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG, + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_UNSPECIFIED, + ImageOutputFormatProto.UNRECOGNIZED -> ImageOutputFormat.JPEG + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun ImageOutputFormat.toProto(): ImageOutputFormatProto { + return when (this) { + ImageOutputFormat.JPEG_ULTRA_HDR -> + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR + ImageOutputFormat.JPEG -> ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun LensFacingProto.toModel(): LensFacing { + return when (this) { + LensFacingProto.LENS_FACING_FRONT -> LensFacing.FRONT + LensFacingProto.LENS_FACING_BACK, + LensFacingProto.LENS_FACING_UNSPECIFIED, + LensFacingProto.UNRECOGNIZED -> LensFacing.BACK + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun LensFacing.toProto(): LensFacingProto { + return when (this) { + LensFacing.FRONT -> LensFacingProto.LENS_FACING_FRONT + LensFacing.BACK -> LensFacingProto.LENS_FACING_BACK + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun LowLightBoostPriorityProto.toModel(): LowLightBoostPriority { + return when (this) { + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES -> + LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE, + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_UNSPECIFIED, + LowLightBoostPriorityProto.UNRECOGNIZED -> LowLightBoostPriority.PRIORITIZE_AE_MODE + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun LowLightBoostPriority.toProto(): LowLightBoostPriorityProto { + return when (this) { + LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES -> + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES + LowLightBoostPriority.PRIORITIZE_AE_MODE -> + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun StabilizationModeProto.toModel(): StabilizationMode { + return when (this) { + StabilizationModeProto.STABILIZATION_MODE_ON -> StabilizationMode.ON + StabilizationModeProto.STABILIZATION_MODE_OFF -> StabilizationMode.OFF + StabilizationModeProto.STABILIZATION_MODE_HIGH_QUALITY -> StabilizationMode.HIGH_QUALITY + StabilizationModeProto.STABILIZATION_MODE_OPTICAL -> StabilizationMode.OPTICAL + StabilizationModeProto.STABILIZATION_MODE_AUTO, + StabilizationModeProto.STABILIZATION_MODE_UNSPECIFIED, + StabilizationModeProto.UNRECOGNIZED -> StabilizationMode.AUTO + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun StabilizationMode.toProto(): StabilizationModeProto { + return when (this) { + StabilizationMode.ON -> StabilizationModeProto.STABILIZATION_MODE_ON + StabilizationMode.OFF -> StabilizationModeProto.STABILIZATION_MODE_OFF + StabilizationMode.HIGH_QUALITY -> StabilizationModeProto.STABILIZATION_MODE_HIGH_QUALITY + StabilizationMode.OPTICAL -> StabilizationModeProto.STABILIZATION_MODE_OPTICAL + StabilizationMode.AUTO -> StabilizationModeProto.STABILIZATION_MODE_AUTO + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun VideoQualityProto.toModel(): VideoQuality { + return when (this) { + VideoQualityProto.VIDEO_QUALITY_SD -> VideoQuality.SD + VideoQualityProto.VIDEO_QUALITY_HD -> VideoQuality.HD + VideoQualityProto.VIDEO_QUALITY_FHD -> VideoQuality.FHD + VideoQualityProto.VIDEO_QUALITY_UHD -> VideoQuality.UHD + VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED, + VideoQualityProto.UNRECOGNIZED -> VideoQuality.UNSPECIFIED + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun VideoQuality.toProto(): VideoQualityProto { + return when (this) { + VideoQuality.SD -> VideoQualityProto.VIDEO_QUALITY_SD + VideoQuality.HD -> VideoQualityProto.VIDEO_QUALITY_HD + VideoQuality.FHD -> VideoQualityProto.VIDEO_QUALITY_FHD + VideoQuality.UHD -> VideoQualityProto.VIDEO_QUALITY_UHD + VideoQuality.UNSPECIFIED -> VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED + } +} + +/** + * Maps a Proto model to a Domain model. + */ +fun ConcurrentCameraModeProto.toModel(): ConcurrentCameraMode { + return when (this) { + ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL -> ConcurrentCameraMode.DUAL + ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_OFF, + ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_UNSPECIFIED, + ConcurrentCameraModeProto.UNRECOGNIZED -> ConcurrentCameraMode.OFF + } +} + +/** + * Maps a Domain model to a Proto model. + */ +fun ConcurrentCameraMode.toProto(): ConcurrentCameraModeProto { + return when (this) { + ConcurrentCameraMode.DUAL -> ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL + ConcurrentCameraMode.OFF -> ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_OFF + } +} diff --git a/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/aspect_ratio.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/aspect_ratio.proto new file mode 100644 index 000000000..7c9d2bf27 --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/aspect_ratio.proto @@ -0,0 +1,27 @@ +/* + * 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_UNSPECIFIED = 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-proto/src/main/proto/com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto new file mode 100644 index 000000000..ead8ea876 --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto @@ -0,0 +1,25 @@ +/* + * 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. + */ +syntax = "proto3"; + +option java_package = "com.google.jetpackcamera.model.proto"; +option java_multiple_files = true; + +enum ConcurrentCameraMode { + CONCURRENT_CAMERA_MODE_UNSPECIFIED = 0; + CONCURRENT_CAMERA_MODE_OFF = 1; + CONCURRENT_CAMERA_MODE_DUAL = 2; +} diff --git a/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/dark_mode.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/dark_mode.proto new file mode 100644 index 000000000..0697f0d9c --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/dark_mode.proto @@ -0,0 +1,27 @@ +/* + * 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_UNSPECIFIED = 0; + DARK_MODE_SYSTEM = 1; + DARK_MODE_LIGHT= 2; + DARK_MODE_DARK = 3; +} \ No newline at end of file diff --git a/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/debug_settings.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/debug_settings.proto new file mode 100644 index 000000000..2c3633afc --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/debug_settings.proto @@ -0,0 +1,29 @@ +/* + * 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-proto/src/main/proto/com/google/jetpackcamera/model/proto/dynamic_range.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/dynamic_range.proto new file mode 100644 index 000000000..6ebe80b97 --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/dynamic_range.proto @@ -0,0 +1,26 @@ +/* + * 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-proto/src/main/proto/com/google/jetpackcamera/model/proto/flash_mode.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/flash_mode.proto new file mode 100644 index 000000000..e96dc45ad --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/flash_mode.proto @@ -0,0 +1,28 @@ +/* + * 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_UNSPECIFIED = 0; + FLASH_MODE_AUTO = 1; + FLASH_MODE_ON = 2; + FLASH_MODE_OFF = 3; + FLASH_MODE_LOW_LIGHT_BOOST = 4; +} \ No newline at end of file diff --git a/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/image_output_format.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/image_output_format.proto new file mode 100644 index 000000000..0a6182cf3 --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/image_output_format.proto @@ -0,0 +1,26 @@ +/* + * 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_UNSPECIFIED = 0; + IMAGE_OUTPUT_FORMAT_JPEG = 1; + IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR = 2; +} \ No newline at end of file diff --git a/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/lens_facing.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/lens_facing.proto new file mode 100644 index 000000000..30e7d7e9d --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/lens_facing.proto @@ -0,0 +1,26 @@ +/* + * 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_UNSPECIFIED = 0; + LENS_FACING_BACK = 1; + LENS_FACING_FRONT = 2; +} \ No newline at end of file diff --git a/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/low_light_boost_priority.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/low_light_boost_priority.proto new file mode 100644 index 000000000..9c64fff3e --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/low_light_boost_priority.proto @@ -0,0 +1,26 @@ +/* + * 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_UNSPECIFIED = 0; + LOW_LIGHT_BOOST_PRIORITY_AE_MODE = 1; + LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES = 2; +} diff --git a/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/stabilization_mode.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/stabilization_mode.proto new file mode 100644 index 000000000..24d0eb764 --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/stabilization_mode.proto @@ -0,0 +1,29 @@ +/* + * 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_UNSPECIFIED = 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-proto/src/main/proto/com/google/jetpackcamera/model/proto/test_pattern.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/test_pattern.proto new file mode 100644 index 000000000..b74d3d96d --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/test_pattern.proto @@ -0,0 +1,55 @@ +/* + * 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-proto/src/main/proto/com/google/jetpackcamera/model/proto/video_quality.proto b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/video_quality.proto new file mode 100644 index 000000000..b88b423cc --- /dev/null +++ b/core/model-proto/src/main/proto/com/google/jetpackcamera/model/proto/video_quality.proto @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +option java_package = "com.google.jetpackcamera.model.proto"; +option java_multiple_files = true; + +enum VideoQuality { + VIDEO_QUALITY_UNSPECIFIED = 0; + VIDEO_QUALITY_SD = 1; + VIDEO_QUALITY_HD = 2; + VIDEO_QUALITY_FHD = 3; + VIDEO_QUALITY_UHD = 4; +} \ No newline at end of file diff --git a/core/model-proto/src/test/java/com/google/jetpackcamera/model/proto/ModelMappersTest.kt b/core/model-proto/src/test/java/com/google/jetpackcamera/model/proto/ModelMappersTest.kt new file mode 100644 index 000000000..4c25cbf24 --- /dev/null +++ b/core/model-proto/src/test/java/com/google/jetpackcamera/model/proto/ModelMappersTest.kt @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.model.proto + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DarkMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.LowLightBoostPriority +import com.google.jetpackcamera.model.StabilizationMode +import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.model.proto.AspectRatio as AspectRatioProto +import com.google.jetpackcamera.model.proto.ConcurrentCameraMode as ConcurrentCameraModeProto +import com.google.jetpackcamera.model.proto.DarkMode as DarkModeProto +import com.google.jetpackcamera.model.proto.DynamicRange as DynamicRangeProto +import com.google.jetpackcamera.model.proto.FlashMode as FlashModeProto +import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto +import com.google.jetpackcamera.model.proto.LensFacing as LensFacingProto +import com.google.jetpackcamera.model.proto.LowLightBoostPriority as LowLightBoostPriorityProto +import com.google.jetpackcamera.model.proto.StabilizationMode as StabilizationModeProto +import com.google.jetpackcamera.model.proto.VideoQuality as VideoQualityProto +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class ModelMappersTest { + + @Test + fun aspectRatioMapsCorrectly() { + assertThat( + AspectRatio.NINE_SIXTEEN.toProto() + ).isEqualTo(AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN) + assertThat( + AspectRatioProto.ASPECT_RATIO_NINE_SIXTEEN.toModel() + ).isEqualTo(AspectRatio.NINE_SIXTEEN) + + assertThat(AspectRatio.ONE_ONE.toProto()).isEqualTo(AspectRatioProto.ASPECT_RATIO_ONE_ONE) + assertThat(AspectRatioProto.ASPECT_RATIO_ONE_ONE.toModel()).isEqualTo(AspectRatio.ONE_ONE) + + assertThat( + AspectRatio.THREE_FOUR.toProto() + ).isEqualTo(AspectRatioProto.ASPECT_RATIO_THREE_FOUR) + assertThat( + AspectRatioProto.ASPECT_RATIO_THREE_FOUR.toModel() + ).isEqualTo(AspectRatio.THREE_FOUR) + + assertThat(AspectRatioProto.UNRECOGNIZED.toModel()).isEqualTo(AspectRatio.THREE_FOUR) + } + + @Test + fun darkModeMapsCorrectly() { + assertThat(DarkMode.DARK.toProto()).isEqualTo(DarkModeProto.DARK_MODE_DARK) + assertThat(DarkModeProto.DARK_MODE_DARK.toModel()).isEqualTo(DarkMode.DARK) + + assertThat(DarkMode.LIGHT.toProto()).isEqualTo(DarkModeProto.DARK_MODE_LIGHT) + assertThat(DarkModeProto.DARK_MODE_LIGHT.toModel()).isEqualTo(DarkMode.LIGHT) + + assertThat(DarkMode.SYSTEM.toProto()).isEqualTo(DarkModeProto.DARK_MODE_SYSTEM) + assertThat(DarkModeProto.DARK_MODE_SYSTEM.toModel()).isEqualTo(DarkMode.SYSTEM) + + assertThat(DarkModeProto.UNRECOGNIZED.toModel()).isEqualTo(DarkMode.SYSTEM) + } + + @Test + fun dynamicRangeMapsCorrectly() { + assertThat(DynamicRange.HLG10.toProto()).isEqualTo(DynamicRangeProto.DYNAMIC_RANGE_HLG10) + assertThat(DynamicRangeProto.DYNAMIC_RANGE_HLG10.toModel()).isEqualTo(DynamicRange.HLG10) + + assertThat(DynamicRange.SDR.toProto()).isEqualTo(DynamicRangeProto.DYNAMIC_RANGE_SDR) + assertThat(DynamicRangeProto.DYNAMIC_RANGE_SDR.toModel()).isEqualTo(DynamicRange.SDR) + + assertThat(DynamicRangeProto.UNRECOGNIZED.toModel()).isEqualTo(DynamicRange.SDR) + } + + @Test + fun flashModeMapsCorrectly() { + assertThat(FlashMode.ON.toProto()).isEqualTo(FlashModeProto.FLASH_MODE_ON) + assertThat(FlashModeProto.FLASH_MODE_ON.toModel()).isEqualTo(FlashMode.ON) + + assertThat(FlashMode.AUTO.toProto()).isEqualTo(FlashModeProto.FLASH_MODE_AUTO) + assertThat(FlashModeProto.FLASH_MODE_AUTO.toModel()).isEqualTo(FlashMode.AUTO) + + assertThat(FlashMode.OFF.toProto()).isEqualTo(FlashModeProto.FLASH_MODE_OFF) + assertThat(FlashModeProto.FLASH_MODE_OFF.toModel()).isEqualTo(FlashMode.OFF) + + assertThat( + FlashMode.LOW_LIGHT_BOOST.toProto() + ).isEqualTo(FlashModeProto.FLASH_MODE_LOW_LIGHT_BOOST) + assertThat( + FlashModeProto.FLASH_MODE_LOW_LIGHT_BOOST.toModel() + ).isEqualTo(FlashMode.LOW_LIGHT_BOOST) + + assertThat(FlashModeProto.UNRECOGNIZED.toModel()).isEqualTo(FlashMode.OFF) + } + + @Test + fun imageOutputFormatMapsCorrectly() { + assertThat( + ImageOutputFormat.JPEG.toProto() + ).isEqualTo(ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG) + assertThat( + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG.toModel() + ).isEqualTo(ImageOutputFormat.JPEG) + + assertThat( + ImageOutputFormat.JPEG_ULTRA_HDR.toProto() + ).isEqualTo(ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR) + assertThat( + ImageOutputFormatProto.IMAGE_OUTPUT_FORMAT_JPEG_ULTRA_HDR.toModel() + ).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) + + assertThat(ImageOutputFormatProto.UNRECOGNIZED.toModel()).isEqualTo(ImageOutputFormat.JPEG) + } + + @Test + fun lensFacingMapsCorrectly() { + assertThat(LensFacing.FRONT.toProto()).isEqualTo(LensFacingProto.LENS_FACING_FRONT) + assertThat(LensFacingProto.LENS_FACING_FRONT.toModel()).isEqualTo(LensFacing.FRONT) + + assertThat(LensFacing.BACK.toProto()).isEqualTo(LensFacingProto.LENS_FACING_BACK) + assertThat(LensFacingProto.LENS_FACING_BACK.toModel()).isEqualTo(LensFacing.BACK) + + assertThat(LensFacingProto.UNRECOGNIZED.toModel()).isEqualTo(LensFacing.BACK) + } + + @Test + fun lowLightBoostPriorityMapsCorrectly() { + assertThat( + LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES.toProto() + ).isEqualTo(LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES) + assertThat( + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_GOOGLE_PLAY_SERVICES.toModel() + ).isEqualTo(LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES) + + assertThat( + LowLightBoostPriority.PRIORITIZE_AE_MODE.toProto() + ).isEqualTo(LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE) + assertThat( + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_AE_MODE.toModel() + ).isEqualTo(LowLightBoostPriority.PRIORITIZE_AE_MODE) + + assertThat( + LowLightBoostPriorityProto.UNRECOGNIZED.toModel() + ).isEqualTo(LowLightBoostPriority.PRIORITIZE_AE_MODE) + } + + @Test + fun stabilizationModeMapsCorrectly() { + assertThat( + StabilizationMode.ON.toProto() + ).isEqualTo(StabilizationModeProto.STABILIZATION_MODE_ON) + assertThat( + StabilizationModeProto.STABILIZATION_MODE_ON.toModel() + ).isEqualTo(StabilizationMode.ON) + + assertThat( + StabilizationMode.OFF.toProto() + ).isEqualTo(StabilizationModeProto.STABILIZATION_MODE_OFF) + assertThat( + StabilizationModeProto.STABILIZATION_MODE_OFF.toModel() + ).isEqualTo(StabilizationMode.OFF) + + assertThat( + StabilizationMode.HIGH_QUALITY.toProto() + ).isEqualTo(StabilizationModeProto.STABILIZATION_MODE_HIGH_QUALITY) + assertThat( + StabilizationModeProto.STABILIZATION_MODE_HIGH_QUALITY.toModel() + ).isEqualTo(StabilizationMode.HIGH_QUALITY) + + assertThat( + StabilizationMode.OPTICAL.toProto() + ).isEqualTo(StabilizationModeProto.STABILIZATION_MODE_OPTICAL) + assertThat( + StabilizationModeProto.STABILIZATION_MODE_OPTICAL.toModel() + ).isEqualTo(StabilizationMode.OPTICAL) + + assertThat( + StabilizationMode.AUTO.toProto() + ).isEqualTo(StabilizationModeProto.STABILIZATION_MODE_AUTO) + assertThat( + StabilizationModeProto.STABILIZATION_MODE_AUTO.toModel() + ).isEqualTo(StabilizationMode.AUTO) + + assertThat(StabilizationModeProto.UNRECOGNIZED.toModel()).isEqualTo(StabilizationMode.AUTO) + } + + @Test + fun videoQualityMapsCorrectly() { + assertThat(VideoQuality.SD.toProto()).isEqualTo(VideoQualityProto.VIDEO_QUALITY_SD) + assertThat(VideoQualityProto.VIDEO_QUALITY_SD.toModel()).isEqualTo(VideoQuality.SD) + + assertThat(VideoQuality.HD.toProto()).isEqualTo(VideoQualityProto.VIDEO_QUALITY_HD) + assertThat(VideoQualityProto.VIDEO_QUALITY_HD.toModel()).isEqualTo(VideoQuality.HD) + + assertThat(VideoQuality.FHD.toProto()).isEqualTo(VideoQualityProto.VIDEO_QUALITY_FHD) + assertThat(VideoQualityProto.VIDEO_QUALITY_FHD.toModel()).isEqualTo(VideoQuality.FHD) + + assertThat(VideoQuality.UHD.toProto()).isEqualTo(VideoQualityProto.VIDEO_QUALITY_UHD) + assertThat(VideoQualityProto.VIDEO_QUALITY_UHD.toModel()).isEqualTo(VideoQuality.UHD) + + assertThat( + VideoQuality.UNSPECIFIED.toProto() + ).isEqualTo(VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED) + assertThat( + VideoQualityProto.VIDEO_QUALITY_UNSPECIFIED.toModel() + ).isEqualTo(VideoQuality.UNSPECIFIED) + + assertThat(VideoQualityProto.UNRECOGNIZED.toModel()).isEqualTo(VideoQuality.UNSPECIFIED) + } + + @Test + fun concurrentCameraModeMapsCorrectly() { + assertThat( + ConcurrentCameraMode.DUAL.toProto() + ).isEqualTo(ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL) + assertThat( + ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_DUAL.toModel() + ).isEqualTo(ConcurrentCameraMode.DUAL) + + assertThat( + ConcurrentCameraMode.OFF.toProto() + ).isEqualTo(ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_OFF) + assertThat( + ConcurrentCameraModeProto.CONCURRENT_CAMERA_MODE_OFF.toModel() + ).isEqualTo(ConcurrentCameraMode.OFF) + + assertThat( + ConcurrentCameraModeProto.UNRECOGNIZED.toModel() + ).isEqualTo(ConcurrentCameraMode.OFF) + } +} diff --git a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt index c81b9cea5..8014d0279 100644 --- a/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt +++ b/core/settings/datastore-prefs/src/main/java/com/google/jetpackcamera/core/settings/datastoreprefs/PrefsDataStoreSettingsDataSource.kt @@ -36,6 +36,9 @@ import com.google.jetpackcamera.model.UNLIMITED_VIDEO_DURATION import com.google.jetpackcamera.model.VideoQuality import com.google.jetpackcamera.settings.SettingsDataSource import com.google.jetpackcamera.settings.model.CameraAppSettings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -167,6 +170,36 @@ class PrefsDataStoreSettingsDataSource( } companion object { + /** + * Creates an instance of [SettingsDataSource] backed by Preferences DataStore. + * + * Note: To avoid breaking DataStore functionality, ensure that only a single instance + * of [DataStore] is active for the settings file at any time (e.g., by managing this + * instance as a Singleton via dependency injection). + * + * @param context The application context. + * @param defaultCaptureModeOverride The default capture mode override. + * @param ioDispatcher The coroutine dispatcher for IO operations. + * @return A [SettingsDataSource] instance. + */ + fun create( + context: android.content.Context, + defaultCaptureModeOverride: CaptureMode, + ioDispatcher: CoroutineDispatcher + ): SettingsDataSource { + val scope = CoroutineScope(ioDispatcher + SupervisorJob()) + val dataStore = androidx.datastore.preferences.core.PreferenceDataStoreFactory.create( + scope = scope, + produceFile = { + java.io.File( + context.filesDir, + "datastore/app_settings.preferences_pb" + ) + } + ) + return PrefsDataStoreSettingsDataSource(dataStore, defaultCaptureModeOverride) + } + private inline fun > String?.toEnumOrDefault(default: T): T { if (this == null) return default return try { diff --git a/core/settings/datastore-proto/build.gradle.kts b/core/settings/datastore-proto/build.gradle.kts new file mode 100644 index 000000000..6bdacdb0b --- /dev/null +++ b/core/settings/datastore-proto/build.gradle.kts @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.google.jetpackcamera.settings.datastore.proto" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + + +dependencies { + implementation(project(":core:settings:settings-proto")) + implementation(libs.androidx.datastore) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.kotlinx.coroutines.test) +} diff --git a/core/settings/datastore-proto/src/main/java/com/google/jetpackcamera/settings/ProtoCameraAppSettingsSerializer.kt b/core/settings/datastore-proto/src/main/java/com/google/jetpackcamera/settings/ProtoCameraAppSettingsSerializer.kt new file mode 100644 index 000000000..527f3c592 --- /dev/null +++ b/core/settings/datastore-proto/src/main/java/com/google/jetpackcamera/settings/ProtoCameraAppSettingsSerializer.kt @@ -0,0 +1,70 @@ +/* + * 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.TARGET_FPS_AUTO +import com.google.jetpackcamera.model.UNLIMITED_VIDEO_DURATION +import com.google.jetpackcamera.model.proto.AspectRatio +import com.google.jetpackcamera.model.proto.ConcurrentCameraMode +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.LowLightBoostPriority as LowLightBoostPriorityProto +import com.google.jetpackcamera.model.proto.StabilizationMode +import com.google.jetpackcamera.model.proto.VideoQuality +import com.google.jetpackcamera.settings.proto.CameraAppSettings as CameraAppSettingsProto +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +/** + * Serializer for the [CameraAppSettingsProto] DataStore. + */ +internal object ProtoCameraAppSettingsSerializer : Serializer { + + override val defaultValue: CameraAppSettingsProto = CameraAppSettingsProto.newBuilder() + .setDarkModeStatus(DarkMode.DARK_MODE_DARK) + .setDefaultLensFacing(LensFacing.LENS_FACING_BACK) + .setFlashModeStatus(FlashMode.FLASH_MODE_OFF) + .setAspectRatioStatus(AspectRatio.ASPECT_RATIO_NINE_SIXTEEN) + .setStabilizationMode(StabilizationMode.STABILIZATION_MODE_AUTO) + .setDynamicRangeStatus(DynamicRange.DYNAMIC_RANGE_UNSPECIFIED) + .setImageFormatStatus(ImageOutputFormat.IMAGE_OUTPUT_FORMAT_JPEG) + .setMaxVideoDurationMillis(UNLIMITED_VIDEO_DURATION) + .setVideoQuality(VideoQuality.VIDEO_QUALITY_UNSPECIFIED) + .setAudioEnabledStatus(true) + .setConcurrentCameraModeStatus(ConcurrentCameraMode.CONCURRENT_CAMERA_MODE_OFF) + .setTargetFrameRate(TARGET_FPS_AUTO) + .setLowLightBoostPriority( + LowLightBoostPriorityProto.LOW_LIGHT_BOOST_PRIORITY_UNSPECIFIED + ) + .build() + + override suspend fun readFrom(input: InputStream): CameraAppSettingsProto { + try { + return CameraAppSettingsProto.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: CameraAppSettingsProto, output: OutputStream) = + t.writeTo(output) +} diff --git a/core/settings/datastore-proto/src/main/java/com/google/jetpackcamera/settings/ProtoDataStoreSettingsDataSource.kt b/core/settings/datastore-proto/src/main/java/com/google/jetpackcamera/settings/ProtoDataStoreSettingsDataSource.kt new file mode 100644 index 000000000..bb8d07d1a --- /dev/null +++ b/core/settings/datastore-proto/src/main/java/com/google/jetpackcamera/settings/ProtoDataStoreSettingsDataSource.kt @@ -0,0 +1,209 @@ +/* + * 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 android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CameraEffectId +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DarkMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.LowLightBoostPriority +import com.google.jetpackcamera.model.StabilizationMode +import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.model.proto.toProto +import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.proto.CameraAppSettings as CameraAppSettingsProto +import java.io.File +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +/** + * Settings data source using Proto DataStore. + */ +class ProtoDataStoreSettingsDataSource( + private val jcaSettings: DataStore, + private val defaultCaptureModeOverride: CaptureMode +) : SettingsDataSource { + + private val jcaSettingsFlow: Flow = + jcaSettings.data.catch { exception -> + if (exception is java.io.IOException) { + emit(CameraAppSettingsProto.getDefaultInstance()) + } else { + throw exception + } + } + + override val defaultCameraAppSettings: Flow = jcaSettingsFlow.map { + it.toModel(defaultCaptureModeOverride) + } + + override suspend fun getCurrentDefaultCameraAppSettings(): CameraAppSettings = + jcaSettingsFlow.first().toModel(defaultCaptureModeOverride) + + override suspend fun updateDefaultLensFacing(lensFacing: LensFacing) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setDefaultLensFacing(lensFacing.toProto()) + .build() + } + } + + override suspend fun updateDarkModeStatus(darkMode: DarkMode) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setDarkModeStatus(darkMode.toProto()) + .build() + } + } + + override suspend fun updateFlashModeStatus(flashMode: FlashMode) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setFlashModeStatus(flashMode.toProto()) + .build() + } + } + + override suspend fun updateAspectRatio(aspectRatio: AspectRatio) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setAspectRatioStatus(aspectRatio.toProto()) + .build() + } + } + + override suspend fun updateSelectedCameraEffect(selectedCameraEffect: CameraEffectId) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setSelectedCameraEffect(selectedCameraEffect.value) + .build() + } + } + + override suspend fun updateLowLightBoostPriority(lowLightBoostPriority: LowLightBoostPriority) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setLowLightBoostPriority(lowLightBoostPriority.toProto()) + .build() + } + } + + override suspend fun updateStabilizationMode(stabilizationMode: StabilizationMode) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setStabilizationMode(stabilizationMode.toProto()) + .build() + } + } + + override suspend fun updateDynamicRange(dynamicRange: DynamicRange) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setDynamicRangeStatus(dynamicRange.toProto()) + .build() + } + } + + override suspend fun updateTargetFrameRate(targetFrameRate: Int) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setTargetFrameRate(targetFrameRate) + .build() + } + } + + override suspend fun updateImageFormat(imageFormat: ImageOutputFormat) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setImageFormatStatus(imageFormat.toProto()) + .build() + } + } + + override suspend fun updateMaxVideoDuration(durationMillis: Long) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setMaxVideoDurationMillis(durationMillis) + .build() + } + } + + override suspend fun updateVideoQuality(videoQuality: VideoQuality) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setVideoQuality(videoQuality.toProto()) + .build() + } + } + + override suspend fun updateAudioEnabled(isAudioEnabled: Boolean) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setAudioEnabledStatus(isAudioEnabled) + .build() + } + } + + override suspend fun updateConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) { + jcaSettings.updateData { currentSettings -> + currentSettings.toBuilder() + .setConcurrentCameraModeStatus(concurrentCameraMode.toProto()) + .build() + } + } + + companion object { + private const val FILE_LOCATION = "CameraAppSettings.pb" + + /** + * Creates an instance of [SettingsDataSource] backed by Proto DataStore. + * + * Note: To avoid breaking DataStore functionality, ensure that only a single instance + * of [DataStore] is active for the settings file at any time (e.g., by managing this + * instance as a Singleton via dependency injection). + * + * @param context The application context. + * @param ioDispatcher The coroutine dispatcher for IO operations. + * @return A [SettingsDataSource] instance. + */ + fun create( + context: Context, + defaultCaptureModeOverride: CaptureMode, + ioDispatcher: CoroutineDispatcher + ): SettingsDataSource { + val scope = CoroutineScope(ioDispatcher + SupervisorJob()) + val dataStore = DataStoreFactory.create( + serializer = ProtoCameraAppSettingsSerializer, + scope = scope, + produceFile = { File(context.filesDir, "datastore/$FILE_LOCATION") } + ) + return ProtoDataStoreSettingsDataSource(dataStore, defaultCaptureModeOverride) + } + } +} diff --git a/core/settings/datastore-proto/src/test/java/com/google/jetpackcamera/settings/ProtoDataStoreSettingsDataSourceTest.kt b/core/settings/datastore-proto/src/test/java/com/google/jetpackcamera/settings/ProtoDataStoreSettingsDataSourceTest.kt new file mode 100644 index 000000000..6b7cb685b --- /dev/null +++ b/core/settings/datastore-proto/src/test/java/com/google/jetpackcamera/settings/ProtoDataStoreSettingsDataSourceTest.kt @@ -0,0 +1,241 @@ +/* + * 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 androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import com.google.common.truth.Truth.assertThat +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.VideoQuality +import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS +import com.google.jetpackcamera.settings.proto.CameraAppSettings as CameraAppSettingsProto +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.resetMain +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 +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class ProtoDataStoreSettingsDataSourceTest { + + @get:Rule + val tempFolder = TemporaryFolder() + + private lateinit var testDataStore: DataStore + private lateinit var datastoreScope: CoroutineScope + private lateinit var repository: ProtoDataStoreSettingsDataSource + + @Before + fun setup() { + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + + testDataStore = DataStoreFactory.create( + serializer = ProtoCameraAppSettingsSerializer, + scope = datastoreScope + ) { + java.io.File(tempFolder.root, "test_jca_settings.pb") + } + repository = ProtoDataStoreSettingsDataSource( + jcaSettings = testDataStore, + defaultCaptureModeOverride = CaptureMode.STANDARD + ) + } + + @After + fun tearDown() { + datastoreScope.cancel() + Dispatchers.resetMain() + } + + @Test + fun repository_can_fetch_initial_datastore() = runTest { + val cameraAppSettings: CameraAppSettings = repository.getCurrentDefaultCameraAppSettings() + + advanceUntilIdle() + assertThat(cameraAppSettings).isEqualTo(DEFAULT_CAMERA_APP_SETTINGS) + } + + @Test + fun can_update_dark_mode() = runTest { + val initialDarkModeStatus = repository.getCurrentDefaultCameraAppSettings().darkMode + repository.updateDarkModeStatus(DarkMode.LIGHT) + val newDarkModeStatus = repository.getCurrentDefaultCameraAppSettings().darkMode + + advanceUntilIdle() + assertThat(initialDarkModeStatus).isEqualTo(DarkMode.DARK) + assertThat(newDarkModeStatus).isEqualTo(DarkMode.LIGHT) + } + + @Test + fun can_update_default_to_front_camera() = runTest { + val initialDefaultLensFacing = + repository.getCurrentDefaultCameraAppSettings().cameraLensFacing + repository.updateDefaultLensFacing(LensFacing.FRONT) + val newDefaultLensFacing = repository.getCurrentDefaultCameraAppSettings().cameraLensFacing + advanceUntilIdle() + + assertThat(initialDefaultLensFacing).isEqualTo(LensFacing.BACK) + assertThat(newDefaultLensFacing).isEqualTo(LensFacing.FRONT) + } + + @Test + fun can_update_flash_mode() = runTest { + val initialFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode + repository.updateFlashModeStatus(FlashMode.ON) + val newFlashModeStatus = repository.getCurrentDefaultCameraAppSettings().flashMode + advanceUntilIdle() + + assertThat(initialFlashModeStatus).isEqualTo(FlashMode.OFF) + assertThat(newFlashModeStatus).isEqualTo(FlashMode.ON) + } + + @Test + fun can_update_dynamic_range() = runTest { + val initialDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange + repository.updateDynamicRange(dynamicRange = DynamicRange.HLG10) + advanceUntilIdle() + val newDynamicRange = repository.getCurrentDefaultCameraAppSettings().dynamicRange + + assertThat(initialDynamicRange).isEqualTo(DynamicRange.SDR) + assertThat(newDynamicRange).isEqualTo(DynamicRange.HLG10) + } + + @Test + fun can_update_image_format() = runTest { + val initialImageFormat = repository.getCurrentDefaultCameraAppSettings().imageFormat + repository.updateImageFormat(imageFormat = ImageOutputFormat.JPEG_ULTRA_HDR) + advanceUntilIdle() + val newImageFormat = repository.getCurrentDefaultCameraAppSettings().imageFormat + + assertThat(initialImageFormat).isEqualTo(ImageOutputFormat.JPEG) + assertThat(newImageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR) + } + + @Test + fun can_update_aspect_ratio() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().aspectRatio + repository.updateAspectRatio(AspectRatio.ONE_ONE) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().aspectRatio + + assertThat(initial).isEqualTo(AspectRatio.NINE_SIXTEEN) + assertThat(new).isEqualTo(AspectRatio.ONE_ONE) + } + + @Test + fun can_update_low_light_boost_priority() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().lowLightBoostPriority + repository.updateLowLightBoostPriority( + LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES + ) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().lowLightBoostPriority + + assertThat(initial).isEqualTo(LowLightBoostPriority.PRIORITIZE_AE_MODE) + assertThat(new).isEqualTo(LowLightBoostPriority.PRIORITIZE_GOOGLE_PLAY_SERVICES) + } + + @Test + fun can_update_stabilization_mode() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().stabilizationMode + repository.updateStabilizationMode(StabilizationMode.ON) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().stabilizationMode + + assertThat(initial).isEqualTo(StabilizationMode.AUTO) + assertThat(new).isEqualTo(StabilizationMode.ON) + } + + @Test + fun can_update_target_frame_rate() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().targetFrameRate + repository.updateTargetFrameRate(60) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().targetFrameRate + + assertThat(initial).isEqualTo(DEFAULT_CAMERA_APP_SETTINGS.targetFrameRate) + assertThat(new).isEqualTo(60) + } + + @Test + fun can_update_max_video_duration() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().maxVideoDurationMillis + repository.updateMaxVideoDuration(10000L) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().maxVideoDurationMillis + + assertThat(initial).isEqualTo(0L) // UNLIMITED_VIDEO_DURATION is 0L + assertThat(new).isEqualTo(10000L) + } + + @Test + fun can_update_video_quality() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().videoQuality + repository.updateVideoQuality(VideoQuality.FHD) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().videoQuality + + assertThat(initial).isEqualTo(VideoQuality.UNSPECIFIED) + assertThat(new).isEqualTo(VideoQuality.FHD) + } + + @Test + fun can_update_audio_enabled() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().audioEnabled + repository.updateAudioEnabled(false) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().audioEnabled + + assertThat(initial).isTrue() + assertThat(new).isFalse() + } + + @Test + fun can_update_concurrent_camera_mode() = runTest { + val initial = repository.getCurrentDefaultCameraAppSettings().concurrentCameraMode + repository.updateConcurrentCameraMode( + com.google.jetpackcamera.model.ConcurrentCameraMode.DUAL + ) + advanceUntilIdle() + val new = repository.getCurrentDefaultCameraAppSettings().concurrentCameraMode + + assertThat(initial).isEqualTo(com.google.jetpackcamera.model.ConcurrentCameraMode.OFF) + assertThat(new).isEqualTo(com.google.jetpackcamera.model.ConcurrentCameraMode.DUAL) + } +} diff --git a/core/settings/settings-proto/build.gradle.kts b/core/settings/settings-proto/build.gradle.kts new file mode 100644 index 000000000..4c2f75846 --- /dev/null +++ b/core/settings/settings-proto/build.gradle.kts @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.protobuf) +} + +android { + namespace = "com.google.jetpackcamera.settings.proto" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + testOptions.targetSdk = libs.versions.targetSdk.get().toInt() + lint.targetSdk = libs.versions.targetSdk.get().toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + flavorDimensions += "flavor" + productFlavors { + create("stable") { + dimension = "flavor" + isDefault = true + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain(17) + } +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.21.12" + } + generateProtoTasks { + all().forEach { task -> + task.builtins { + create("java") { + option("lite") + } + create("kotlin") { + option("lite") + } + } + } + } +} + +dependencies { + api(project(":core:settings")) + api(project(":core:model-proto")) + api(libs.protobuf.kotlin.lite) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.truth) + testImplementation(libs.kotlinx.coroutines.test) +} diff --git a/core/settings/settings-proto/src/main/java/com/google/jetpackcamera/settings/SettingsMappers.kt b/core/settings/settings-proto/src/main/java/com/google/jetpackcamera/settings/SettingsMappers.kt new file mode 100644 index 000000000..fdd46fb5b --- /dev/null +++ b/core/settings/settings-proto/src/main/java/com/google/jetpackcamera/settings/SettingsMappers.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.jetpackcamera.settings + +import com.google.jetpackcamera.model.CameraEffectId +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.NONE_EFFECT_ID +import com.google.jetpackcamera.model.proto.toModel +import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.settings.proto.CameraAppSettings as CameraAppSettingsProto + +/** + * Maps a [CameraAppSettingsProto] to a [CameraAppSettings] domain model. + */ +fun CameraAppSettingsProto.toModel(defaultCaptureModeOverride: CaptureMode): CameraAppSettings { + return CameraAppSettings( + captureMode = defaultCaptureModeOverride, + selectedCameraEffect = if (this.selectedCameraEffect.isEmpty()) { + NONE_EFFECT_ID + } else { + CameraEffectId(this.selectedCameraEffect) + }, + + cameraLensFacing = this.defaultLensFacing.toModel(), + flashMode = this.flashModeStatus.toModel(), + targetFrameRate = this.targetFrameRate, + aspectRatio = this.aspectRatioStatus.toModel(), + stabilizationMode = this.stabilizationMode.toModel(), + dynamicRange = this.dynamicRangeStatus.toModel(), + imageFormat = this.imageFormatStatus.toModel(), + maxVideoDurationMillis = this.maxVideoDurationMillis, + videoQuality = this.videoQuality.toModel(), + audioEnabled = this.audioEnabledStatus, + lowLightBoostPriority = this.lowLightBoostPriority.toModel(), + darkMode = this.darkModeStatus.toModel(), + concurrentCameraMode = this.concurrentCameraModeStatus.toModel() + ) +} diff --git a/core/settings/settings-proto/src/main/proto/com/google/jetpackcamera/settings/camera_app_settings.proto b/core/settings/settings-proto/src/main/proto/com/google/jetpackcamera/settings/camera_app_settings.proto new file mode 100644 index 000000000..9400cda6d --- /dev/null +++ b/core/settings/settings-proto/src/main/proto/com/google/jetpackcamera/settings/camera_app_settings.proto @@ -0,0 +1,52 @@ +/* + * 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/dark_mode.proto"; +import "com/google/jetpackcamera/model/proto/dynamic_range.proto"; +import "com/google/jetpackcamera/model/proto/flash_mode.proto"; +import "com/google/jetpackcamera/model/proto/image_output_format.proto"; +import "com/google/jetpackcamera/model/proto/lens_facing.proto"; +import "com/google/jetpackcamera/model/proto/stabilization_mode.proto"; +import "com/google/jetpackcamera/model/proto/video_quality.proto"; +import "com/google/jetpackcamera/model/proto/low_light_boost_priority.proto"; +import "com/google/jetpackcamera/model/proto/concurrent_camera_mode.proto"; + + +option java_package = "com.google.jetpackcamera.settings.proto"; +option java_multiple_files = true; + +message CameraAppSettings { + // Camera settings + LensFacing default_lens_facing = 1; + FlashMode flash_mode_status = 2; + int32 target_frame_rate = 3; + AspectRatio aspect_ratio_status = 4; + 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; + ConcurrentCameraMode concurrent_camera_mode_status = 15; + string selected_camera_effect = 16; +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6094f3ab2..3d1196af4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ accompanist = "0.37.3" # See https://developer.android.com/jetpack/androidx/releases/compose-kotlin kotlinPlugin = "2.2.0" androidGradlePlugin = "8.10.1" - +protobufPlugin = "0.9.5" androidxActivityCompose = "1.10.1" androidxAppCompat = "1.7.1" @@ -25,7 +25,6 @@ androidxCamera = "1.5.0-SNAPSHOT" androidxConcurrentFutures = "1.3.0" androidxCoreKtx = "1.16.0" androidxDatastore = "1.1.7" - androidxGraphicsCore = "1.0.3" androidxHiltNavigationCompose = "1.3.0" androidxLifecycle = "2.9.2" @@ -45,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" @@ -64,7 +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-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidxDatastore" } - +androidx-datastore = { module = "androidx.datastore:datastore", 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" } @@ -107,7 +106,7 @@ kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-cor play-services-camera-low-light-boost = { module = "com.google.android.gms:play-services-camera-low-light-boost", version.ref = "cameraLowLightBoost" } play-services-tasks = { module = "com.google.android.gms:play-services-tasks", version.ref = "playServicesTasks" } testParameterInjector = { group = "com.google.testparameterinjector", name = "test-parameter-injector", version.ref = "testParameterInjector" } - +protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } @@ -121,6 +120,6 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlinPlugin" } dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } - +protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinPlugin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlinPlugin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d6b51e3b..b2b7f7921 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,3 +70,6 @@ include(":core:settings:datastore-prefs") include(":core:settings:datastore-prefs:testing") include(":ui:debug") include(":ui:debug:testing") +include(":core:settings:datastore-proto") +include(":core:model-proto") +include(":core:settings:settings-proto") diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt index 4d2ba8efb..53e2b403b 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/DisabledReason.kt @@ -21,7 +21,7 @@ import com.google.jetpackcamera.ui.uistate.DisableRationale * Represents reasons why a UI component or functionality might be disabled, providing * a string resource ID for user-facing explanation. */ -enum class DisabledReason( +internal enum class DisabledReason( override val reasonTextResId: Int ) : DisableRationale { VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED(