Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.google.jetpackcamera.ui.components.capture.GRID_OVERLAY
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_GRID_BUTTON
import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS
import com.google.jetpackcamera.utils.runMainActivityScenarioTest
import com.google.jetpackcamera.utils.visitQuickSettings
import com.google.jetpackcamera.utils.waitForCaptureButton
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PreviewScreenTest {

@get:Rule
val permissionsRule: GrantPermissionRule =
GrantPermissionRule.grant(*(TEST_REQUIRED_PERMISSIONS).toTypedArray())

@get:Rule
val composeTestRule = createEmptyComposeRule()

@Test
fun can_toggle_grid_on_and_off() = runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
composeTestRule.visitQuickSettings(QUICK_SETTINGS_GRID_BUTTON) {
// Click the grid button to turn it on
onNodeWithTag(QUICK_SETTINGS_GRID_BUTTON).performClick()
}

// Verify the grid is displayed
composeTestRule.onNodeWithTag(GRID_OVERLAY).assertIsDisplayed()

composeTestRule.visitQuickSettings(QUICK_SETTINGS_GRID_BUTTON) {
// Click the grid button to turn it off
onNodeWithTag(QUICK_SETTINGS_GRID_BUTTON).performClick()
}
// Verify the grid is not displayed
composeTestRule.onNodeWithTag(GRID_OVERLAY).assertDoesNotExist()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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

import com.google.jetpackcamera.model.proto.GridTypeProto

enum class GridType {
NONE,
RULE_OF_THIRDS;

companion object {
fun GridType.toProto(): GridTypeProto {
return when (this) {
NONE -> GridTypeProto.GRID_TYPE_PROTO_NONE
RULE_OF_THIRDS -> GridTypeProto.GRID_TYPE_PROTO_RULE_OF_THIRDS
}
}

fun fromProto(proto: GridTypeProto): GridType {
return when (proto) {
GridTypeProto.GRID_TYPE_PROTO_NONE -> NONE
GridTypeProto.GRID_TYPE_PROTO_RULE_OF_THIRDS -> RULE_OF_THIRDS
else -> NONE
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 GridTypeProto {
GRID_TYPE_PROTO_NONE = 0;
GRID_TYPE_PROTO_RULE_OF_THIRDS = 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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.GridType
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.LensFacing
import com.google.jetpackcamera.settings.DataStoreModule.provideDataStore
Expand Down Expand Up @@ -166,4 +167,16 @@ class LocalSettingsRepositoryInstrumentedTest {
assertThat(initialImageFormat).isEqualTo(ImageOutputFormat.JPEG)
assertThat(newImageFormat).isEqualTo(ImageOutputFormat.JPEG_ULTRA_HDR)
}

@Test
fun can_update_grid_type() = runTest {
val initialGridType = repository.getCurrentDefaultCameraAppSettings().gridType
repository.updateGridType(GridType.RULE_OF_THIRDS)
val newGridType = repository.getCurrentDefaultCameraAppSettings().gridType

advanceUntilIdle()
assertThat(initialGridType).isNotEqualTo(newGridType)
assertThat(initialGridType).isEqualTo(GridType.NONE)
assertThat(newGridType).isEqualTo(GridType.RULE_OF_THIRDS)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import com.google.jetpackcamera.model.DarkMode
import com.google.jetpackcamera.model.DynamicRange
import com.google.jetpackcamera.model.DynamicRange.Companion.toProto
import com.google.jetpackcamera.model.FlashMode
import com.google.jetpackcamera.model.GridType
import com.google.jetpackcamera.model.GridType.Companion.fromProto
import com.google.jetpackcamera.model.GridType.Companion.toProto
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.ImageOutputFormat.Companion.toProto
import com.google.jetpackcamera.model.LensFacing
Expand Down Expand Up @@ -84,6 +87,7 @@ class LocalSettingsRepository @Inject constructor(
maxVideoDurationMillis = it.maxVideoDurationMillis,
videoQuality = VideoQuality.fromProto(it.videoQuality),
audioEnabled = it.audioEnabledStatus,
gridType = fromProto(it.gridType),
captureMode = defaultCaptureModeOverride
)
}
Expand Down Expand Up @@ -220,4 +224,12 @@ class LocalSettingsRepository @Inject constructor(
.build()
}
}

override suspend fun updateGridType(gridType: GridType) {
jcaSettings.updateData { currentSettings ->
currentSettings.toBuilder()
.setGridType(gridType.toProto())
.build()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.google.jetpackcamera.model.AspectRatio
import com.google.jetpackcamera.model.DarkMode
import com.google.jetpackcamera.model.DynamicRange
import com.google.jetpackcamera.model.FlashMode
import com.google.jetpackcamera.model.GridType
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.LensFacing
import com.google.jetpackcamera.model.LowLightBoostPriority
Expand Down Expand Up @@ -62,4 +63,6 @@ interface SettingsRepository {
suspend fun updateVideoQuality(videoQuality: VideoQuality)

suspend fun updateAudioEnabled(isAudioEnabled: Boolean)

suspend fun updateGridType(gridType: GridType)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.google.jetpackcamera.model.DynamicRange
import com.google.jetpackcamera.model.ExternalCaptureMode
import com.google.jetpackcamera.model.ExternalCaptureMode.Companion.toCaptureMode
import com.google.jetpackcamera.model.FlashMode
import com.google.jetpackcamera.model.GridType
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.LensFacing
import com.google.jetpackcamera.model.LowLightBoostPriority
Expand Down Expand Up @@ -56,6 +57,7 @@ data class CameraAppSettings(
val concurrentCameraMode: ConcurrentCameraMode = ConcurrentCameraMode.OFF,
val maxVideoDurationMillis: Long = UNLIMITED_VIDEO_DURATION,
val lowLightBoostPriority: LowLightBoostPriority = LowLightBoostPriority.PRIORITIZE_AE_MODE,
val gridType: GridType = GridType.NONE,
val debugSettings: DebugSettings = DebugSettings()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.google.jetpackcamera.model.AspectRatio
import com.google.jetpackcamera.model.DarkMode
import com.google.jetpackcamera.model.DynamicRange
import com.google.jetpackcamera.model.FlashMode
import com.google.jetpackcamera.model.GridType
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.LensFacing
import com.google.jetpackcamera.model.LowLightBoostPriority
Expand Down Expand Up @@ -98,4 +99,9 @@ object FakeSettingsRepository : SettingsRepository {
currentCameraSettings =
currentCameraSettings.copy(audioEnabled = isAudioEnabled)
}

override suspend fun updateGridType(gridType: GridType) {
currentCameraSettings =
currentCameraSettings.copy(gridType = gridType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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/grid_type.proto";


option java_package = "com.google.jetpackcamera.settings";
Expand All @@ -45,6 +46,7 @@ message JcaSettings {
VideoQuality video_quality = 12;
bool audio_enabled_status = 13;
LowLightBoostPriority low_light_boost_priority = 14;
GridTypeProto grid_type = 15;

// Non-camera app settings
DarkMode dark_mode_status = 9;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ package com.google.jetpackcamera.settings
import com.google.common.truth.Truth.assertThat
import com.google.jetpackcamera.model.DynamicRange
import com.google.jetpackcamera.model.DynamicRange.Companion.toProto
import com.google.jetpackcamera.model.GridType
import com.google.jetpackcamera.model.GridType.Companion.toProto
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.ImageOutputFormat.Companion.toProto
import com.google.jetpackcamera.model.proto.DynamicRange as DynamicRangeProto
import com.google.jetpackcamera.model.proto.GridTypeProto
import com.google.jetpackcamera.model.proto.ImageOutputFormat as ImageOutputFormatProto
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -107,4 +110,34 @@ class ProtoConversionTest {
assertThat(correctConversions(it)).isEqualTo(ImageOutputFormat.fromProto(it))
}
}

@Test
fun gridType_convertsToCorrectProto() {
val correctConversions = { gridType: GridType ->
when (gridType) {
GridType.NONE -> GridTypeProto.GRID_TYPE_PROTO_NONE
GridType.RULE_OF_THIRDS -> GridTypeProto.GRID_TYPE_PROTO_RULE_OF_THIRDS
}
}

enumValues<GridType>().forEach {
assertThat(correctConversions(it)).isEqualTo(it.toProto())
}
}

@Test
fun gridTypeProto_convertsToCorrectGridType() {
val correctConversions = { gridTypeProto: GridTypeProto ->
when (gridTypeProto) {
GridTypeProto.GRID_TYPE_PROTO_NONE,
GridTypeProto.UNRECOGNIZED
-> GridType.NONE
GridTypeProto.GRID_TYPE_PROTO_RULE_OF_THIRDS -> GridType.RULE_OF_THIRDS
}
}

enumValues<GridTypeProto>().forEach {
assertThat(correctConversions(it)).isEqualTo(GridType.fromProto(it))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import com.google.jetpackcamera.model.ConcurrentCameraMode
import com.google.jetpackcamera.model.DynamicRange
import com.google.jetpackcamera.model.ExternalCaptureMode
import com.google.jetpackcamera.model.FlashMode
import com.google.jetpackcamera.model.GridType
import com.google.jetpackcamera.model.ImageCaptureEvent
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.LensFacing
Expand Down Expand Up @@ -335,6 +336,7 @@ fun PreviewScreen(
onStartVideoRecording = viewModel::startVideoRecording,
onStopVideoRecording = viewModel::stopVideoRecording,
onLockVideoRecording = viewModel::setLockedRecording,
onUpdateGridType = viewModel::updateGridType,
onRequestWindowColorMode = onRequestWindowColorMode,
onSnackBarResult = viewModel::onSnackBarResult,
onNavigatePostCapture = onNavigateToPostCapture,
Expand Down Expand Up @@ -391,6 +393,7 @@ private fun ContentScreen(
onStartVideoRecording: () -> Unit = {},
onStopVideoRecording: () -> Unit = {},
onLockVideoRecording: (Boolean) -> Unit = {},
onUpdateGridType: (GridType) -> Unit = {},
onRequestWindowColorMode: (Int) -> Unit = {},
onSnackBarResult: (String) -> Unit = {},
onNavigatePostCapture: () -> Unit = {},
Expand All @@ -417,8 +420,11 @@ private fun ContentScreen(
}
}

val debugHidingComponents =
(debugUiState as? DebugUiState.Enabled)?.debugHidingComponents == true
LayoutWrapper(
modifier = modifier,
debugHidingComponents = debugHidingComponents,
hdrIndicator = { HdrIndicator(modifier = it, hdrUiState = captureUiState.hdrUiState) },
flashModeIndicator = {
FlashModeIndicator(
Expand Down Expand Up @@ -447,7 +453,8 @@ private fun ContentScreen(
onScaleZoom = { onScaleZoom(it, LensToZoom.PRIMARY) },
surfaceRequest = surfaceRequest,
onRequestWindowColorMode = onRequestWindowColorMode,
focusMeteringUiState = captureUiState.focusMeteringUiState
focusMeteringUiState = captureUiState.focusMeteringUiState,
debugHidingComponents = debugHidingComponents
)
},
captureButton = {
Expand Down Expand Up @@ -567,6 +574,7 @@ private fun ContentScreen(
onImageOutputFormatClick = onChangeImageFormat,
onConcurrentCameraModeClick = onChangeConcurrentCameraMode,
onCaptureModeClick = onSetCaptureMode,
onGridClick = onUpdateGridType,
onNavigateToSettings = {
onToggleQuickSettings()
onNavigateToSettings()
Expand Down Expand Up @@ -656,6 +664,7 @@ private fun LoadingScreen(modifier: Modifier = Modifier) {
@Composable
private fun LayoutWrapper(
modifier: Modifier = Modifier,
debugHidingComponents: Boolean,
viewfinder: @Composable (modifier: Modifier) -> Unit,
captureButton: @Composable (modifier: Modifier) -> Unit,
flipCameraButton: @Composable (modifier: Modifier) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import com.google.jetpackcamera.model.DeviceRotation
import com.google.jetpackcamera.model.DynamicRange
import com.google.jetpackcamera.model.ExternalCaptureMode
import com.google.jetpackcamera.model.FlashMode
import com.google.jetpackcamera.model.GridType
import com.google.jetpackcamera.model.ImageCaptureEvent
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.model.IntProgress
Expand Down Expand Up @@ -182,6 +183,9 @@ class PreviewViewModel @Inject constructor(

init {
viewModelScope.launch {
val initialGridType =
settingsRepository.getCurrentDefaultCameraAppSettings().gridType
trackedCaptureUiState.update { it.copy(gridType = initialGridType) }
launch {
var oldCameraAppSettings: CameraAppSettings? = null
settingsRepository.defaultCameraAppSettings
Expand Down Expand Up @@ -694,4 +698,11 @@ class PreviewViewModel @Inject constructor(
cameraSystem.setDeviceRotation(deviceRotation)
}
}

fun updateGridType(gridType: GridType) {
trackedCaptureUiState.update { it.copy(gridType = gridType) }
viewModelScope.launch {
settingsRepository.updateGridType(gridType)
}
}
}
Loading
Loading