Skip to content
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
bf18caa
test(core:camera): track initialized settings in FakeCameraSystem
Kimblebee Sep 4, 2026
f682fb6
test(ui:components): add test tags for restricted capture modes
Kimblebee Sep 4, 2026
0746f80
feat(data:settings): introduce settings API module and restriction co…
Kimblebee Sep 4, 2026
3bc4aae
feat(ui:uistateadapter): enforce option restrictions and disabled rat…
Kimblebee Sep 4, 2026
57e7df2
feat(feature:preview): wire developer config into PreviewViewModel an…
Kimblebee Sep 4, 2026
1276240
feat(app): provide DeveloperAppConfig and parse developer config inte…
Kimblebee Sep 4, 2026
866d7a7
feat(data:settings): harden RestrictionConfig with defaults, equality…
Kimblebee Sep 4, 2026
c8573a7
fix(ui:uistateadapter): support nullable appConfig in captureUiState …
Kimblebee Sep 4, 2026
4f56882
fix(feature:preview): preserve un-overridden settings, eliminate with…
Kimblebee Sep 4, 2026
036e4e3
refactor(app, data:settings): remove withoutRestrictions and simplify…
Kimblebee Sep 4, 2026
79d2566
refactor(data:settings, ui:uistateadapter): rename to OptionAvailabil…
Kimblebee Sep 4, 2026
31e6cc3
spotless
Kimblebee Sep 4, 2026
5fe2a47
Merge branch 'main' into kim/developerOptions/hybrid-api
Kimblebee Sep 4, 2026
29bd176
Merge remote-tracking branch 'origin/main' into kim/developerOptions/…
Kimblebee Sep 10, 2026
76df2ba
test(ui:uistateadapter): add captureUiState test for restricted devel…
Kimblebee Sep 10, 2026
94d7bb3
spotless
Kimblebee Sep 10, 2026
2a0f98a
refactor: remove intent extra gating for developer options config
Kimblebee Sep 14, 2026
0480148
refactor(settings): rename DeveloperAppConfig to CameraAppConfig and …
Kimblebee Sep 14, 2026
612f60c
fix(ui): rename restrictionConfig to visibilityConfig, make nullable,…
Kimblebee Sep 14, 2026
25a468f
refactor(settings): consolidate CameraAppConfig into :core:settings a…
Kimblebee Sep 15, 2026
045910d
feat(ui): connect FlashMode, ImageFormat, and DynamicRange visibility…
Kimblebee Sep 15, 2026
a734805
spotless
Kimblebee Sep 15, 2026
49eca43
fix ui visual bug where llb disables all hdr options when "off" shoul…
Kimblebee Sep 15, 2026
00a70dc
feat(camera): wire CameraAppConfig default values into CameraXCameraS…
Kimblebee Sep 15, 2026
520739c
feat(settings): enforce ImageFormat and DynamicRange default constrai…
Kimblebee Sep 16, 2026
3b96ac9
test(camera): verify CameraAppConfig default values applied in reposi…
Kimblebee Sep 16, 2026
f85edf1
test(app): add instrumentation device tests for CameraAppConfig
Kimblebee Sep 16, 2026
e4ab707
refactor(settings): rename OptionAvailabilityConfig to OptionVisibility
Kimblebee Sep 16, 2026
9a26742
docs(settings): update KDocs for CameraAppConfig and OptionVisibility
Kimblebee Sep 16, 2026
dc6afff
refactor: clean up dead code, unused tags, and naming across config a…
Kimblebee Sep 16, 2026
14d9c98
refactor(settings, ui): enforce non-null config bounds and parameter …
Kimblebee Sep 16, 2026
10c0207
test: expand unit and device test coverage for CameraAppConfig and op…
Kimblebee Sep 16, 2026
a480bd7
docs(camera): add TODO to revisit preserving aspect ratio preference …
Kimblebee Sep 16, 2026
07dcd2f
Rename CameraAppConfig to CameraFeaturePolicy
Kimblebee Sep 16, 2026
0861e17
feat(settings): add safe OptionVisibility.from factory methods
Kimblebee Sep 16, 2026
e731302
refactor(settings): integrate CameraFeaturePolicy into settings layer
Kimblebee Sep 17, 2026
7180b6e
feat(ui): support OptionVisibility restrictions for aspect ratio cont…
Kimblebee Sep 17, 2026
cda2b01
Refactor vestigial CameraAppConfig references to CameraFeaturePolicy
Kimblebee Sep 17, 2026
97e8d84
stabilize image capture for default image-only capture mode policy
Kimblebee Sep 17, 2026
c0e8012
docs: add TODOs for hiding quick settings when all rows are unavailable
Kimblebee Sep 17, 2026
add951f
spotless
Kimblebee Sep 17, 2026
59f53fb
rename visibilityConfig param
Kimblebee Sep 17, 2026
20c4756
Address review comments on CameraFeaturePolicy and UI adapters
Kimblebee Sep 18, 2026
6aa7fd6
docs(core:settings), test(ui:uistateadapter): refine OptionVisibility…
Kimblebee Sep 21, 2026
76e2105
feat(feature:preview), refactor(ui:uistateadapter): hide quick settin…
Kimblebee Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ dependencies {
implementation(project(":data:settings"))
implementation(project(":core:settings:datastore-prefs"))
implementation(project(":core:settings"))

implementation(project(":core:model"))
implementation(libs.androidx.datastore.preferences)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import com.google.common.truth.Truth.assertThat
import com.google.common.truth.TruthJUnit.assume
import com.google.jetpackcamera.model.CaptureMode
import com.google.jetpackcamera.model.ConcurrentCameraMode
import com.google.jetpackcamera.model.ImageOutputFormat
import com.google.jetpackcamera.settings.model.CameraFeaturePolicy
import com.google.jetpackcamera.settings.model.OptionVisibility
import com.google.jetpackcamera.settings.model.SettingConfig
import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON
import com.google.jetpackcamera.ui.components.capture.CAPTURE_MODE_TOGGLE_BUTTON
import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_CAPTURE_MODE
Expand All @@ -58,6 +62,7 @@ import com.google.jetpackcamera.utils.waitForCaptureButton
import com.google.jetpackcamera.utils.waitForCaptureModeToggleState
import com.google.jetpackcamera.utils.waitForNodeWithTag
import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -73,6 +78,12 @@ internal class CaptureModeSettingsTest {

private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val uiDevice = UiDevice.getInstance(instrumentation)

@After
fun tearDown() {
AppModule.testCameraFeaturePolicy = null
}

private fun ComposeTestRule.checkCaptureModeSettingState(captureMode: CaptureMode? = null) =
visitQuickSettings {
captureMode?.let {
Expand Down Expand Up @@ -157,11 +168,8 @@ internal class CaptureModeSettingsTest {
// should not be able to switch between capture modes
onNodeWithTag(ROW_QUICK_SETTINGS_CAPTURE_MODE).assertDoesNotExist()
}
// verify switch is disabled and locked on video only
assertThat(composeTestRule.isCaptureModeToggleEnabled()).isFalse()
assertThat(
composeTestRule.getCaptureModeToggleState()
).isEqualTo(CaptureMode.VIDEO_ONLY)
// verify switch is removed when mode switching is not supported
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertDoesNotExist()

// set concurrent camera mode back to off in settings
composeTestRule.setConcurrentCameraModeInSettings(ConcurrentCameraMode.OFF)
Expand Down Expand Up @@ -228,30 +236,36 @@ internal class CaptureModeSettingsTest {

@Test
fun hdr_supports_video_only() {
runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
AppModule.testCameraFeaturePolicy = CameraFeaturePolicy(
imageFormat = SettingConfig(
defaultValue = ImageOutputFormat.JPEG,
visibility = OptionVisibility.Hidden
)
)
try {
runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()

// Switch to VIDEO_ONLY first since STANDARD doesn't support HDR
composeTestRule.setCaptureMode(CaptureMode.VIDEO_ONLY)
composeTestRule.setHdrEnabled(true)
// Switch to VIDEO_ONLY first since STANDARD doesn't support HDR
composeTestRule.setCaptureMode(CaptureMode.VIDEO_ONLY)
composeTestRule.setHdrEnabled(true)

// check that switch is disabled and only supports video
composeTestRule.waitForNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON)
// should not be able use capture toggle
assume().that(composeTestRule.isCaptureModeToggleEnabled()).isFalse()
assume().that(composeTestRule.getCaptureModeToggleState())
.isEqualTo(CaptureMode.VIDEO_ONLY)
// check that switch is removed when mode switching is not supported
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertDoesNotExist()

composeTestRule.visitQuickSettings {
// capture mode should be video only
assertThat(getCurrentCaptureMode()).isEqualTo(CaptureMode.VIDEO_ONLY)
onNodeWithTag(ROW_QUICK_SETTINGS_CAPTURE_MODE).assertDoesNotExist()
}
assertThat(composeTestRule.isCaptureModeToggleEnabled()).isFalse()
composeTestRule.visitQuickSettings {
// capture mode should be video only
assertThat(getCurrentCaptureMode()).isEqualTo(CaptureMode.VIDEO_ONLY)
onNodeWithTag(ROW_QUICK_SETTINGS_CAPTURE_MODE).assertDoesNotExist()
}
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertDoesNotExist()

composeTestRule.setHdrEnabled(false)
// Remains VIDEO_ONLY since we explicitly switched to it
composeTestRule.checkCaptureModeSettingState(CaptureMode.VIDEO_ONLY)
composeTestRule.setHdrEnabled(false)
// Remains VIDEO_ONLY since we explicitly switched to it
composeTestRule.checkCaptureModeSettingState(CaptureMode.VIDEO_ONLY)
}
} finally {
AppModule.testCameraFeaturePolicy = null
}
}

Expand Down Expand Up @@ -326,7 +340,7 @@ internal class CaptureModeSettingsTest {
}

@Test
fun image_intent_disables_capture_mode_toggle() {
fun image_intent_removes_capture_mode_toggle() {
val timeStamp = System.currentTimeMillis()
val uri = getTestUri(PICTURES_DIR_PATH, timeStamp, "jpg")
val result =
Expand All @@ -338,10 +352,7 @@ internal class CaptureModeSettingsTest {
composeTestRule.visitQuickSettings {
checkCaptureModeSettingState(CaptureMode.IMAGE_ONLY)
}
assertThat(composeTestRule.isCaptureModeToggleEnabled()).isFalse()
assertThat(
composeTestRule.getCaptureModeToggleState()
).isEqualTo(CaptureMode.IMAGE_ONLY)
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertDoesNotExist()

uiDevice.pressBack()
}
Expand Down Expand Up @@ -370,7 +381,7 @@ internal class CaptureModeSettingsTest {
}

@Test
fun video_intent_disables_capture_mode_toggle() {
fun video_intent_removes_capture_mode_toggle() {
val timeStamp = System.currentTimeMillis()
val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4")
val result =
Expand All @@ -382,10 +393,7 @@ internal class CaptureModeSettingsTest {
composeTestRule.visitQuickSettings {
checkCaptureModeSettingState(CaptureMode.VIDEO_ONLY)
}
assertThat(composeTestRule.isCaptureModeToggleEnabled()).isFalse()
assertThat(
composeTestRule.getCaptureModeToggleState()
).isEqualTo(CaptureMode.VIDEO_ONLY)
composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertDoesNotExist()

uiDevice.pressBack()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* 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.featurepolicy

import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.google.jetpackcamera.AppModule
import com.google.jetpackcamera.model.AspectRatio
import com.google.jetpackcamera.settings.model.CameraFeaturePolicy
import com.google.jetpackcamera.settings.model.OptionVisibility
import com.google.jetpackcamera.settings.model.SettingConfig
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_3_4_BUTTON
import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_9_16_BUTTON
import com.google.jetpackcamera.ui.components.capture.ROW_QUICK_SETTINGS_ASPECT_RATIO
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.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
internal class AspectRatioFeaturePolicyDeviceTest {

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

@get:Rule
val composeTestRule = createEmptyComposeRule()

@After
fun tearDown() {
AppModule.testCameraFeaturePolicy = null
}

// /////////////////////////////////////////////////////////////////////////
//
// Section A: Default Values
//
// /////////////////////////////////////////////////////////////////////////

@Test
fun defaultAspectRatio_oneOne_startsInOneOne() {
AppModule.testCameraFeaturePolicy = CameraFeaturePolicy(
aspectRatio = SettingConfig(defaultValue = AspectRatio.ONE_ONE)
)

runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
composeTestRule.visitQuickSettings {
onNodeWithTag(QUICK_SETTINGS_RATIO_1_1_BUTTON).assertIsOn()
}
}
}

@Test
fun defaultAspectRatio_threeFour_startsInThreeFour() {
AppModule.testCameraFeaturePolicy = CameraFeaturePolicy(
aspectRatio = SettingConfig(defaultValue = AspectRatio.THREE_FOUR)
)

runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
composeTestRule.visitQuickSettings {
onNodeWithTag(QUICK_SETTINGS_RATIO_3_4_BUTTON).assertIsOn()
}
}
}

@Test
fun defaultAspectRatio_nineSixteen_startsInNineSixteen() {
AppModule.testCameraFeaturePolicy = CameraFeaturePolicy(
aspectRatio = SettingConfig(defaultValue = AspectRatio.NINE_SIXTEEN)
)

runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
composeTestRule.visitQuickSettings {
onNodeWithTag(QUICK_SETTINGS_RATIO_9_16_BUTTON).assertIsOn()
}
}
}

// /////////////////////////////////////////////////////////////////////////
//
// Section B: Hidden
//
// /////////////////////////////////////////////////////////////////////////

@Test
fun hiddenAspectRatio_removesAspectRatioControlFromQuickSettings() {
AppModule.testCameraFeaturePolicy = CameraFeaturePolicy(
aspectRatio = SettingConfig(
defaultValue = AspectRatio.ONE_ONE,
visibility = OptionVisibility.Hidden
)
)

runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
composeTestRule.visitQuickSettings {
onNodeWithTag(ROW_QUICK_SETTINGS_ASPECT_RATIO).assertDoesNotExist()
}
}
}

// /////////////////////////////////////////////////////////////////////////
//
// Section C: OptionsEnabled / Filtering
//
// /////////////////////////////////////////////////////////////////////////

@Test
fun optionsEnabled_oneOneAndThreeFour_displaysOnlyThoseInQuickSettings() {
AppModule.testCameraFeaturePolicy = CameraFeaturePolicy(
aspectRatio = SettingConfig(
defaultValue = AspectRatio.ONE_ONE,
visibility = OptionVisibility.Only(
setOf(AspectRatio.ONE_ONE, AspectRatio.THREE_FOUR)
)
)
)

runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
composeTestRule.visitQuickSettings {
onNodeWithTag(ROW_QUICK_SETTINGS_ASPECT_RATIO).assertExists()
onNodeWithTag(QUICK_SETTINGS_RATIO_1_1_BUTTON).assertExists()
onNodeWithTag(QUICK_SETTINGS_RATIO_3_4_BUTTON).assertExists()
onNodeWithTag(QUICK_SETTINGS_RATIO_9_16_BUTTON).assertDoesNotExist()
}
}
}

@Test
fun optionsEnabled_singleOptionViaFactory_removesAspectRatioControlFromQuickSettings() {
AppModule.testCameraFeaturePolicy = CameraFeaturePolicy(
aspectRatio = SettingConfig(
defaultValue = AspectRatio.ONE_ONE,
visibility = OptionVisibility.from(setOf(AspectRatio.ONE_ONE))
)
)

runMainActivityScenarioTest {
composeTestRule.waitForCaptureButton()
composeTestRule.visitQuickSettings {
onNodeWithTag(ROW_QUICK_SETTINGS_ASPECT_RATIO).assertDoesNotExist()
}
}
}
}
Loading
Loading