From dfb50e4e39f1edb5d5c25b324571715dd6eef4a0 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 14 Jan 2026 11:36:20 -0800 Subject: [PATCH 01/40] Increase test timeouts for API 28 emulators --- .../java/com/google/jetpackcamera/utils/UiTestUtil.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt index c80b971c7..f6721e203 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -74,13 +74,13 @@ val compatMainActivityExtras: Bundle? val debugExtra: Bundle = Bundle().apply { putBoolean("KEY_DEBUG_MODE", true) } val cacheExtra: Bundle = Bundle().apply { putBoolean("KEY_REVIEW_AFTER_CAPTURE", true) } -const val DEFAULT_TIMEOUT_MILLIS = 1_000L -const val APP_START_TIMEOUT_MILLIS = 10_000L +const val DEFAULT_TIMEOUT_MILLIS = 5_000L +const val APP_START_TIMEOUT_MILLIS = 20_000L const val ELAPSED_TIME_TEXT_TIMEOUT_MILLIS = 45_000L const val SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS = 5_000L const val IMAGE_CAPTURE_TIMEOUT_MILLIS = 45_000L -const val VIDEO_CAPTURE_TIMEOUT_MILLIS = 5_000L -const val SAVE_MEDIA_TIMEOUT_MILLIS = 5_000L +const val VIDEO_CAPTURE_TIMEOUT_MILLIS = 15_000L +const val SAVE_MEDIA_TIMEOUT_MILLIS = 15_000L const val IMAGE_WELL_LOAD_TIMEOUT_MILLIS = 10_000L const val VIDEO_DURATION_MILLIS = 3_000L From 3494bdffdfa06b3765ccbf041c30bba4ef2e5e99 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 14 Jan 2026 13:49:35 -0800 Subject: [PATCH 02/40] Update tests to use common timeouts rather than `waitUntil` directly --- .../CachedImageCaptureDeviceTest.kt | 19 +-- .../CachedVideoRecordingDeviceTest.kt | 21 ++-- .../jetpackcamera/CaptureModeSettingsTest.kt | 40 +++--- .../jetpackcamera/ConcurrentCameraTest.kt | 13 +- .../jetpackcamera/DebugHideComponentsTest.kt | 12 +- .../google/jetpackcamera/FlashDeviceTest.kt | 26 ++-- .../google/jetpackcamera/FocusMeteringTest.kt | 17 ++- .../jetpackcamera/ImageCaptureDeviceTest.kt | 20 ++- .../google/jetpackcamera/NavigationTest.kt | 18 ++- .../google/jetpackcamera/PermissionsTest.kt | 114 +++++++++--------- .../google/jetpackcamera/PostCaptureTest.kt | 98 ++++++--------- .../google/jetpackcamera/SwitchCameraTest.kt | 6 +- .../jetpackcamera/VideoRecordingDeviceTest.kt | 19 +-- .../jetpackcamera/utils/ComposeTestRuleExt.kt | 27 +++++ 14 files changed, 220 insertions(+), 230 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt index 13cdc1145..20bd7076b 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt @@ -51,6 +51,7 @@ import com.google.jetpackcamera.utils.getTestUri import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult import com.google.jetpackcamera.utils.waitForCaptureButton +import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -79,12 +80,11 @@ class CachedImageCaptureDeviceTest { .performClick() // navigate to postcapture screen - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIEWER_POST_CAPTURE_IMAGE).isDisplayed() - } - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIEWER_POST_CAPTURE_IMAGE, + timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS + ) + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_EXIT) composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).performClick() composeTestRule.waitForCaptureButton() @@ -135,9 +135,10 @@ class CachedImageCaptureDeviceTest { .assertExists() .performClick() - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_FAILURE_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + IMAGE_CAPTURE_FAILURE_TAG, + timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS + ) uiDevice.pressBack() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt index fe00e076a..4728de0f6 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt @@ -46,6 +46,7 @@ import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedT import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.runScenarioTestForResult import com.google.jetpackcamera.utils.waitForCaptureButton +import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -70,14 +71,11 @@ class CachedVideoRecordingDeviceTest { composeTestRule.longClickForVideoRecordingCheckingElapsedTime() // navigate to postcapture screen - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIEWER_POST_CAPTURE_VIDEO).isDisplayed() - } - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag( - BUTTON_POST_CAPTURE_EXIT - ).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIEWER_POST_CAPTURE_VIDEO, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_EXIT) composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).performClick() composeTestRule.waitForCaptureButton() @@ -116,9 +114,10 @@ class CachedVideoRecordingDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } composeTestRule.longClickForVideoRecording() - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_FAILURE_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIDEO_CAPTURE_FAILURE_TAG, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) uiDevice.pressBack() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt index 3ece418b0..473e3e58c 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt @@ -20,8 +20,6 @@ import android.provider.MediaStore import androidx.compose.ui.geometry.Offset import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled -import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -63,7 +61,9 @@ import com.google.jetpackcamera.utils.unFocusQuickSetting import com.google.jetpackcamera.utils.visitQuickSettings import com.google.jetpackcamera.utils.wait 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.Rule import org.junit.Test import org.junit.runner.RunWith @@ -108,9 +108,7 @@ internal class CaptureModeSettingsTest { setCaptureMode(captureMode) } - waitUntil(DEFAULT_TIMEOUT_MILLIS) { - onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).isDisplayed() - } + waitForNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON, DEFAULT_TIMEOUT_MILLIS) onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertExists() } @@ -393,18 +391,19 @@ internal class CaptureModeSettingsTest { composeTestRule.initializeCaptureSwitch() val initialCaptureMode = composeTestRule.getCaptureModeToggleState() + val targetCaptureMode = if (initialCaptureMode == CaptureMode.IMAGE_ONLY) { + CaptureMode.VIDEO_ONLY + } else { + CaptureMode.IMAGE_ONLY + } // should be different from initial capture mode composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).performClick() - composeTestRule.waitUntil { - composeTestRule.getCaptureModeToggleState() != initialCaptureMode - } + composeTestRule.waitForCaptureModeToggleState(targetCaptureMode) // should now be she same as the initial capture mode. composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).performClick() - composeTestRule.waitUntil { - composeTestRule.getCaptureModeToggleState() == initialCaptureMode - } + composeTestRule.waitForCaptureModeToggleState(initialCaptureMode) } @Test @@ -413,6 +412,11 @@ internal class CaptureModeSettingsTest { composeTestRule.waitForCaptureButton() composeTestRule.initializeCaptureSwitch() val initialCaptureMode = composeTestRule.getCaptureModeToggleState() + val targetCaptureMode = if (initialCaptureMode == CaptureMode.IMAGE_ONLY) { + CaptureMode.VIDEO_ONLY + } else { + CaptureMode.IMAGE_ONLY + } val captureToggleNode = composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON) val toggleNodeWidth = captureToggleNode.fetchSemanticsNode().size.width.toFloat() val offsetToSwitch = when (initialCaptureMode) { @@ -442,9 +446,7 @@ internal class CaptureModeSettingsTest { captureToggleNode.performTouchInput { up() } - composeTestRule.waitUntil { - initialCaptureMode != composeTestRule.getCaptureModeToggleState() - } + composeTestRule.waitForCaptureModeToggleState(targetCaptureMode) } @Test @@ -459,17 +461,13 @@ internal class CaptureModeSettingsTest { // start recording composeTestRule.tapStartLockedVideoRecording() // check that recording - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear(CAPTURE_MODE_TOGGLE_BUTTON) // stop recording composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).isDisplayed() && - composeTestRule.getCaptureModeToggleState() == CaptureMode.VIDEO_ONLY - } + composeTestRule.waitForNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON) + composeTestRule.waitForCaptureModeToggleState(CaptureMode.VIDEO_ONLY) deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ConcurrentCameraTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ConcurrentCameraTest.kt index 2658934f8..53d2292dc 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/ConcurrentCameraTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/ConcurrentCameraTest.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.isNotEnabled import androidx.compose.ui.test.junit4.createEmptyComposeRule @@ -33,7 +32,6 @@ import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.MainActivity import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.ui.components.capture.BTN_QUICK_SETTINGS_FOCUS_CAPTURE_MODE -import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.FLIP_CAMERA_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN @@ -44,7 +42,6 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_BUTTO import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_STREAM_CONFIG_BUTTON import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.assume @@ -53,6 +50,8 @@ import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedT import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.stateDescriptionMatches +import com.google.jetpackcamera.utils.waitForCaptureButton +import com.google.jetpackcamera.utils.waitForNodeWithTag import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test @@ -280,9 +279,7 @@ class ConcurrentCameraTest { longClickForVideoRecordingCheckingElapsedTime() - waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() - } + waitForNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG, VIDEO_CAPTURE_TIMEOUT_MILLIS) } } @@ -296,9 +293,7 @@ class ConcurrentCameraTest { ) { val wrappedBlock: ActivityScenario.() -> Unit = { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // /////////////////////////////////////////////////// // Check that the device supports concurrent camera // diff --git a/app/src/androidTest/java/com/google/jetpackcamera/DebugHideComponentsTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/DebugHideComponentsTest.kt index 8b9bc1688..d908b2f56 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/DebugHideComponentsTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/DebugHideComponentsTest.kt @@ -15,8 +15,6 @@ */ package com.google.jetpackcamera -import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -39,6 +37,8 @@ import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.debugExtra import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.waitForCaptureButton +import com.google.jetpackcamera.utils.waitForNodeWithTag +import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear import org.junit.Before import org.junit.Rule import org.junit.Test @@ -73,9 +73,7 @@ class DebugHideComponentsTest { composeTestRule.onNodeWithTag(BTN_DEBUG_HIDE_COMPONENTS_TAG).performClick() - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear(CAPTURE_BUTTON) composeTestRule.onNodeWithTag(ZOOM_BUTTON_ROW_TAG).assertDoesNotExist() composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertDoesNotExist() composeTestRule.onNodeWithTag(AMPLITUDE_NONE_TAG).assertDoesNotExist() @@ -87,9 +85,7 @@ class DebugHideComponentsTest { composeTestRule.onNodeWithTag(BTN_DEBUG_HIDE_COMPONENTS_TAG).performClick() - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag(CAPTURE_BUTTON) composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assertExists() composeTestRule.onNodeWithTag(DEBUG_OVERLAY_BUTTON).assertExists() composeTestRule.onNodeWithTag(LOGICAL_CAMERA_ID_TAG).assertExists() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt index e418a23f3..90758b0a8 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt @@ -46,6 +46,7 @@ import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedT import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.setFlashMode +import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Before import org.junit.Rule import org.junit.Test @@ -141,9 +142,7 @@ internal class FlashDeviceTest { .assertExists() .performClick() - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) } @Test @@ -172,13 +171,15 @@ internal class FlashDeviceTest { .assertExists() .performClick() - composeTestRule.waitUntil(timeoutMillis = SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(SCREEN_FLASH_OVERLAY).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + SCREEN_FLASH_OVERLAY, + SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS + ) - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + IMAGE_CAPTURE_SUCCESS_TAG, + IMAGE_CAPTURE_TIMEOUT_MILLIS + ) } @Test @@ -209,8 +210,9 @@ internal class FlashDeviceTest { composeTestRule.setFlashMode(FlashMode.ON) composeTestRule.longClickForVideoRecordingCheckingElapsedTime() - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIDEO_CAPTURE_SUCCESS_TAG, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) } } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/FocusMeteringTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/FocusMeteringTest.kt index f55e341ca..f72ebbd16 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/FocusMeteringTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/FocusMeteringTest.kt @@ -18,8 +18,6 @@ package com.google.jetpackcamera import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.test.click -import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag @@ -37,6 +35,8 @@ import com.google.jetpackcamera.utils.debugExtra import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.wait import com.google.jetpackcamera.utils.waitForCaptureButton +import com.google.jetpackcamera.utils.waitForNodeWithTag +import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -58,9 +58,7 @@ class FocusMeteringTest { // Hide all components so we don't accidentally tap on them composeTestRule.onNodeWithTag(BTN_DEBUG_HIDE_COMPONENTS_TAG).performClick() - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear(CAPTURE_BUTTON) // Define the four quadrants of the screen val quadrants = listOf( @@ -79,11 +77,10 @@ class FocusMeteringTest { performTouchInput { click(position = percentOffset(x, y)) } // Wait for the focus metering indicator to be visible - composeTestRule.waitUntil(FOCUS_METERING_INDICATOR_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag( - FOCUS_METERING_INDICATOR_TAG - ).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + FOCUS_METERING_INDICATOR_TAG, + FOCUS_METERING_INDICATOR_TIMEOUT_MILLIS + ) composeTestRule.waitUntil(FOCUS_METERING_INDICATOR_TIMEOUT_MILLIS) { composeTestRule.onAllNodesWithTag(FOCUS_METERING_INDICATOR_TAG).run { diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt index cf9b896d7..0139e3f8c 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt @@ -53,6 +53,7 @@ import com.google.jetpackcamera.utils.getTestUri import com.google.jetpackcamera.utils.longClickForVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult +import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -84,9 +85,7 @@ internal class ImageCaptureDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() .performClick() - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) } @Test @@ -101,9 +100,7 @@ internal class ImageCaptureDeviceTest { uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_UP) - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) } @Test @@ -117,9 +114,7 @@ internal class ImageCaptureDeviceTest { } uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN) - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) } @Test @@ -164,9 +159,10 @@ internal class ImageCaptureDeviceTest { .assertExists() .performClick() - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_FAILURE_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + IMAGE_CAPTURE_FAILURE_TAG, + IMAGE_CAPTURE_TIMEOUT_MILLIS + ) uiDevice.pressBack() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt index 4ec476485..db57373c0 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt @@ -17,7 +17,6 @@ package com.google.jetpackcamera import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled -import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -41,6 +40,8 @@ import com.google.jetpackcamera.utils.assume import com.google.jetpackcamera.utils.onNodeWithText import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.searchForQuickSetting +import com.google.jetpackcamera.utils.waitForNodeWithTag +import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -141,9 +142,7 @@ class NavigationTest { .performClick() // Wait for the quick settings to be displayed - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(QUICK_SETTINGS_RATIO_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag(QUICK_SETTINGS_RATIO_BUTTON) // Press the device's back button uiDevice.pressBack() @@ -172,16 +171,15 @@ class NavigationTest { .performClick() // Wait for the 1:1 ratio button to be displayed - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(QUICK_SETTINGS_RATIO_1_1_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag(QUICK_SETTINGS_RATIO_1_1_BUTTON) // Press the device's back button uiDevice.pressBack() // Assert bottom sheet closed - composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(QUICK_SETTINGS_BOTTOM_SHEET).isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear( + QUICK_SETTINGS_BOTTOM_SHEET, + DEFAULT_TIMEOUT_MILLIS + ) } } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt index a79828813..e829e1d8e 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt @@ -18,7 +18,6 @@ package com.google.jetpackcamera import android.Manifest.permission.CAMERA import android.Manifest.permission.RECORD_AUDIO import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.isNotDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -47,6 +46,9 @@ import com.google.jetpackcamera.utils.grantPermissionDialog import com.google.jetpackcamera.utils.onNodeWithText import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.waitForCaptureButton +import com.google.jetpackcamera.utils.waitForNodeWithTag +import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear +import com.google.jetpackcamera.utils.waitForNodeWithText import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -94,9 +96,10 @@ class PermissionsTest { @Test fun cameraPermission_granted_closesPage() = runMainActivityScenarioTest { // Wait for the camera permission screen to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + CAMERA_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // Click button to request permission composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON) @@ -117,9 +120,10 @@ class PermissionsTest { uiDevice.waitForIdle() runMainActivityScenarioTest { // Wait for the camera permission screen to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + CAMERA_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // Click button to request permission composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON) @@ -139,9 +143,10 @@ class PermissionsTest { // required permissions should persist on screen // Wait for the permission screen to be displayed runMainActivityScenarioTest { - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + CAMERA_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // Click button to request permission composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON) @@ -157,13 +162,11 @@ class PermissionsTest { composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed() // text changed after permission denied - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithText( - com.google.jetpackcamera.permissions.R.string - .camera_permission_declined_rationale - ) - .isDisplayed() - } + composeTestRule.waitForNodeWithText( + com.google.jetpackcamera.permissions.R.string + .camera_permission_declined_rationale, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // request permissions button should now say to navigate to settings composeTestRule.onNodeWithText( com.google.jetpackcamera.permissions @@ -176,9 +179,10 @@ class PermissionsTest { fun recordAudioPermission_granted_closesPage() { // optional permissions should close the screen after declining runMainActivityScenarioTest { - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(RECORD_AUDIO_PERMISSION_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + RECORD_AUDIO_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // Click button to request permission composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON) @@ -190,9 +194,10 @@ class PermissionsTest { uiDevice.waitForIdle() // Assert we're on a different page - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(RECORD_AUDIO_PERMISSION_BUTTON).isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear( + RECORD_AUDIO_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) } } @@ -200,9 +205,10 @@ class PermissionsTest { fun recordAudioPermission_denied_closesPage() { // optional permissions should close the screen after declining runMainActivityScenarioTest { - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(RECORD_AUDIO_PERMISSION_BUTTON).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + RECORD_AUDIO_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // Click button to request permission composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON) @@ -214,9 +220,10 @@ class PermissionsTest { uiDevice.waitForIdle() // Assert we're on a different page - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(RECORD_AUDIO_PERMISSION_BUTTON).isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear( + RECORD_AUDIO_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) } } @@ -229,11 +236,10 @@ class PermissionsTest { val timeStamp = System.currentTimeMillis() runMainActivityScenarioTest { // Wait for the camera permission screen to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag( - WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON - ).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // Click button to request permission composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON) @@ -244,11 +250,10 @@ class PermissionsTest { uiDevice.grantPermissionDialog() // permission screen should close - composeTestRule.waitUntil(timeoutMillis = 5_000) { - composeTestRule - .onNodeWithTag(WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON) - .isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear( + WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON, + timeoutMillis = 5_000 + ) composeTestRule.waitForCaptureButton() @@ -258,9 +263,10 @@ class PermissionsTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() .performClick() - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + IMAGE_CAPTURE_SUCCESS_TAG, + timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS + ) } deleteFilesInDirAfterTimestamp(PICTURES_DIR_PATH, instrumentation, timeStamp) @@ -272,11 +278,10 @@ class PermissionsTest { uiDevice.waitForIdle() runMainActivityScenarioTest { // Wait for the camera permission screen to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag( - WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON - ).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON, + timeoutMillis = APP_START_TIMEOUT_MILLIS + ) // Click button to request permission composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON) @@ -287,20 +292,19 @@ class PermissionsTest { uiDevice.denyPermissionDialog() // storage permission is optional and the screen should close - composeTestRule.waitUntil { - composeTestRule - .onNodeWithTag(WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON) - .isNotDisplayed() - } + composeTestRule.waitForNodeWithTagToDisappear( + WRITE_EXTERNAL_STORAGE_PERMISSION_BUTTON + ) composeTestRule.waitForCaptureButton() // check for image capture failure composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() - composeTestRule.waitUntil(timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_FAILURE_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + IMAGE_CAPTURE_FAILURE_TAG, + timeoutMillis = IMAGE_CAPTURE_TIMEOUT_MILLIS + ) // imageWell shouldn't appear composeTestRule.ensureTagNotAppears(IMAGE_WELL_TAG) } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt index 0ea61cbf3..a80529f3a 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/PostCaptureTest.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera import android.provider.MediaStore -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -50,6 +49,7 @@ import com.google.jetpackcamera.utils.mediaStoreEntryExistsAfterTimestamp import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.wait import com.google.jetpackcamera.utils.waitForCaptureButton +import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.After import org.junit.Before import org.junit.Rule @@ -96,20 +96,14 @@ class PostCaptureTest { private fun enterImageWellAndDelete(recentCaptureViewerTag: String) { // enter postcapture via imagewell - composeTestRule.waitUntil(IMAGE_WELL_LOAD_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_WELL_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_WELL_TAG, IMAGE_WELL_LOAD_TIMEOUT_MILLIS) composeTestRule.onNodeWithTag(IMAGE_WELL_TAG).assertExists().performClick() // most recent capture tag - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(recentCaptureViewerTag).isDisplayed() - } + composeTestRule.waitForNodeWithTag(recentCaptureViewerTag, VIDEO_CAPTURE_TIMEOUT_MILLIS) // delete most recent capture - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_DELETE).isDisplayed() - } + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_DELETE) composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_DELETE).assertExists().performClick() // wait for capture button after automatically exiting post capture @@ -118,20 +112,14 @@ class PostCaptureTest { private fun enterImageWellAndSave(recentCaptureViewerTag: String) { // enter postcapture via imagewell - composeTestRule.waitUntil(IMAGE_WELL_LOAD_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_WELL_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_WELL_TAG, IMAGE_WELL_LOAD_TIMEOUT_MILLIS) composeTestRule.onNodeWithTag(IMAGE_WELL_TAG).assertExists().performClick() // most recent capture tag - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(recentCaptureViewerTag).isDisplayed() - } + composeTestRule.waitForNodeWithTag(recentCaptureViewerTag, VIDEO_CAPTURE_TIMEOUT_MILLIS) // delete most recent capture - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_DELETE).isDisplayed() - } + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_DELETE) composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_SAVE).assertExists().performClick() } @@ -146,20 +134,19 @@ class PostCaptureTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() // navigate to postcapture screen - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIEWER_POST_CAPTURE_IMAGE).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIEWER_POST_CAPTURE_IMAGE, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_SAVE).isDisplayed() - } + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_SAVE) composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_SAVE).performClick() // Wait for image save success message - composeTestRule.waitUntil(timeoutMillis = SAVE_MEDIA_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(SNACKBAR_POST_CAPTURE_IMAGE_SAVE_SUCCESS) - .isDisplayed() - } + composeTestRule.waitForNodeWithTag( + SNACKBAR_POST_CAPTURE_IMAGE_SAVE_SUCCESS, + SAVE_MEDIA_TIMEOUT_MILLIS + ) assertThat(newImageMediaExists()).isTrue() } @@ -172,25 +159,22 @@ class PostCaptureTest { composeTestRule.longClickForVideoRecordingCheckingElapsedTime() // navigate to postcapture screen - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIEWER_POST_CAPTURE_VIDEO).isDisplayed() - } - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIEWER_POST_CAPTURE_VIDEO, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_EXIT) assertThat(newVideoMediaExists()).isFalse() // save video - composeTestRule.waitUntil { - composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_SAVE).isDisplayed() - } + composeTestRule.waitForNodeWithTag(BUTTON_POST_CAPTURE_SAVE) composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_SAVE).performClick() // Wait for video save success message - composeTestRule.waitUntil(timeoutMillis = SAVE_MEDIA_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(SNACKBAR_POST_CAPTURE_VIDEO_SAVE_SUCCESS) - .isDisplayed() - } + composeTestRule.waitForNodeWithTag( + SNACKBAR_POST_CAPTURE_VIDEO_SAVE_SUCCESS, + SAVE_MEDIA_TIMEOUT_MILLIS + ) assertThat(newVideoMediaExists()).isTrue() composeTestRule.onNodeWithTag(BUTTON_POST_CAPTURE_EXIT).performClick() @@ -203,9 +187,7 @@ class PostCaptureTest { composeTestRule.waitForCaptureButton() assertThat(newImageMediaExists()).isFalse() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() - composeTestRule.waitUntil(IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) assertThat(newImageMediaExists()).isTrue() // enter postcapture via imagewell and delete recent capture enterImageWellAndDelete(VIEWER_POST_CAPTURE_IMAGE) @@ -222,9 +204,7 @@ class PostCaptureTest { assertThat(newVideoMediaExists()).isFalse() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() - composeTestRule.waitUntil(VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG, VIDEO_CAPTURE_TIMEOUT_MILLIS) assertThat(newVideoMediaExists()).isTrue() // enter postcapture via imagewell and delete recent capture enterImageWellAndDelete(VIEWER_POST_CAPTURE_VIDEO) @@ -239,17 +219,16 @@ class PostCaptureTest { composeTestRule.waitForCaptureButton() assertThat(newImageMediaExists()).isFalse() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() - composeTestRule.waitUntil(IMAGE_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) assertThat(newImageMediaExists()).isTrue() // enter postcapture via imagewell and save recent capture val newTimestamp = System.currentTimeMillis() enterImageWellAndSave(VIEWER_POST_CAPTURE_IMAGE) - composeTestRule.waitUntil(SAVE_MEDIA_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(SNACKBAR_POST_CAPTURE_IMAGE_SAVE_SUCCESS).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + SNACKBAR_POST_CAPTURE_IMAGE_SAVE_SUCCESS, + SAVE_MEDIA_TIMEOUT_MILLIS + ) composeTestRule.waitUntil(timeoutMillis = SAVE_MEDIA_TIMEOUT_MILLIS) { newImageMediaExists(newTimestamp) } @@ -262,16 +241,15 @@ class PostCaptureTest { assertThat(newVideoMediaExists()).isFalse() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() - composeTestRule.waitUntil(VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG, VIDEO_CAPTURE_TIMEOUT_MILLIS) assertThat(newVideoMediaExists()).isTrue() // enter postcapture via imagewell and save recent capture val newTimestamp = System.currentTimeMillis() enterImageWellAndSave(VIEWER_POST_CAPTURE_VIDEO) - composeTestRule.waitUntil(SAVE_MEDIA_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(SNACKBAR_POST_CAPTURE_VIDEO_SAVE_SUCCESS).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + SNACKBAR_POST_CAPTURE_VIDEO_SAVE_SUCCESS, + SAVE_MEDIA_TIMEOUT_MILLIS + ) composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { newVideoMediaExists(newTimestamp) } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt index f66e4fda5..f56d5e80d 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/SwitchCameraTest.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera import androidx.compose.ui.test.doubleClick -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createEmptyComposeRule @@ -37,6 +36,7 @@ import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.assume import com.google.jetpackcamera.utils.getCurrentLensFacing import com.google.jetpackcamera.utils.runMainActivityScenarioTest +import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -141,9 +141,7 @@ inline fun runFlipCameraTest( crossinline block: ActivityScenario.() -> Unit ) = runMainActivityScenarioTest { // Wait for the preview display to be visible - composeTestRule.waitUntil(APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(PREVIEW_DISPLAY).isDisplayed() - } + composeTestRule.waitForNodeWithTag(PREVIEW_DISPLAY, APP_START_TIMEOUT_MILLIS) // If flipping the camera is available, flip it. Otherwise skip test. composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON).assume(isEnabled()) { diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index a37e62623..bd5557934 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -46,6 +46,7 @@ import com.google.jetpackcamera.utils.pressAndDragToLockVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runScenarioTestForResult import com.google.jetpackcamera.utils.tapStartLockedVideoRecording +import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -72,9 +73,7 @@ internal class VideoRecordingDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } composeTestRule.longClickForVideoRecordingCheckingElapsedTime() - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG, VIDEO_CAPTURE_TIMEOUT_MILLIS) deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) } @@ -95,9 +94,10 @@ internal class VideoRecordingDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIDEO_CAPTURE_SUCCESS_TAG, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) } @@ -157,9 +157,10 @@ internal class VideoRecordingDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() } composeTestRule.longClickForVideoRecording() - composeTestRule.waitUntil(timeoutMillis = VIDEO_CAPTURE_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(VIDEO_CAPTURE_FAILURE_TAG).isDisplayed() - } + composeTestRule.waitForNodeWithTag( + VIDEO_CAPTURE_FAILURE_TAG, + VIDEO_CAPTURE_TIMEOUT_MILLIS + ) uiDevice.pressBack() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt index b6f6501bf..4a9e56f72 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt @@ -154,6 +154,24 @@ fun ComposeTestRule.waitForNodeWithTag(tag: String, timeoutMillis: Long = DEFAUL waitUntil(timeoutMillis = timeoutMillis) { onNodeWithTag(tag).isDisplayed() } } +fun ComposeTestRule.waitForNodeWithTagToDisappear( + tag: String, + timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS +) { + waitUntil(timeoutMillis = timeoutMillis) { + onNodeWithTag(tag).isNotDisplayed() + } +} + +fun ComposeTestRule.waitForNodeWithText( + @StringRes textResId: Int, + timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS +) { + waitUntil(timeoutMillis = timeoutMillis) { + onNodeWithText(textResId).isDisplayed() + } +} + private fun ComposeTestRule.idleForVideoDuration( durationMillis: Long = VIDEO_DURATION_MILLIS, earlyExitPredicate: () -> Boolean = { @@ -342,6 +360,15 @@ fun ComposeTestRule.getCaptureModeToggleState(): CaptureMode = } } +fun ComposeTestRule.waitForCaptureModeToggleState( + targetState: CaptureMode, + timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS +) { + waitUntil(timeoutMillis = timeoutMillis) { + getCaptureModeToggleState() == targetState + } +} + // ////////////////////// // // check current quick settings state From 0090aed4e226306f01bee11524d9ed70cc2187ee Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 14 Jan 2026 14:23:05 -0800 Subject: [PATCH 03/40] Update style guide with some guidance on timeouts in tests --- .gemini/styleguide.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index a8dbf4c01..6b70feefa 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -43,6 +43,9 @@ When reviewing a pull request, focus on the following key areas: * **Descriptive Test Names:** Test function names must be clear, descriptive, and follow a consistent pattern. * **Use Truth Assertions:** Always prefer [Truth](https://truth.dev/) assertions (`assertThat(...)`) over JUnit assertions (`assertEquals`, `assertTrue`, etc.). Truth provides more readable assertion chains and more informative failure messages. Avoid functions from `org.junit.Assert` such as `assertEquals`, `assertTrue`, `assertFalse`, `assertNull`, and `assertNotNull`. * **Explicit Test Runners:** All test classes must be annotated with `@RunWith(...)` to explicitly declare which test runner should be used (e.g., `@RunWith(AndroidJUnit4::class)`, `@RunWith(RobolectricTestRunner::class)`, or `@RunWith(JUnit4::class)` for host tests with no Android dependencies). + * **Test Stability & Timeouts:** + * **Explicit Timeouts:** Avoid using `waitUntil` (or similar synchronization) without explicitly defining a `timeoutMillis`. Default timeouts are often too short for slower emulators (like API 28) or low-end devices, leading to flakiness. + * **Helper Functions for Waits:** If a wait condition is repeated (e.g., waiting for a specific UI element), extract it into a helper function (e.g., `waitForNodeWithTag`). This consolidates the logic and allows the timeout duration to be tuned centrally for that specific scenario. 6. **Documentation Sync** * **Check for necessary updates:** Analyze if the PR's changes (e.g., adding a new feature, changing build logic, deprecating functionality) require updates to `README.md` or other documentation files. From 0787e1af5bb3aa7009aaf9c666c7c124b62bb94f Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 10 Nov 2025 18:30:34 -0800 Subject: [PATCH 04/40] Add isCameraRunning state Introduces an `isCameraRunning` flag to the `CameraState` to accurately reflect the underlying camera's status. This state is derived from the CameraX `CameraState` and is used to disable the capture button when the camera is not open or available. This commit also renames `torchEnabled` to `isTorchEnabled` for clarity and consistency, and ensures the `ZoomState` is being correctly updated in the `CameraState`. The capture button's UI has been improved to prevent flickering during brief transitional states (e.g. switching cameras). This is achieved by visually debouncing the disabled state. While the button's semantics and pointer input are disabled immediately, the visual change to a disabled appearance is delayed by one second. If the button becomes enabled again within this period, the distracting flicker is avoided. Color transitions are animated for a smoother user experience. Finally, the test suite has been made more robust by updating the tests to wait for the capture button to be both displayed and enabled before interaction. --- .../jetpackcamera/BackgroundDeviceTest.kt | 19 ++--- .../jetpackcamera/ImageCaptureDeviceTest.kt | 42 +++------- .../google/jetpackcamera/NavigationTest.kt | 19 ++--- .../google/jetpackcamera/PermissionsTest.kt | 4 +- .../jetpackcamera/VideoRecordingDeviceTest.kt | 25 ++---- .../jetpackcamera/utils/ComposeTestRuleExt.kt | 4 +- .../capture/CaptureButtonComponents.kt | 77 ++++++++++++++++--- .../capture/CaptureButtonUiStateAdapter.kt | 34 ++++---- 8 files changed, 115 insertions(+), 109 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt index 742c2ae3c..57f0e4ca6 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/BackgroundDeviceTest.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera import android.os.Build -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -28,7 +27,6 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.google.common.truth.Truth.assertThat import com.google.common.truth.TruthJUnit.assume -import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_FLIP_CAMERA_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON @@ -37,6 +35,7 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_STREAM_CONF import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.runMainActivityScenarioTest +import com.google.jetpackcamera.utils.waitForCaptureButton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -74,9 +73,7 @@ class BackgroundDeviceTest { @Test fun background_foreground() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() backgroundThenForegroundApp() } @@ -84,9 +81,7 @@ class BackgroundDeviceTest { @Test fun flipCamera_then_background_foreground() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) @@ -109,9 +104,7 @@ class BackgroundDeviceTest { @Test fun setAspectRatio_then_background_foreground() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) @@ -147,9 +140,7 @@ class BackgroundDeviceTest { assumeSupportsSingleStream() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt index 0139e3f8c..becb2c3da 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt @@ -34,7 +34,6 @@ import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.FILE_PREFIX import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_PREFIX @@ -53,6 +52,7 @@ import com.google.jetpackcamera.utils.getTestUri import com.google.jetpackcamera.utils.longClickForVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Rule import org.junit.Test @@ -78,9 +78,7 @@ internal class ImageCaptureDeviceTest { filePrefix = FILE_PREFIX ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -94,9 +92,7 @@ internal class ImageCaptureDeviceTest { filePrefix = FILE_PREFIX ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_UP) @@ -109,9 +105,7 @@ internal class ImageCaptureDeviceTest { filePrefix = FILE_PREFIX ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN) composeTestRule.waitForNodeWithTag(IMAGE_CAPTURE_SUCCESS_TAG, IMAGE_CAPTURE_TIMEOUT_MILLIS) @@ -126,9 +120,7 @@ internal class ImageCaptureDeviceTest { getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -151,9 +143,7 @@ internal class ImageCaptureDeviceTest { getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -178,9 +168,7 @@ internal class ImageCaptureDeviceTest { getSingleImageCaptureIntent(uri, MediaStore.ACTION_IMAGE_CAPTURE) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecording() @@ -211,9 +199,7 @@ internal class ImageCaptureDeviceTest { ) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() repeat(2) { clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, @@ -239,9 +225,7 @@ internal class ImageCaptureDeviceTest { getMultipleImageCaptureIntent(null, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() repeat(2) { clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, @@ -263,9 +247,7 @@ internal class ImageCaptureDeviceTest { getMultipleImageCaptureIntent(null, MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() uiDevice.pressBack() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) @@ -285,9 +267,7 @@ internal class ImageCaptureDeviceTest { ) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, IMAGE_CAPTURE_FAILURE_TAG diff --git a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt index db57373c0..cdaa1cac0 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/NavigationTest.kt @@ -15,7 +15,6 @@ */ package com.google.jetpackcamera -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -33,13 +32,13 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_DROP_DOWN import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_1_1_BUTTON import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_BUTTON import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.DEFAULT_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.assume import com.google.jetpackcamera.utils.onNodeWithText import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.searchForQuickSetting +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import com.google.jetpackcamera.utils.waitForNodeWithTagToDisappear import org.junit.Rule @@ -61,9 +60,7 @@ class NavigationTest { @Test fun backAfterReturnFromSettings_doesNotReturnToSettings() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // open quick settings composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN).assertExists().performClick() @@ -97,9 +94,7 @@ class NavigationTest { @Test fun returnFromSettings_afterFlipCamera_returnsToPreview() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // If flipping the camera is available, flip it. Otherwise skip test. composeTestRule.onNodeWithTag(FLIP_CAMERA_BUTTON) @@ -132,9 +127,7 @@ class NavigationTest { @Test fun backFromQuickSettings_returnToPreview() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to the quick settings screen composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) @@ -154,9 +147,7 @@ class NavigationTest { @Test fun backFromQuickSettingsExpended_returnToQuickSettings() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Navigate to the quick settings screen composeTestRule.onNodeWithTag(QUICK_SETTINGS_DROP_DOWN) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt index e829e1d8e..297eb3ca7 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt @@ -87,9 +87,7 @@ class PermissionsTest { @Test fun allPermissions_alreadyGranted_screenNotShown() { runMainActivityScenarioTest { - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() } } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index bd5557934..24d31c89f 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -18,7 +18,6 @@ package com.google.jetpackcamera import android.app.Activity import android.net.Uri import android.provider.MediaStore -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -30,7 +29,6 @@ import com.google.common.truth.Truth import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_PREFIX import com.google.jetpackcamera.utils.MOVIES_DIR_PATH import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS @@ -46,6 +44,7 @@ import com.google.jetpackcamera.utils.pressAndDragToLockVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runScenarioTestForResult import com.google.jetpackcamera.utils.tapStartLockedVideoRecording +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Rule import org.junit.Test @@ -69,9 +68,7 @@ internal class VideoRecordingDeviceTest { ) { val timeStamp = System.currentTimeMillis() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() composeTestRule.waitForNodeWithTag(VIDEO_CAPTURE_SUCCESS_TAG, VIDEO_CAPTURE_TIMEOUT_MILLIS) deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) @@ -84,13 +81,11 @@ internal class VideoRecordingDeviceTest { ) { val timeStamp = System.currentTimeMillis() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.pressAndDragToLockVideoRecording() // stop recording - // fixme: this shouldnt need two clicks + // fixme: this shouldn't need two clicks composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().performClick() @@ -111,9 +106,7 @@ internal class VideoRecordingDeviceTest { getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) @@ -130,9 +123,7 @@ internal class VideoRecordingDeviceTest { getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // start recording composeTestRule.tapStartLockedVideoRecording() @@ -153,9 +144,7 @@ internal class VideoRecordingDeviceTest { getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecording() composeTestRule.waitForNodeWithTag( VIDEO_CAPTURE_FAILURE_TAG, diff --git a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt index 4a9e56f72..09c6f0472 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/ComposeTestRuleExt.kt @@ -144,9 +144,9 @@ fun ComposeTestRule.wait(timeoutMillis: Long) { } } fun ComposeTestRule.waitForCaptureButton(timeoutMillis: Long = APP_START_TIMEOUT_MILLIS) { - // Wait for the capture button to be displayed + // Wait for the capture button to be displayed and enabled waitUntil(timeoutMillis = timeoutMillis) { - onNodeWithTag(CAPTURE_BUTTON).isDisplayed() + onNode(hasTestTag(CAPTURE_BUTTON) and isEnabled()).isDisplayed() } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index d659a84e3..abec033db 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -67,6 +67,8 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -166,9 +168,9 @@ fun CaptureButton( captureButtonUiState: CaptureButtonUiState, captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE ) { - var currentUiState = rememberUpdatedState(captureButtonUiState) + val currentUiState = rememberUpdatedState(captureButtonUiState) val firstKeyPressed = remember { mutableStateOf(null) } - val isLongPressing = remember { mutableStateOf(false) } + val isLongPressing = remember { mutableStateOf(false) } var longPressJob by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() val longPressTimeout = LocalViewConfiguration.current.longPressTimeoutMillis @@ -183,7 +185,7 @@ fun CaptureButton( } } fun onLongPress() { - if (isLongPressing.value == false) { + if (!isLongPressing.value) { when (val current = currentUiState.value) { is CaptureButtonUiState.Enabled.Idle -> when (current.captureMode) { CaptureMode.STANDARD, @@ -289,10 +291,37 @@ private fun CaptureButton( val currentUiState = rememberUpdatedState(captureButtonUiState) val switchWidth = (captureButtonSize * LOCK_SWITCH_WIDTH_SCALE) - val currentColor = LocalContentColor.current var relativeCaptureButtonBounds by remember { mutableStateOf(null) } + val isEnabled = captureButtonUiState !is CaptureButtonUiState.Unavailable + + var isVisuallyDisabled by remember { + mutableStateOf(captureButtonUiState is CaptureButtonUiState.Unavailable) + } + val currentCaptureButtonUiState = rememberUpdatedState(captureButtonUiState) + + LaunchedEffect(captureButtonUiState) { + if (currentCaptureButtonUiState.value is CaptureButtonUiState.Unavailable) { + delay(1000) + if (currentCaptureButtonUiState.value is CaptureButtonUiState.Unavailable) { + isVisuallyDisabled = true + } + } else { + isVisuallyDisabled = false + } + } + + val animatedColor by animateColorAsState( + targetValue = if (isVisuallyDisabled) { + LocalContentColor.current.copy(alpha = 0.38f) + } else { + LocalContentColor.current + }, + animationSpec = tween(durationMillis = if (isVisuallyDisabled) 1000 else 300), + label = "Capture Button Color" + ) + fun shouldBeLocked(): Boolean = switchPosition > MINIMUM_LOCK_THRESHOLD fun setLockSwitchPosition(positionX: Float, offsetX: Float) { @@ -323,12 +352,8 @@ private fun CaptureButton( LOCK_SWITCH_POSITION_ON } } - CaptureButtonRing( - modifier = modifier - .onSizeChanged { - relativeCaptureButtonBounds = - Rect(0f, 0f, it.width.toFloat(), it.height.toFloat()) - } + val gestureModifier = if (isEnabled) { + Modifier .pointerInput(Unit) { detectTapGestures( // onLongPress cannot be null, otherwise it won't detect the release if the @@ -385,9 +410,24 @@ private fun CaptureButton( } } ) - }, + } + } else { + Modifier + } + CaptureButtonRing( + modifier = modifier + .onSizeChanged { + relativeCaptureButtonBounds = + Rect(0f, 0f, it.width.toFloat(), it.height.toFloat()) + } + .semantics { + if (!isEnabled) { + disabled() + } + } + .then(gestureModifier), captureButtonSize = captureButtonSize, - color = currentColor + color = animatedColor ) { if (useLockSwitch) { LockSwitchCaptureButtonNucleus( @@ -633,6 +673,19 @@ private fun CaptureButtonNucleus( } } +@Preview +@Composable +private fun CaptureButtonUnavailablePreview() { + CaptureButton( + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Unavailable + ) +} + @Preview @Composable private fun IdleStandardCaptureButtonPreview() { diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt index e8e11a7ab..724abc0a1 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt @@ -24,21 +24,25 @@ fun CaptureButtonUiState.Companion.from( cameraAppSettings: CameraAppSettings, cameraState: CameraState, lockedState: Boolean -): CaptureButtonUiState = when (cameraState.videoRecordingState) { - // if not currently recording, check capturemode to determine idle capture button UI - is VideoRecordingState.Inactive -> - CaptureButtonUiState - .Enabled.Idle(captureMode = cameraAppSettings.captureMode) +): CaptureButtonUiState = if (cameraState.isCameraRunning) { + when (cameraState.videoRecordingState) { + // if not currently recording, check capturemode to determine idle capture button UI + is VideoRecordingState.Inactive -> + CaptureButtonUiState + .Enabled.Idle(captureMode = cameraAppSettings.captureMode) - // display different capture button UI depending on if recording is pressed or locked - is VideoRecordingState.Active.Recording, is VideoRecordingState.Active.Paused -> - if (lockedState) { - CaptureButtonUiState.Enabled.Recording.LockedRecording - } else { - CaptureButtonUiState.Enabled.Recording.PressedRecording - } + // display different capture button UI depending on if recording is pressed or locked + is VideoRecordingState.Active.Recording, is VideoRecordingState.Active.Paused -> + if (lockedState) { + CaptureButtonUiState.Enabled.Recording.LockedRecording + } else { + CaptureButtonUiState.Enabled.Recording.PressedRecording + } - is VideoRecordingState.Starting -> - CaptureButtonUiState - .Enabled.Idle(captureMode = cameraAppSettings.captureMode) + is VideoRecordingState.Starting -> + CaptureButtonUiState + .Enabled.Idle(captureMode = cameraAppSettings.captureMode) + } +} else { + CaptureButtonUiState.Unavailable } From 0e9aa1bfef883d4b79aff5c505a38d2e9c9a55bd Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 13 Jan 2026 16:39:52 -0800 Subject: [PATCH 05/40] Update more tests to use `waitForCaptureButton` --- .../CachedImageCaptureDeviceTest.kt | 25 ++++----------- .../CachedVideoRecordingDeviceTest.kt | 11 ++----- .../google/jetpackcamera/FlashDeviceTest.kt | 31 +++++-------------- .../jetpackcamera/SettingsDeviceTest.kt | 7 ++--- .../google/jetpackcamera/VideoAudioTest.kt | 7 ++--- 5 files changed, 20 insertions(+), 61 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt index 20bd7076b..51c7e1261 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/CachedImageCaptureDeviceTest.kt @@ -34,7 +34,6 @@ import com.google.jetpackcamera.feature.postcapture.ui.VIEWER_POST_CAPTURE_IMAGE import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_PREFIX import com.google.jetpackcamera.utils.MESSAGE_DISAPPEAR_TIMEOUT_MILLIS @@ -101,9 +100,7 @@ class CachedImageCaptureDeviceTest { cacheExtra ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -127,9 +124,7 @@ class CachedImageCaptureDeviceTest { cacheExtra ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() @@ -162,9 +157,7 @@ class CachedImageCaptureDeviceTest { cacheExtra ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() repeat(2) { clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, @@ -191,9 +184,7 @@ class CachedImageCaptureDeviceTest { cacheExtra ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() repeat(2) { clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, @@ -219,9 +210,7 @@ class CachedImageCaptureDeviceTest { cacheExtra ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() uiDevice.pressBack() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_CANCELED) @@ -242,9 +231,7 @@ class CachedImageCaptureDeviceTest { cacheExtra ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() clickCaptureAndWaitUntilMessageDisappears( IMAGE_CAPTURE_TIMEOUT_MILLIS, IMAGE_CAPTURE_FAILURE_TAG diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt index 4728de0f6..4c419d85e 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/CachedVideoRecordingDeviceTest.kt @@ -18,7 +18,6 @@ package com.google.jetpackcamera import android.app.Activity import android.net.Uri import android.provider.MediaStore -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick @@ -29,9 +28,7 @@ import androidx.test.uiautomator.UiDevice import com.google.common.truth.Truth import com.google.jetpackcamera.feature.postcapture.ui.BUTTON_POST_CAPTURE_EXIT import com.google.jetpackcamera.feature.postcapture.ui.VIEWER_POST_CAPTURE_VIDEO -import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.MOVIES_DIR_PATH import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.VIDEO_CAPTURE_TIMEOUT_MILLIS @@ -92,9 +89,7 @@ class CachedVideoRecordingDeviceTest { cacheExtra ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() } Truth.assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK) @@ -110,9 +105,7 @@ class CachedVideoRecordingDeviceTest { getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE) ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecording() composeTestRule.waitForNodeWithTag( VIDEO_CAPTURE_FAILURE_TAG, diff --git a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt index 90758b0a8..6649d27c5 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/FlashDeviceTest.kt @@ -17,7 +17,6 @@ package com.google.jetpackcamera import android.os.Build import android.provider.MediaStore -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onNodeWithTag @@ -35,7 +34,6 @@ import com.google.jetpackcamera.ui.components.capture.FLIP_CAMERA_BUTTON import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.ui.components.capture.SCREEN_FLASH_OVERLAY import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.IMAGE_CAPTURE_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.SCREEN_FLASH_OVERLAY_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS @@ -46,6 +44,7 @@ import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedT import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.setFlashMode +import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag import org.junit.Before import org.junit.Rule @@ -72,9 +71,7 @@ internal class FlashDeviceTest { @Test fun set_flash_on() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.ON) } @@ -82,9 +79,7 @@ internal class FlashDeviceTest { @Test fun set_flash_auto() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.AUTO) } @@ -92,9 +87,7 @@ internal class FlashDeviceTest { @Test fun set_flash_off() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.OFF) } @@ -102,9 +95,7 @@ internal class FlashDeviceTest { @Test fun set_flash_low_light_boost() = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.setFlashMode(FlashMode.LOW_LIGHT_BOOST) } @@ -124,9 +115,7 @@ internal class FlashDeviceTest { assumeHalStableOnImageCapture() // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Ensure camera has a back camera and flip to it val lensFacing = composeTestRule.getCurrentLensFacing() @@ -152,9 +141,7 @@ internal class FlashDeviceTest { filePrefix = "JCA" ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Ensure camera has a front camera and flip to it val lensFacing = composeTestRule.getCurrentLensFacing() @@ -195,9 +182,7 @@ internal class FlashDeviceTest { mediaUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI ) { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // Ensure camera has the target lens facing camera and flip to it val lensFacing = composeTestRule.getCurrentLensFacing() diff --git a/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt index d3b9a7837..a66587cdc 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/SettingsDeviceTest.kt @@ -68,14 +68,13 @@ import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_STREAM_CONFI import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_VIDEO_DURATION_TAG import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_VIDEO_QUALITY_TAG import com.google.jetpackcamera.settings.ui.BTN_OPEN_DIALOG_SETTING_VIDEO_STABILIZATION_TAG -import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.DEFAULT_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.runMainActivityScenarioTest import com.google.jetpackcamera.utils.selectLensFacing import com.google.jetpackcamera.utils.visitSettingDialog import com.google.jetpackcamera.utils.visitSettingsScreen +import com.google.jetpackcamera.utils.waitForCaptureButton import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -105,9 +104,7 @@ class SettingsDeviceTest(private val lensFacing: LensFacing) { action: ComposeTestRule.() -> Unit ): Unit = runMainActivityScenarioTest { // Wait for the capture button to be displayed - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() composeTestRule.visitSettingsScreen { // Ensure appropriate lens facing is selected diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt index 626249b9f..3b02122b4 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoAudioTest.kt @@ -15,7 +15,6 @@ */ package com.google.jetpackcamera -import androidx.compose.ui.test.isDisplayed import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onNodeWithTag @@ -30,10 +29,10 @@ import androidx.test.uiautomator.Until import com.google.common.truth.Truth.assertThat import com.google.jetpackcamera.ui.components.capture.AMPLITUDE_HOT_TAG import com.google.jetpackcamera.ui.components.capture.CAPTURE_BUTTON -import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS import com.google.jetpackcamera.utils.TEST_REQUIRED_PERMISSIONS import com.google.jetpackcamera.utils.debugExtra import com.google.jetpackcamera.utils.runMainActivityScenarioTest +import com.google.jetpackcamera.utils.waitForCaptureButton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -61,9 +60,7 @@ class VideoAudioTest { runMainActivityScenarioTest(debugExtra) { // check audio visualizer composable for muted/unmuted icon. // icon will only be unmuted if audio is nonzero - composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) { - composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed() - } + composeTestRule.waitForCaptureButton() // record video composeTestRule.onNodeWithTag(CAPTURE_BUTTON) From 74fd57266b62d384f28c67f4090cab55bc32b20e Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 10 Nov 2025 21:55:50 -0800 Subject: [PATCH 06/40] Move visually debounced button state into its own `remember` hook --- .../capture/CaptureButtonComponents.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index abec033db..4eb0295f7 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -47,6 +47,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -269,6 +270,37 @@ fun CaptureButton( ) } +/** + * A composable that returns a debounced boolean state for whether the capture button should be + * visually disabled. + * + * While the button's semantics and pointer input are disabled immediately, the visual change + * to a disabled appearance is delayed. If the button becomes enabled again within this period, + * the distracting flicker is avoided. + * + * @param captureButtonUiState The current UI state of the capture button. + * @param delayMillis The duration to wait before visually disabling the button. + * @return A [State] holding `true` if the button should be visually disabled, `false` otherwise. + */ +@Composable +private fun rememberDebouncedVisuallyDisabled( + captureButtonUiState: CaptureButtonUiState, + delayMillis: Long = 1000L +): State { + val isVisuallyDisabled = remember { + mutableStateOf(captureButtonUiState is CaptureButtonUiState.Unavailable) + } + LaunchedEffect(captureButtonUiState) { + if (captureButtonUiState is CaptureButtonUiState.Unavailable) { + delay(delayMillis) + isVisuallyDisabled.value = true + } else { + isVisuallyDisabled.value = false + } + } + return isVisuallyDisabled +} + @Composable private fun CaptureButton( modifier: Modifier = Modifier, @@ -296,21 +328,9 @@ private fun CaptureButton( val isEnabled = captureButtonUiState !is CaptureButtonUiState.Unavailable - var isVisuallyDisabled by remember { - mutableStateOf(captureButtonUiState is CaptureButtonUiState.Unavailable) - } - val currentCaptureButtonUiState = rememberUpdatedState(captureButtonUiState) - - LaunchedEffect(captureButtonUiState) { - if (currentCaptureButtonUiState.value is CaptureButtonUiState.Unavailable) { - delay(1000) - if (currentCaptureButtonUiState.value is CaptureButtonUiState.Unavailable) { - isVisuallyDisabled = true - } - } else { - isVisuallyDisabled = false - } - } + val isVisuallyDisabled by rememberDebouncedVisuallyDisabled( + captureButtonUiState = captureButtonUiState + ) val animatedColor by animateColorAsState( targetValue = if (isVisuallyDisabled) { From c1db907ff01731a89bea113a1281960ba6581046 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 15:13:11 -0800 Subject: [PATCH 07/40] Rename CaptureButtonUiState.Enabled to "Available" This is the opposite of "Unavailable". --- .../feature/preview/PreviewScreen.kt | 8 +-- .../capture/CaptureButtonComponents.kt | 54 ++++++++++--------- .../capture/CaptureScreenComponents.kt | 4 +- .../uistate/capture/CaptureButtonUiState.kt | 6 +-- .../capture/CaptureButtonUiStateAdapter.kt | 8 +-- 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index 17095ca55..bb5625efc 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -739,7 +739,7 @@ private fun ContentScreen_ImageOnly_Idle() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_READY.copy( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY) ), screenFlashUiState = ScreenFlashUiState(), surfaceRequest = null @@ -753,7 +753,7 @@ private fun ContentScreen_VideoOnly_Idle() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_READY.copy( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.VIDEO_ONLY) ), screenFlashUiState = ScreenFlashUiState(), surfaceRequest = null @@ -793,12 +793,12 @@ private val FAKE_PREVIEW_UI_STATE_READY = CaptureUiState.Ready( private val FAKE_PREVIEW_UI_STATE_PRESSED_RECORDING = FAKE_PREVIEW_UI_STATE_READY.copy( videoRecordingState = VideoRecordingState.Active.Recording(0, 0.0, 0), - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, audioUiState = AudioUiState.Enabled.On(1.0) ) private val FAKE_PREVIEW_UI_STATE_LOCKED_RECORDING = FAKE_PREVIEW_UI_STATE_READY.copy( videoRecordingState = VideoRecordingState.Active.Recording(0, 0.0, 0), - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording, + captureButtonUiState = CaptureButtonUiState.Available.Recording.LockedRecording, audioUiState = AudioUiState.Enabled.On(1.0) ) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 4eb0295f7..2d5cddaa2 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -177,9 +177,11 @@ fun CaptureButton( val longPressTimeout = LocalViewConfiguration.current.longPressTimeoutMillis LaunchedEffect(captureButtonUiState) { - if (captureButtonUiState is CaptureButtonUiState.Enabled.Idle) { + if (captureButtonUiState is CaptureButtonUiState.Available.Idle) { onLockVideoRecording(false) - } else if (captureButtonUiState is CaptureButtonUiState.Enabled.Recording.LockedRecording) { + } else if (captureButtonUiState + is CaptureButtonUiState.Available.Recording.LockedRecording + ) { longPressJob = null isLongPressing.value = false firstKeyPressed.value = null @@ -188,7 +190,7 @@ fun CaptureButton( fun onLongPress() { if (!isLongPressing.value) { when (val current = currentUiState.value) { - is CaptureButtonUiState.Enabled.Idle -> when (current.captureMode) { + is CaptureButtonUiState.Available.Idle -> when (current.captureMode) { CaptureMode.STANDARD, CaptureMode.VIDEO_ONLY -> { isLongPressing.value = true @@ -222,7 +224,7 @@ fun CaptureButton( if (isLongPressing.value) { if (!isLocked && currentUiState.value is - CaptureButtonUiState.Enabled.Recording.PressedRecording + CaptureButtonUiState.Available.Recording.PressedRecording ) { Log.d(TAG, "Stopping recording") onStopRecording() @@ -231,7 +233,7 @@ fun CaptureButton( // on click else { when (val current = currentUiState.value) { - is CaptureButtonUiState.Enabled.Idle -> when (current.captureMode) { + is CaptureButtonUiState.Available.Idle -> when (current.captureMode) { CaptureMode.STANDARD, CaptureMode.IMAGE_ONLY -> onImageCapture() @@ -242,8 +244,8 @@ fun CaptureButton( } } - CaptureButtonUiState.Enabled.Recording.LockedRecording -> onStopRecording() - CaptureButtonUiState.Enabled.Recording.PressedRecording, + CaptureButtonUiState.Available.Recording.LockedRecording -> onStopRecording() + CaptureButtonUiState.Available.Recording.PressedRecording, CaptureButtonUiState.Unavailable -> { } } @@ -401,7 +403,7 @@ private fun CaptureButton( onDragCancel = {}, onDrag = { change, deltaOffset -> if (currentUiState.value == - CaptureButtonUiState.Enabled.Recording.PressedRecording + CaptureButtonUiState.Available.Recording.PressedRecording ) { val newPoint = change.position @@ -522,7 +524,7 @@ private fun LockSwitchCaptureButtonNucleus( // grey cylinder offset to the left and fades in when pressed recording AnimatedVisibility( visible = captureButtonUiState == - CaptureButtonUiState.Enabled.Recording.PressedRecording, + CaptureButtonUiState.Available.Recording.PressedRecording, enter = fadeIn(), exit = ExitTransition.None ) { @@ -554,7 +556,7 @@ private fun LockSwitchCaptureButtonNucleus( // locked icon, matches cylinder offset AnimatedVisibility( visible = captureButtonUiState == - CaptureButtonUiState.Enabled.Recording.PressedRecording, + CaptureButtonUiState.Available.Recording.PressedRecording, enter = fadeIn(), exit = ExitTransition.None ) { @@ -618,13 +620,13 @@ private fun CaptureButtonNucleus( val centerShapeSize by animateDpAsState( targetValue = when (val uiState = currentUiState.value) { // inner circle fills white ring when locked - CaptureButtonUiState.Enabled.Recording.LockedRecording -> captureButtonSize.dp + CaptureButtonUiState.Available.Recording.LockedRecording -> captureButtonSize.dp - CaptureButtonUiState.Enabled.Recording.PressedRecording -> + CaptureButtonUiState.Available.Recording.PressedRecording -> (captureButtonSize * pressedVideoCaptureScale).dp CaptureButtonUiState.Unavailable -> 0.dp - is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { + is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { // no inner circle will be visible on STANDARD CaptureMode.STANDARD -> 0.dp // large white circle will be visible on IMAGE_ONLY @@ -639,13 +641,13 @@ private fun CaptureButtonNucleus( // used to fade between red/white in the center of the capture button val animatedColor by animateColorAsState( targetValue = when (val uiState = currentUiState.value) { - is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { + is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { CaptureMode.STANDARD -> imageCaptureModeColor CaptureMode.IMAGE_ONLY -> imageCaptureModeColor CaptureMode.VIDEO_ONLY -> recordingColor } - is CaptureButtonUiState.Enabled.Recording -> recordingColor + is CaptureButtonUiState.Available.Recording -> recordingColor is CaptureButtonUiState.Unavailable -> Color.Transparent }, animationSpec = tween(durationMillis = 500) @@ -663,7 +665,7 @@ private fun CaptureButtonNucleus( .alpha( if (isPressed && currentUiState.value == - CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY) ) { .5f // transparency to indicate click ONLY on IMAGE_ONLY } else { @@ -676,7 +678,7 @@ private fun CaptureButtonNucleus( // central "square" stop icon AnimatedVisibility( visible = currentUiState.value is - CaptureButtonUiState.Enabled.Recording.LockedRecording, + CaptureButtonUiState.Available.Recording.LockedRecording, enter = scaleIn(initialScale = .5f) + fadeIn(), exit = fadeOut() ) { @@ -711,7 +713,7 @@ private fun CaptureButtonUnavailablePreview() { private fun IdleStandardCaptureButtonPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD), + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.STANDARD), isPressed = false, captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE ) @@ -723,7 +725,7 @@ private fun IdleStandardCaptureButtonPreview() { private fun IdleImageCaptureButtonPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY), + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), isPressed = false, captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE ) @@ -735,7 +737,7 @@ private fun IdleImageCaptureButtonPreview() { private fun PressedImageCaptureButtonPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY), + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), isPressed = true, captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE ) @@ -747,7 +749,7 @@ private fun PressedImageCaptureButtonPreview() { private fun IdleRecordingCaptureButtonPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY), + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.VIDEO_ONLY), isPressed = false, captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE ) @@ -759,7 +761,7 @@ private fun IdleRecordingCaptureButtonPreview() { private fun SimpleNucleusPressedRecordingPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, isPressed = true, captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE ) @@ -771,7 +773,7 @@ private fun SimpleNucleusPressedRecordingPreview() { private fun LockedRecordingPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording, + captureButtonUiState = CaptureButtonUiState.Available.Recording.LockedRecording, isPressed = false, captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE ) @@ -786,7 +788,7 @@ private fun LockSwitchUnlockedPressedRecordingPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, switchPosition = 0f, onToggleSwitchPosition = {}, @@ -804,7 +806,7 @@ private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, switchPosition = MINIMUM_LOCK_THRESHOLD, onToggleSwitchPosition = {}, @@ -822,7 +824,7 @@ private fun LockSwitchLockedPressedRecordingPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, switchPosition = 1f, onToggleSwitchPosition = {}, diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index 7a06a0826..a86bed710 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -529,7 +529,7 @@ fun CaptureButton( modifier = modifier.testTag(CAPTURE_BUTTON), onIncrementZoom = onIncrementZoom, onImageCapture = { - if (captureButtonUiState is CaptureButtonUiState.Enabled) { + if (captureButtonUiState is CaptureButtonUiState.Available) { multipleEventsCutter.processEvent { onCaptureImage(context.contentResolver) } @@ -539,7 +539,7 @@ fun CaptureButton( } }, onStartRecording = { - if (captureButtonUiState is CaptureButtonUiState.Enabled) { + if (captureButtonUiState is CaptureButtonUiState.Available) { onStartVideoRecording() } }, diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt index 7ab930379..962659160 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt @@ -19,10 +19,10 @@ import com.google.jetpackcamera.model.CaptureMode sealed interface CaptureButtonUiState { data object Unavailable : CaptureButtonUiState - sealed interface Enabled : CaptureButtonUiState { - data class Idle(val captureMode: CaptureMode) : Enabled + sealed interface Available : CaptureButtonUiState { + data class Idle(val captureMode: CaptureMode) : Available - sealed interface Recording : Enabled { + sealed interface Recording : Available { data object PressedRecording : Recording data object LockedRecording : Recording } diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt index 724abc0a1..4f13c540f 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt @@ -29,19 +29,19 @@ fun CaptureButtonUiState.Companion.from( // if not currently recording, check capturemode to determine idle capture button UI is VideoRecordingState.Inactive -> CaptureButtonUiState - .Enabled.Idle(captureMode = cameraAppSettings.captureMode) + .Available.Idle(captureMode = cameraAppSettings.captureMode) // display different capture button UI depending on if recording is pressed or locked is VideoRecordingState.Active.Recording, is VideoRecordingState.Active.Paused -> if (lockedState) { - CaptureButtonUiState.Enabled.Recording.LockedRecording + CaptureButtonUiState.Available.Recording.LockedRecording } else { - CaptureButtonUiState.Enabled.Recording.PressedRecording + CaptureButtonUiState.Available.Recording.PressedRecording } is VideoRecordingState.Starting -> CaptureButtonUiState - .Enabled.Idle(captureMode = cameraAppSettings.captureMode) + .Available.Idle(captureMode = cameraAppSettings.captureMode) } } else { CaptureButtonUiState.Unavailable From 4833dac6b263134f8d01cf80c01b08fd6c8d88f4 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 15:45:45 -0800 Subject: [PATCH 08/40] Add isEnabled property to CaptureButtonUiState to control button interactivity --- .../components/capture/CaptureButtonComponents.kt | 11 +++++------ .../ui/uistate/capture/CaptureButtonUiState.kt | 14 ++++++++++++-- .../capture/CaptureButtonUiStateAdapter.kt | 3 ++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 2d5cddaa2..cc7073f7b 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -209,6 +209,7 @@ fun CaptureButton( } fun onPress(captureSource: CaptureSource) { + if (!captureButtonUiState.isEnabled) return if (firstKeyPressed.value == null) { firstKeyPressed.value = captureSource longPressJob = scope.launch { @@ -290,10 +291,10 @@ private fun rememberDebouncedVisuallyDisabled( delayMillis: Long = 1000L ): State { val isVisuallyDisabled = remember { - mutableStateOf(captureButtonUiState is CaptureButtonUiState.Unavailable) + mutableStateOf(!captureButtonUiState.isEnabled) } LaunchedEffect(captureButtonUiState) { - if (captureButtonUiState is CaptureButtonUiState.Unavailable) { + if (!captureButtonUiState.isEnabled) { delay(delayMillis) isVisuallyDisabled.value = true } else { @@ -328,8 +329,6 @@ private fun CaptureButton( var relativeCaptureButtonBounds by remember { mutableStateOf(null) } - val isEnabled = captureButtonUiState !is CaptureButtonUiState.Unavailable - val isVisuallyDisabled by rememberDebouncedVisuallyDisabled( captureButtonUiState = captureButtonUiState ) @@ -374,7 +373,7 @@ private fun CaptureButton( LOCK_SWITCH_POSITION_ON } } - val gestureModifier = if (isEnabled) { + val gestureModifier = if (captureButtonUiState.isEnabled) { Modifier .pointerInput(Unit) { detectTapGestures( @@ -443,7 +442,7 @@ private fun CaptureButton( Rect(0f, 0f, it.width.toFloat(), it.height.toFloat()) } .semantics { - if (!isEnabled) { + if (!captureButtonUiState.isEnabled) { disabled() } } diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt index 962659160..771771e42 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt @@ -18,11 +18,21 @@ package com.google.jetpackcamera.ui.uistate.capture import com.google.jetpackcamera.model.CaptureMode sealed interface CaptureButtonUiState { - data object Unavailable : CaptureButtonUiState + val isEnabled: Boolean + + data object Unavailable : CaptureButtonUiState { + override val isEnabled: Boolean = false + } + sealed interface Available : CaptureButtonUiState { - data class Idle(val captureMode: CaptureMode) : Available + data class Idle( + val captureMode: CaptureMode, + override val isEnabled: Boolean = true + ) : Available sealed interface Recording : Available { + override val isEnabled: Boolean get() = true + data object PressedRecording : Recording data object LockedRecording : Recording } diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt index 4f13c540f..5642261c4 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt @@ -44,5 +44,6 @@ fun CaptureButtonUiState.Companion.from( .Available.Idle(captureMode = cameraAppSettings.captureMode) } } else { - CaptureButtonUiState.Unavailable + CaptureButtonUiState + .Available.Idle(captureMode = cameraAppSettings.captureMode, isEnabled = false) } From db5fdbf4a54569381543a35cd9ad0e0325f356ef Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 15:47:24 -0800 Subject: [PATCH 09/40] Add documentation to CaptureButtonUiState --- .../uistate/capture/CaptureButtonUiState.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt index 771771e42..d5e7459d8 100644 --- a/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt +++ b/ui/uistate/capture/src/main/java/com/google/jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt @@ -17,23 +17,51 @@ package com.google.jetpackcamera.ui.uistate.capture import com.google.jetpackcamera.model.CaptureMode +/** + * Defines the UI state for the capture button. + */ sealed interface CaptureButtonUiState { + /** + * Whether the capture button is enabled and can be interacted with. + */ val isEnabled: Boolean + /** + * The capture button is unavailable and should not be shown or interacted with. + */ data object Unavailable : CaptureButtonUiState { override val isEnabled: Boolean = false } + /** + * The capture button is available to be shown. + */ sealed interface Available : CaptureButtonUiState { + /** + * The capture button is idle and ready to capture. + * + * @property captureMode The current capture mode. + * @property isEnabled Whether the button is enabled for interaction. + */ data class Idle( val captureMode: CaptureMode, override val isEnabled: Boolean = true ) : Available + /** + * The capture button is currently recording video. + */ sealed interface Recording : Available { override val isEnabled: Boolean get() = true + /** + * The user is actively pressing the capture button to record. + */ data object PressedRecording : Recording + + /** + * The recording is locked and continues without user interaction. + */ data object LockedRecording : Recording } } From 728a5cf27f34d6b752b058541013019477bbd3bc Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 16:00:53 -0800 Subject: [PATCH 10/40] Add new test for CaptureButtonUiStateAdapter --- .../CaptureButtonUiStateAdapterTest.kt | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapterTest.kt diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapterTest.kt new file mode 100644 index 000000000..a046caeb0 --- /dev/null +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureButtonUiStateAdapterTest.kt @@ -0,0 +1,111 @@ +/* + * 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.ui.uistateadapter.capture + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.core.camera.CameraState +import com.google.jetpackcamera.core.camera.VideoRecordingState +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.settings.model.CameraAppSettings +import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class CaptureButtonUiStateAdapterTest { + private val defaultCameraAppSettings = CameraAppSettings(captureMode = CaptureMode.STANDARD) + private val defaultCameraState = CameraState(isCameraRunning = true) + + @Test + fun from_cameraNotRunning_returnsIdleAndDisabled() { + val cameraState = CameraState(isCameraRunning = false) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState).isInstanceOf(CaptureButtonUiState.Available.Idle::class.java) + assertThat(uiState.isEnabled).isFalse() + assertThat((uiState as CaptureButtonUiState.Available.Idle).captureMode) + .isEqualTo(CaptureMode.STANDARD) + } + + @Test + fun from_cameraRunning_recordingInactive_returnsIdleAndEnabled() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Inactive() + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState).isInstanceOf(CaptureButtonUiState.Available.Idle::class.java) + assertThat(uiState.isEnabled).isTrue() + assertThat((uiState as CaptureButtonUiState.Available.Idle).captureMode) + .isEqualTo(CaptureMode.STANDARD) + } + + @Test + fun from_cameraRunning_recordingPressed_returnsPressedRecording() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Active.Recording(0L, 0.0, 0L) + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState) + .isInstanceOf(CaptureButtonUiState.Available.Recording.PressedRecording::class.java) + assertThat(uiState.isEnabled).isTrue() + } + + @Test + fun from_cameraRunning_recordingLocked_returnsLockedRecording() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Active.Recording(0L, 0.0, 0L) + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = true + ) + + assertThat(uiState) + .isInstanceOf(CaptureButtonUiState.Available.Recording.LockedRecording::class.java) + assertThat(uiState.isEnabled).isTrue() + } + + @Test + fun from_cameraRunning_recordingStarting_returnsIdleAndEnabled() { + val cameraState = defaultCameraState.copy( + videoRecordingState = VideoRecordingState.Starting(null) + ) + val uiState = CaptureButtonUiState.from( + defaultCameraAppSettings, + cameraState, + lockedState = false + ) + + assertThat(uiState).isInstanceOf(CaptureButtonUiState.Available.Idle::class.java) + assertThat(uiState.isEnabled).isTrue() + } +} From 1f5bccd1377d4c24f72523197492151eb61b1c59 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 16:01:17 -0800 Subject: [PATCH 11/40] Add new Compose Previews for disabled capture button states --- .../capture/CaptureButtonComponents.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index cc7073f7b..0cc314aab 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -731,6 +731,63 @@ private fun IdleImageCaptureButtonPreview() { } } +@Preview +@Composable +private fun IdleVideoOnlyCaptureButtonPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.VIDEO_ONLY), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + +@Preview +@Composable +private fun IdleStandardCaptureButtonDisabledPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.STANDARD, + isEnabled = false + ), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + +@Preview +@Composable +private fun IdleImageCaptureButtonDisabledPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.IMAGE_ONLY, + isEnabled = false + ), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + +@Preview +@Composable +private fun IdleVideoOnlyCaptureButtonDisabledPreview() { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.VIDEO_ONLY, + isEnabled = false + ), + isPressed = false, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } +} + @Preview @Composable private fun PressedImageCaptureButtonPreview() { From a4dea88de2dac628613bc502e1a56d1f3807f596 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 14 Jan 2026 18:20:51 -0800 Subject: [PATCH 12/40] Remove repeated logic from `CaptureModeSettingsTest` --- .../jetpackcamera/CaptureModeSettingsTest.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt index 473e3e58c..e8599e37a 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/CaptureModeSettingsTest.kt @@ -113,6 +113,15 @@ internal class CaptureModeSettingsTest { onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).assertExists() } + private fun flip(mode: CaptureMode): CaptureMode { + require(mode == CaptureMode.IMAGE_ONLY || mode == CaptureMode.VIDEO_ONLY) + return if (mode == CaptureMode.IMAGE_ONLY) { + CaptureMode.VIDEO_ONLY + } else { + CaptureMode.IMAGE_ONLY + } + } + @Test fun can_set_capture_mode_in_quick_settings() { runMainActivityScenarioTest { @@ -391,11 +400,7 @@ internal class CaptureModeSettingsTest { composeTestRule.initializeCaptureSwitch() val initialCaptureMode = composeTestRule.getCaptureModeToggleState() - val targetCaptureMode = if (initialCaptureMode == CaptureMode.IMAGE_ONLY) { - CaptureMode.VIDEO_ONLY - } else { - CaptureMode.IMAGE_ONLY - } + val targetCaptureMode = flip(initialCaptureMode) // should be different from initial capture mode composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON).performClick() @@ -412,11 +417,7 @@ internal class CaptureModeSettingsTest { composeTestRule.waitForCaptureButton() composeTestRule.initializeCaptureSwitch() val initialCaptureMode = composeTestRule.getCaptureModeToggleState() - val targetCaptureMode = if (initialCaptureMode == CaptureMode.IMAGE_ONLY) { - CaptureMode.VIDEO_ONLY - } else { - CaptureMode.IMAGE_ONLY - } + val targetCaptureMode = flip(initialCaptureMode) val captureToggleNode = composeTestRule.onNodeWithTag(CAPTURE_MODE_TOGGLE_BUTTON) val toggleNodeWidth = captureToggleNode.fetchSemanticsNode().size.width.toFloat() val offsetToSwitch = when (initialCaptureMode) { From e448e8002009286148021ceca2b2c966b7b5ffba Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 17:09:08 -0800 Subject: [PATCH 13/40] Update CaptureButton Compose Previews to use real logic This also adds an additional interactionSource to the capture button so we can emulate touch events in our compose previews. --- .../capture/CaptureButtonComponents.kt | 198 +++++++++--------- 1 file changed, 95 insertions(+), 103 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 0cc314aab..b2faca086 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -32,6 +32,9 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset @@ -44,6 +47,7 @@ import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -167,7 +171,8 @@ fun CaptureButton( onLockVideoRecording: (Boolean) -> Unit, onIncrementZoom: (Float) -> Unit, captureButtonUiState: CaptureButtonUiState, - captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE + captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { val currentUiState = rememberUpdatedState(captureButtonUiState) val firstKeyPressed = remember { mutableStateOf(null) } @@ -269,7 +274,8 @@ fun CaptureButton( onLockVideoRecording = onLockVideoRecording, onDragZoom = onIncrementZoom, captureButtonUiState = captureButtonUiState, - captureButtonSize = captureButtonSize + captureButtonSize = captureButtonSize, + interactionSource = interactionSource ) } @@ -313,13 +319,9 @@ private fun CaptureButton( onLockVideoRecording: (Boolean) -> Unit, captureButtonUiState: CaptureButtonUiState, useLockSwitch: Boolean = true, - captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE + captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { - // todo: explore MutableInteractionSource - var isCaptureButtonPressed by remember { - mutableStateOf(false) - } - var switchPosition by remember { mutableFloatStateOf(LOCK_SWITCH_POSITION_OFF) } @@ -333,6 +335,8 @@ private fun CaptureButton( captureButtonUiState = captureButtonUiState ) + val isCaptureButtonPressed by interactionSource.collectIsPressedAsState() + val animatedColor by animateColorAsState( targetValue = if (isVisuallyDisabled) { LocalContentColor.current.copy(alpha = 0.38f) @@ -381,10 +385,11 @@ private fun CaptureButton( // touch is dragged off the component onLongPress = {}, onPress = { - isCaptureButtonPressed = true + val press = PressInteraction.Press(it) + interactionSource.emit(press) onPress() awaitRelease() - isCaptureButtonPressed = false + interactionSource.emit(PressInteraction.Release(press)) if (shouldBeLocked()) { onLockVideoRecording(true) onRelease(true) @@ -457,13 +462,15 @@ private fun CaptureButton( switchWidth = switchWidth.dp, switchPosition = switchPosition, onToggleSwitchPosition = { toggleSwitchPosition() }, - shouldBeLocked = { shouldBeLocked() } + shouldBeLocked = { shouldBeLocked() }, + isVisuallyDisabled = isVisuallyDisabled ) } else { CaptureButtonNucleus( captureButtonUiState = captureButtonUiState, isPressed = isCaptureButtonPressed, - captureButtonSize = captureButtonSize + captureButtonSize = captureButtonSize, + isVisuallyDisabled = isVisuallyDisabled ) } } @@ -502,7 +509,8 @@ private fun LockSwitchCaptureButtonNucleus( switchWidth: Dp, switchPosition: Float, onToggleSwitchPosition: () -> Unit, - shouldBeLocked: () -> Boolean + shouldBeLocked: () -> Boolean, + isVisuallyDisabled: Boolean = false ) { val pressedNucleusSize = (captureButtonSize * LOCK_SWITCH_PRESSED_NUCLEUS_SCALE).dp val switchHeight = (pressedNucleusSize * LOCK_SWITCH_HEIGHT_SCALE) @@ -549,7 +557,8 @@ private fun LockSwitchCaptureButtonNucleus( captureButtonSize = captureButtonSize, captureButtonUiState = captureButtonUiState, pressedVideoCaptureScale = LOCK_SWITCH_PRESSED_NUCLEUS_SCALE, - isPressed = false + isPressed = false, + isVisuallyDisabled = isVisuallyDisabled ) // locked icon, matches cylinder offset @@ -601,7 +610,8 @@ private fun CaptureButtonNucleus( imageCaptureModeColor: Color = Color.White, idleImageCaptureScale: Float = .7f, idleVideoCaptureScale: Float = .35f, - pressedVideoCaptureScale: Float = .7f + pressedVideoCaptureScale: Float = .7f, + isVisuallyDisabled: Boolean = false ) { require(idleImageCaptureScale in 0f..1f) { "value must be between 0 and 1 to remain within the bounds of the capture button" @@ -639,15 +649,19 @@ private fun CaptureButtonNucleus( // used to fade between red/white in the center of the capture button val animatedColor by animateColorAsState( - targetValue = when (val uiState = currentUiState.value) { - is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { - CaptureMode.STANDARD -> imageCaptureModeColor - CaptureMode.IMAGE_ONLY -> imageCaptureModeColor - CaptureMode.VIDEO_ONLY -> recordingColor - } + targetValue = if (isVisuallyDisabled) { + LocalContentColor.current.copy(alpha = 0.38f) + } else { + when (val uiState = currentUiState.value) { + is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { + CaptureMode.STANDARD -> imageCaptureModeColor + CaptureMode.IMAGE_ONLY -> imageCaptureModeColor + CaptureMode.VIDEO_ONLY -> recordingColor + } - is CaptureButtonUiState.Available.Recording -> recordingColor - is CaptureButtonUiState.Unavailable -> Color.Transparent + is CaptureButtonUiState.Available.Recording -> recordingColor + is CaptureButtonUiState.Unavailable -> Color.Transparent + } }, animationSpec = tween(durationMillis = 500) ) @@ -707,133 +721,111 @@ private fun CaptureButtonUnavailablePreview() { ) } -@Preview @Composable -private fun IdleStandardCaptureButtonPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.STANDARD), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE +private fun PreviewCaptureButton( + captureButtonUiState: CaptureButtonUiState, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + Surface(color = Color.Black, contentColor = Color.White) { + CaptureButton( + modifier = modifier, + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = captureButtonUiState, + interactionSource = interactionSource ) } } +@Preview +@Composable +private fun IdleStandardCaptureButtonPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.STANDARD) + ) +} + @Preview @Composable private fun IdleImageCaptureButtonPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY) + ) } @Preview @Composable private fun IdleVideoOnlyCaptureButtonPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.VIDEO_ONLY), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.VIDEO_ONLY) + ) } @Preview @Composable private fun IdleStandardCaptureButtonDisabledPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle( - CaptureMode.STANDARD, - isEnabled = false - ), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.STANDARD, + isEnabled = false ) - } + ) } @Preview @Composable private fun IdleImageCaptureButtonDisabledPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle( - CaptureMode.IMAGE_ONLY, - isEnabled = false - ), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.IMAGE_ONLY, + isEnabled = false ) - } + ) } @Preview @Composable private fun IdleVideoOnlyCaptureButtonDisabledPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle( - CaptureMode.VIDEO_ONLY, - isEnabled = false - ), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.VIDEO_ONLY, + isEnabled = false ) - } + ) } @Preview @Composable private fun PressedImageCaptureButtonPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), - isPressed = true, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } -} - -@Preview -@Composable -private fun IdleRecordingCaptureButtonPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.VIDEO_ONLY), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(Unit) { + interactionSource.emit(PressInteraction.Press(Offset.Zero)) } + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), + interactionSource = interactionSource + ) } @Preview @Composable private fun SimpleNucleusPressedRecordingPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, - isPressed = true, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } + // This state is visual only based on UI State, doesn't require press interaction to look pressed + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording + ) } @Preview @Composable private fun LockedRecordingPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Recording.LockedRecording, - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Available.Recording.LockedRecording + ) } @Preview From cc0ca6b8842622c0a3a05d4d52180f384d5d17b1 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 17:28:19 -0800 Subject: [PATCH 14/40] Simplify Preview logic for CaptureButtonComponent --- .../capture/CaptureButtonComponents.kt | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index b2faca086..92b2aa423 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -725,19 +725,22 @@ private fun CaptureButtonUnavailablePreview() { private fun PreviewCaptureButton( captureButtonUiState: CaptureButtonUiState, modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.Center, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { - Surface(color = Color.Black, contentColor = Color.White) { - CaptureButton( - modifier = modifier, - onImageCapture = {}, - onStartRecording = {}, - onStopRecording = {}, - onLockVideoRecording = {}, - onIncrementZoom = {}, - captureButtonUiState = captureButtonUiState, - interactionSource = interactionSource - ) + Surface(color = Color.Black, contentColor = Color.White, modifier = modifier) { + Box(contentAlignment = contentAlignment) { + CaptureButton( + modifier = Modifier, + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = captureButtonUiState, + interactionSource = interactionSource + ) + } } } @@ -813,10 +816,12 @@ private fun PressedImageCaptureButtonPreview() { @Preview @Composable -private fun SimpleNucleusPressedRecordingPreview() { - // This state is visual only based on UI State, doesn't require press interaction to look pressed +private fun LockSwitchUnlockedPressedRecordingPreview() { + // box is here to account for the offset lock switch PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording + captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, + modifier = Modifier.width(150.dp), + contentAlignment = Alignment.CenterEnd ) } @@ -828,24 +833,6 @@ private fun LockedRecordingPreview() { ) } -@Preview -@Composable -private fun LockSwitchUnlockedPressedRecordingPreview() { - // box is here to account for the offset lock switch - Box(modifier = Modifier.width(150.dp), contentAlignment = Alignment.CenterEnd) { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - LockSwitchCaptureButtonNucleus( - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, - switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, - switchPosition = 0f, - onToggleSwitchPosition = {}, - shouldBeLocked = { false } - ) - } - } -} - @Preview @Composable private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { From 89b9ddb1806ffdb0d1e8ed75defb7ec3a2a989bc Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 19:02:39 -0800 Subject: [PATCH 15/40] Ensure pressed state for IMAGE_ONLY is right color Also ensures the disabled state for the capture button has the correct animations for the nucleus. --- .../capture/CaptureButtonComponents.kt | 147 +++++++++++------- 1 file changed, 92 insertions(+), 55 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 92b2aa423..64cbc8f37 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -15,14 +15,16 @@ */ package com.google.jetpackcamera.ui.components.capture -import android.util.Log import android.view.KeyEvent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExitTransition +import androidx.compose.animation.animateColor import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn @@ -47,7 +49,9 @@ import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -199,7 +203,6 @@ fun CaptureButton( CaptureMode.STANDARD, CaptureMode.VIDEO_ONLY -> { isLongPressing.value = true - Log.d(TAG, "Starting recording") onStartRecording() } @@ -232,7 +235,6 @@ fun CaptureButton( currentUiState.value is CaptureButtonUiState.Available.Recording.PressedRecording ) { - Log.d(TAG, "Stopping recording") onStopRecording() } } @@ -245,7 +247,6 @@ fun CaptureButton( CaptureMode.VIDEO_ONLY -> { onLockVideoRecording(true) - Log.d(TAG, "Starting recording") onStartRecording() } } @@ -322,6 +323,10 @@ private fun CaptureButton( captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { + var isCaptureButtonPressed by remember { + mutableStateOf(false) + } + var switchPosition by remember { mutableFloatStateOf(LOCK_SWITCH_POSITION_OFF) } @@ -335,7 +340,7 @@ private fun CaptureButton( captureButtonUiState = captureButtonUiState ) - val isCaptureButtonPressed by interactionSource.collectIsPressedAsState() + val isPressedInteraction by interactionSource.collectIsPressedAsState() val animatedColor by animateColorAsState( targetValue = if (isVisuallyDisabled) { @@ -387,8 +392,10 @@ private fun CaptureButton( onPress = { val press = PressInteraction.Press(it) interactionSource.emit(press) + isCaptureButtonPressed = true // Manually set pressed state onPress() awaitRelease() + isCaptureButtonPressed = false // Manually unset pressed state interactionSource.emit(PressInteraction.Release(press)) if (shouldBeLocked()) { onLockVideoRecording(true) @@ -463,12 +470,13 @@ private fun CaptureButton( switchPosition = switchPosition, onToggleSwitchPosition = { toggleSwitchPosition() }, shouldBeLocked = { shouldBeLocked() }, - isVisuallyDisabled = isVisuallyDisabled + isVisuallyDisabled = isVisuallyDisabled, + isPressed = isCaptureButtonPressed || isPressedInteraction ) } else { CaptureButtonNucleus( captureButtonUiState = captureButtonUiState, - isPressed = isCaptureButtonPressed, + isPressed = isCaptureButtonPressed || isPressedInteraction, captureButtonSize = captureButtonSize, isVisuallyDisabled = isVisuallyDisabled ) @@ -510,7 +518,8 @@ private fun LockSwitchCaptureButtonNucleus( switchPosition: Float, onToggleSwitchPosition: () -> Unit, shouldBeLocked: () -> Boolean, - isVisuallyDisabled: Boolean = false + isVisuallyDisabled: Boolean = false, + isPressed: Boolean ) { val pressedNucleusSize = (captureButtonSize * LOCK_SWITCH_PRESSED_NUCLEUS_SCALE).dp val switchHeight = (pressedNucleusSize * LOCK_SWITCH_HEIGHT_SCALE) @@ -557,7 +566,7 @@ private fun LockSwitchCaptureButtonNucleus( captureButtonSize = captureButtonSize, captureButtonUiState = captureButtonUiState, pressedVideoCaptureScale = LOCK_SWITCH_PRESSED_NUCLEUS_SCALE, - isPressed = false, + isPressed = isPressed, isVisuallyDisabled = isVisuallyDisabled ) @@ -589,6 +598,10 @@ private fun LockSwitchCaptureButtonNucleus( } } +private enum class NucleusState { + Disabled, Idle, Pressed +} + /** * The animated center of the capture button. It serves as a visual indicator of the current capture and recording states. * @@ -648,23 +661,44 @@ private fun CaptureButtonNucleus( ) // used to fade between red/white in the center of the capture button - val animatedColor by animateColorAsState( - targetValue = if (isVisuallyDisabled) { - LocalContentColor.current.copy(alpha = 0.38f) - } else { - when (val uiState = currentUiState.value) { - is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { - CaptureMode.STANDARD -> imageCaptureModeColor - CaptureMode.IMAGE_ONLY -> imageCaptureModeColor - CaptureMode.VIDEO_ONLY -> recordingColor - } + val isPressableImageMode = currentUiState.value.let { + it is CaptureButtonUiState.Available.Idle && + (it.captureMode == CaptureMode.IMAGE_ONLY || it.captureMode == CaptureMode.STANDARD) + } + val nucleusState = when { + isVisuallyDisabled -> NucleusState.Disabled + isPressed && isPressableImageMode -> NucleusState.Pressed + else -> NucleusState.Idle + } - is CaptureButtonUiState.Available.Recording -> recordingColor - is CaptureButtonUiState.Unavailable -> Color.Transparent + val transition = updateTransition(targetState = nucleusState, label = "Nucleus Color Transition") + val animatedColor by transition.animateColor( + label = "Nucleus Color", + transitionSpec = { + when { + NucleusState.Disabled isTransitioningTo NucleusState.Idle -> tween(durationMillis = 300) + NucleusState.Idle isTransitioningTo NucleusState.Disabled -> tween(durationMillis = 1000) + else -> snap() } - }, - animationSpec = tween(durationMillis = 500) - ) + } + ) { state -> + when (state) { + NucleusState.Disabled -> LocalContentColor.current.copy(alpha = 0.38f) + NucleusState.Pressed -> MaterialTheme.colorScheme.primaryFixedDim + NucleusState.Idle -> { + when (val uiState = currentUiState.value) { + is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { + CaptureMode.STANDARD -> imageCaptureModeColor + CaptureMode.IMAGE_ONLY -> imageCaptureModeColor + CaptureMode.VIDEO_ONLY -> recordingColor + } + + is CaptureButtonUiState.Available.Recording -> recordingColor + is CaptureButtonUiState.Unavailable -> Color.Transparent + } + } + } + } // this box contains and centers everything Box(modifier = modifier.offset(x = offsetX), contentAlignment = Alignment.Center) { @@ -675,16 +709,6 @@ private fun CaptureButtonNucleus( modifier = Modifier .size(centerShapeSize) .clip(CircleShape) - .alpha( - if (isPressed && - currentUiState.value == - CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY) - ) { - .5f // transparency to indicate click ONLY on IMAGE_ONLY - } else { - 1f // solid alpha the rest of the time - } - ) .background(animatedColor) ) {} } @@ -728,18 +752,20 @@ private fun PreviewCaptureButton( contentAlignment: Alignment = Alignment.Center, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { - Surface(color = Color.Black, contentColor = Color.White, modifier = modifier) { - Box(contentAlignment = contentAlignment) { - CaptureButton( - modifier = Modifier, - onImageCapture = {}, - onStartRecording = {}, - onStopRecording = {}, - onLockVideoRecording = {}, - onIncrementZoom = {}, - captureButtonUiState = captureButtonUiState, - interactionSource = interactionSource - ) + MaterialTheme(colorScheme = darkColorScheme()) { + Surface(color = Color.Black, contentColor = Color.White, modifier = modifier) { + Box(contentAlignment = contentAlignment) { + CaptureButton( + modifier = Modifier, + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = captureButtonUiState, + interactionSource = interactionSource + ) + } } } } @@ -804,14 +830,23 @@ private fun IdleVideoOnlyCaptureButtonDisabledPreview() { @Preview @Composable private fun PressedImageCaptureButtonPreview() { - val interactionSource = remember { MutableInteractionSource() } - LaunchedEffect(Unit) { - interactionSource.emit(PressInteraction.Press(Offset.Zero)) + // Manually constructed preview to verify visual state without relying on interaction source + MaterialTheme(colorScheme = darkColorScheme()) { + Surface(color = Color.Black, contentColor = Color.White) { + Box(contentAlignment = Alignment.Center) { + CaptureButtonRing( + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, + color = Color.White + ) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), + isPressed = true, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) + } + } + } } - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), - interactionSource = interactionSource - ) } @Preview @@ -845,7 +880,8 @@ private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, switchPosition = MINIMUM_LOCK_THRESHOLD, onToggleSwitchPosition = {}, - shouldBeLocked = { true } + shouldBeLocked = { true }, + isPressed = false ) } } @@ -863,7 +899,8 @@ private fun LockSwitchLockedPressedRecordingPreview() { switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, switchPosition = 1f, onToggleSwitchPosition = {}, - shouldBeLocked = { true } + shouldBeLocked = { true }, + isPressed = false ) } } From 4737c83107b987567ed70dbb1893f085dce8913f Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 19:13:07 -0800 Subject: [PATCH 16/40] Make capture button nucles correct color and size during capture Also animates to/from the pressed state --- .../capture/CaptureButtonComponents.kt | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 64cbc8f37..4ee8e7a67 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -21,6 +21,7 @@ import androidx.compose.animation.ExitTransition import androidx.compose.animation.animateColor import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween @@ -348,7 +349,11 @@ private fun CaptureButton( } else { LocalContentColor.current }, - animationSpec = tween(durationMillis = if (isVisuallyDisabled) 1000 else 300), + animationSpec = if (isVisuallyDisabled) { + tween(durationMillis = 1000) + } else { + tween(durationMillis = 300) + }, label = "Capture Button Color" ) @@ -639,7 +644,7 @@ private fun CaptureButtonNucleus( val currentUiState = rememberUpdatedState(captureButtonUiState) // smoothly animate between the size changes of the capture button center - val centerShapeSize by animateDpAsState( + val standardShapeSize by animateDpAsState( targetValue = when (val uiState = currentUiState.value) { // inner circle fills white ring when locked CaptureButtonUiState.Available.Recording.LockedRecording -> captureButtonSize.dp @@ -659,6 +664,31 @@ private fun CaptureButtonNucleus( }, animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) ) + + val pressTransition = updateTransition( + targetState = isPressed && + currentUiState.value.let { + it is CaptureButtonUiState.Available.Idle && it.captureMode == CaptureMode.IMAGE_ONLY + }, + label = "Press Size Transition" + ) + + val centerShapeSize by pressTransition.animateDp( + transitionSpec = { + if (targetState) { + snap() + } else { + tween(durationMillis = 200) + } + }, + label = "Nucleus Size" + ) { isPressedImage -> + if (isPressedImage) { + captureButtonSize.dp + } else { + standardShapeSize + } + } // used to fade between red/white in the center of the capture button val isPressableImageMode = currentUiState.value.let { @@ -678,6 +708,7 @@ private fun CaptureButtonNucleus( when { NucleusState.Disabled isTransitioningTo NucleusState.Idle -> tween(durationMillis = 300) NucleusState.Idle isTransitioningTo NucleusState.Disabled -> tween(durationMillis = 1000) + NucleusState.Pressed isTransitioningTo NucleusState.Idle -> tween(durationMillis = 100) else -> snap() } } From 539a2266c3544d999d73de116f7e7f994ef607b1 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Mon, 12 Jan 2026 19:21:39 -0800 Subject: [PATCH 17/40] Ensure capture animation works with volume buttons --- .../capture/CaptureButtonComponents.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 4ee8e7a67..5855e5d65 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -186,6 +186,9 @@ fun CaptureButton( val scope = rememberCoroutineScope() val longPressTimeout = LocalViewConfiguration.current.longPressTimeoutMillis + // To handle press interactions from key events + var currentPressInteraction by remember { mutableStateOf(null) } + LaunchedEffect(captureButtonUiState) { if (captureButtonUiState is CaptureButtonUiState.Available.Idle) { onLockVideoRecording(false) @@ -221,6 +224,16 @@ fun CaptureButton( if (!captureButtonUiState.isEnabled) return if (firstKeyPressed.value == null) { firstKeyPressed.value = captureSource + + // Emit press interaction for key events to trigger UI feedback + if (captureSource != CaptureSource.CAPTURE_BUTTON) { + val press = PressInteraction.Press(Offset.Zero) + currentPressInteraction = press + scope.launch { + interactionSource.emit(press) + } + } + longPressJob = scope.launch { delay(longPressTimeout) onLongPress() @@ -231,6 +244,18 @@ fun CaptureButton( fun onKeyUp(captureSource: CaptureSource, isLocked: Boolean = false) { // releasing while pressed recording if (firstKeyPressed.value == captureSource) { + // Emit release interaction for key events + if (captureSource != CaptureSource.CAPTURE_BUTTON) { + val interactionToRelease = currentPressInteraction + currentPressInteraction = null + if (interactionToRelease != null) { + scope.launch { + delay(50) // Ensure visible press state for fast taps + interactionSource.emit(PressInteraction.Release(interactionToRelease)) + } + } + } + if (isLongPressing.value) { if (!isLocked && currentUiState.value is From 828c02e0c83a446fd63278323a4a91b6a61b3c64 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 13 Jan 2026 17:21:31 -0800 Subject: [PATCH 18/40] Apply Spotless --- .../capture/CaptureButtonComponents.kt | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 5855e5d65..00a5b7d45 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -629,7 +629,9 @@ private fun LockSwitchCaptureButtonNucleus( } private enum class NucleusState { - Disabled, Idle, Pressed + Disabled, + Idle, + Pressed } /** @@ -689,11 +691,12 @@ private fun CaptureButtonNucleus( }, animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) ) - + val pressTransition = updateTransition( targetState = isPressed && currentUiState.value.let { - it is CaptureButtonUiState.Available.Idle && it.captureMode == CaptureMode.IMAGE_ONLY + it is CaptureButtonUiState.Available.Idle && + it.captureMode == CaptureMode.IMAGE_ONLY }, label = "Press Size Transition" ) @@ -718,7 +721,7 @@ private fun CaptureButtonNucleus( // used to fade between red/white in the center of the capture button val isPressableImageMode = currentUiState.value.let { it is CaptureButtonUiState.Available.Idle && - (it.captureMode == CaptureMode.IMAGE_ONLY || it.captureMode == CaptureMode.STANDARD) + (it.captureMode == CaptureMode.IMAGE_ONLY || it.captureMode == CaptureMode.STANDARD) } val nucleusState = when { isVisuallyDisabled -> NucleusState.Disabled @@ -726,14 +729,21 @@ private fun CaptureButtonNucleus( else -> NucleusState.Idle } - val transition = updateTransition(targetState = nucleusState, label = "Nucleus Color Transition") + val transition = + updateTransition(targetState = nucleusState, label = "Nucleus Color Transition") val animatedColor by transition.animateColor( label = "Nucleus Color", transitionSpec = { when { - NucleusState.Disabled isTransitioningTo NucleusState.Idle -> tween(durationMillis = 300) - NucleusState.Idle isTransitioningTo NucleusState.Disabled -> tween(durationMillis = 1000) - NucleusState.Pressed isTransitioningTo NucleusState.Idle -> tween(durationMillis = 100) + NucleusState.Disabled isTransitioningTo NucleusState.Idle -> tween( + durationMillis = 300 + ) + NucleusState.Idle isTransitioningTo NucleusState.Disabled -> tween( + durationMillis = 1000 + ) + NucleusState.Pressed isTransitioningTo NucleusState.Idle -> tween( + durationMillis = 100 + ) else -> snap() } } @@ -895,7 +905,9 @@ private fun PressedImageCaptureButtonPreview() { color = Color.White ) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY), + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.IMAGE_ONLY + ), isPressed = true, captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE ) From 940a090e6c6c2cf5778d31078a93c25f18acb2c9 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 13 Jan 2026 17:38:23 -0800 Subject: [PATCH 19/40] Adjust size of capture button, ring stroke, and nucleus to match mocks --- .../ui/components/capture/CaptureButtonComponents.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 00a5b7d45..670153339 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -90,7 +90,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch private const val TAG = "CaptureButton" -private const val DEFAULT_CAPTURE_BUTTON_SIZE = 80f +private const val DEFAULT_CAPTURE_BUTTON_SIZE = 76f // scales against the size of the capture button private const val LOCK_SWITCH_PRESSED_NUCLEUS_SCALE = .5f @@ -519,7 +519,7 @@ fun CaptureButtonRing( modifier: Modifier = Modifier, captureButtonSize: Float, color: Color, - borderWidth: Float = 4f, + borderWidth: Float = 3f, contents: (@Composable () -> Unit)? = null ) { Box(modifier = modifier, contentAlignment = Alignment.Center) { @@ -653,8 +653,8 @@ private fun CaptureButtonNucleus( offsetX: Dp = 0.dp, recordingColor: Color = Color.Red, imageCaptureModeColor: Color = Color.White, - idleImageCaptureScale: Float = .7f, - idleVideoCaptureScale: Float = .35f, + idleImageCaptureScale: Float = .74f, + idleVideoCaptureScale: Float = .26f, pressedVideoCaptureScale: Float = .7f, isVisuallyDisabled: Boolean = false ) { From 098cc251ddc92b42b949de20550341402dcae8bb Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 13 Jan 2026 17:49:32 -0800 Subject: [PATCH 20/40] Make capture button nucleus white when in idle VIDEO_ONLY mode --- .../ui/components/capture/CaptureButtonComponents.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 670153339..39b92d033 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -756,7 +756,8 @@ private fun CaptureButtonNucleus( is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { CaptureMode.STANDARD -> imageCaptureModeColor CaptureMode.IMAGE_ONLY -> imageCaptureModeColor - CaptureMode.VIDEO_ONLY -> recordingColor + CaptureMode.VIDEO_ONLY -> + if (isPressed) recordingColor else imageCaptureModeColor } is CaptureButtonUiState.Available.Recording -> recordingColor From 6e8f7c35175ffc44610bc50fb15b62cb1d9a85c7 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 13 Jan 2026 17:58:06 -0800 Subject: [PATCH 21/40] Add 50% black background to capture button to match mocks --- .../ui/components/capture/CaptureButtonComponents.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 39b92d033..8caafe8cf 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -523,6 +523,11 @@ fun CaptureButtonRing( contents: (@Composable () -> Unit)? = null ) { Box(modifier = modifier, contentAlignment = Alignment.Center) { + Box( + modifier = Modifier + .size(captureButtonSize.dp) + .background(Color.Black.copy(alpha = 0.5f), CircleShape) + ) contents?.invoke() // todo(): use a canvas instead of a box. // the sizing gets funny so the scales need to be completely readjusted From 647522bb7eb0468ff56b2e7f447640b26e0131a7 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 13 Jan 2026 18:30:12 -0800 Subject: [PATCH 22/40] Update compose previews to use a gradient background for higher vis --- .../capture/CaptureButtonComponents.kt | 76 +++++++++++++------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 8caafe8cf..c89642d10 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -51,9 +51,9 @@ import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -72,6 +72,7 @@ import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged @@ -807,12 +808,7 @@ private fun CaptureButtonNucleus( @Preview @Composable private fun CaptureButtonUnavailablePreview() { - CaptureButton( - onImageCapture = {}, - onStartRecording = {}, - onStopRecording = {}, - onLockVideoRecording = {}, - onIncrementZoom = {}, + PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Unavailable ) } @@ -825,8 +821,16 @@ private fun PreviewCaptureButton( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { MaterialTheme(colorScheme = darkColorScheme()) { - Surface(color = Color.Black, contentColor = Color.White, modifier = modifier) { - Box(contentAlignment = contentAlignment) { + CompositionLocalProvider(LocalContentColor provides Color.White) { + Box( + modifier = modifier + .background( + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) + ) + ), + contentAlignment = contentAlignment + ) { CaptureButton( modifier = Modifier, onImageCapture = {}, @@ -904,20 +908,26 @@ private fun IdleVideoOnlyCaptureButtonDisabledPreview() { private fun PressedImageCaptureButtonPreview() { // Manually constructed preview to verify visual state without relying on interaction source MaterialTheme(colorScheme = darkColorScheme()) { - Surface(color = Color.Black, contentColor = Color.White) { - Box(contentAlignment = Alignment.Center) { - CaptureButtonRing( - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - color = Color.White - ) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle( - CaptureMode.IMAGE_ONLY - ), - isPressed = true, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + Box( + modifier = Modifier + .background( + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) ) - } + ), + contentAlignment = Alignment.Center + ) { + CaptureButtonRing( + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, + color = Color.White + ) { + CaptureButtonNucleus( + captureButtonUiState = CaptureButtonUiState.Available.Idle( + CaptureMode.IMAGE_ONLY + ), + isPressed = true, + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE + ) } } } @@ -946,7 +956,16 @@ private fun LockedRecordingPreview() { @Composable private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { // box is here to account for the offset lock switch - Box(modifier = Modifier.width(150.dp), contentAlignment = Alignment.CenterEnd) { + Box( + modifier = Modifier + .width(150.dp) + .background( + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) + ) + ), + contentAlignment = Alignment.CenterEnd + ) { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, @@ -965,7 +984,16 @@ private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { @Composable private fun LockSwitchLockedPressedRecordingPreview() { // box is here to account for the offset lock switch - Box(modifier = Modifier.width(150.dp), contentAlignment = Alignment.CenterEnd) { + Box( + modifier = Modifier + .width(150.dp) + .background( + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) + ) + ), + contentAlignment = Alignment.CenterEnd + ) { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, From 9d3a49885fe14f0b4b8df2f6f2f8c2f2b1900a6e Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 06:09:04 +0000 Subject: [PATCH 23/40] Setup build environment and dependencies for testing and screenshots --- gradle.properties | 3 +- gradle/libs.versions.toml | 4 +++ ui/components/capture/build.gradle.kts | 11 ++++++ .../components/capture/CaptureButtonTest.kt | 35 +++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt diff --git a/gradle.properties b/gradle.properties index 15ab33aba..c6243cc14 100644 --- a/gradle.properties +++ b/gradle.properties @@ -44,4 +44,5 @@ android.nonFinalResIds=false android.experimental.testOptions.managedDevices.maxConcurrentDevices=1 android.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 # Ensure we can run managed devices on servers that don't support hardware rendering -android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect \ No newline at end of file +android.testoptions.manageddevices.emulator.gpu=swiftshader_indirect +android.experimental.enableScreenshotTest=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c4f9b336..c6cdc2b97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ accompanist = "0.37.3" kotlinPlugin = "2.2.0" androidGradlePlugin = "8.10.1" protobufPlugin = "0.9.5" +composeScreenshot = "0.0.1-alpha14" androidxActivityCompose = "1.10.1" androidxAppCompat = "1.7.1" @@ -64,6 +65,7 @@ androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreKtx" } androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidxDatastore" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxTestEspresso" } +androidx-espresso-accessibility = { module = "androidx.test.espresso:espresso-accessibility", version.ref = "androidxTestEspresso" } androidx-graphics-core = { module = "androidx.graphics:graphics-core", version.ref = "androidxGraphicsCore" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestJunit" } androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidxLifecycle" } @@ -88,6 +90,7 @@ compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeB compose-junit = { module = "androidx.compose.ui:ui-test-junit4" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial" } compose-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +screenshot-validation-api = { module = "com.android.tools.screenshot:screenshot-validation-api", version.ref = "composeScreenshot" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } @@ -121,3 +124,4 @@ dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hi google-protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinPlugin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlinPlugin" } +compose-screenshot = { id = "com.android.compose.screenshot", version.ref = "composeScreenshot" } diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 70395892c..82fe6a4a3 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -19,12 +19,15 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.kapt) alias(libs.plugins.compose.compiler) + alias(libs.plugins.compose.screenshot) } android { namespace = "com.google.jetpackcamera.ui.components.capture" compileSdk = libs.versions.compileSdk.get().toInt() + experimentalProperties["android.experimental.enableScreenshotTest"] = true + defaultConfig { minSdk = libs.versions.minSdk.get().toInt() testOptions.targetSdk = libs.versions.targetSdk.get().toInt() @@ -87,6 +90,8 @@ dependencies { // noinspection TestManifestGradleConfiguration: required for release build unit tests testImplementation(libs.compose.test.manifest) testImplementation(libs.compose.junit) + screenshotTestImplementation(libs.screenshot.validation.api) + screenshotTestImplementation(libs.compose.ui.tooling) // Testing testImplementation(libs.junit) @@ -98,6 +103,7 @@ dependencies { implementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.espresso.accessibility) implementation(project(":ui:uistate")) implementation(project(":ui:uistate:capture")) @@ -116,3 +122,8 @@ dependencies { kapt { correctErrorTypes = true } +configurations.all { + resolutionStrategy { + exclude(group = "com.google.protobuf", module = "protobuf-lite") + } +} diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt new file mode 100644 index 000000000..cdb52d8df --- /dev/null +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -0,0 +1,35 @@ +package com.google.jetpackcamera.ui.components.capture + +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CaptureButtonTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun captureButton_exists() { + composeTestRule.setContent { + CaptureButton( + modifier = Modifier.testTag("CaptureButtonTestTag"), + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) + ) + } + + composeTestRule.onNodeWithTag("CaptureButtonTestTag").assertExists() + } +} From 0c6620f4312518646f71ef4991a5facef9c25277 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 06:10:23 +0000 Subject: [PATCH 24/40] Improve accessibility of CaptureButton with content descriptions and roles --- .gemini/styleguide.md | 4 + .../components/capture/CaptureButtonTest.kt | 74 ++++++++++++++++++- .../capture/CaptureButtonComponents.kt | 12 +++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index dd76e4e19..3072470e0 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -87,6 +87,10 @@ When reviewing a pull request, focus on the following key areas: * **Apply Proper Semantics:** When building custom UI components from the ground up (e.g., a custom button made of an `Icon` and a `Text`), apply the correct semantics to ensure they are accessible. * Use `semantics { role = Role.Button }` (or `Role.Checkbox`, etc.) to define the component's logical purpose for screen readers. * For components made of multiple parts that should be read as a single, coherent unit, use `semantics { mergeDescendants = true }`. This prevents screen readers from announcing inner elements (like an icon and its text label) as separate, unrelated items. + * **Content vs State Descriptions:** + * Use `contentDescription` to describe the **identity** or **action** of the component (e.g., "Capture Photo", "Start Video Recording"). + * Use `stateDescription` to describe the **current state** of the component (e.g., "Locked", "Selected"). + * **Avoid Redundancy:** Do not include state information or control type in `contentDescription` (e.g., avoid "Locked Video Button" or "Shutter Button"). Let the system announce role and state automatically. ## Rules for Providing Feedback * **Be Constructive:** Frame feedback as suggestions, not commands. Explain the reasoning ("why") behind each comment. diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt index cdb52d8df..d61359301 100644 --- a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -4,20 +4,32 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import androidx.test.espresso.accessibility.AccessibilityChecks +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.Role @RunWith(AndroidJUnit4::class) class CaptureButtonTest { @get:Rule val composeTestRule = createComposeRule() + @Before + fun setUp() { + AccessibilityChecks.enable() + } + @Test - fun captureButton_exists() { + fun captureButton_standard_exists() { composeTestRule.setContent { CaptureButton( modifier = Modifier.testTag("CaptureButtonTestTag"), @@ -31,5 +43,65 @@ class CaptureButtonTest { } composeTestRule.onNodeWithTag("CaptureButtonTestTag").assertExists() + composeTestRule.onNodeWithTag("CaptureButtonTestTag").assertContentDescriptionEquals("Capture Photo") + composeTestRule.onNodeWithTag("CaptureButtonTestTag", useUnmergedTree = true) + .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) + } + + @Test + fun captureButton_imageOnly_exists() { + composeTestRule.setContent { + CaptureButton( + modifier = Modifier.testTag("CaptureButtonImageOnly"), + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + ) + } + composeTestRule.onNodeWithTag("CaptureButtonImageOnly").assertExists() + composeTestRule.onNodeWithTag("CaptureButtonImageOnly").assertContentDescriptionEquals("Capture Photo") + composeTestRule.onNodeWithTag("CaptureButtonImageOnly", useUnmergedTree = true) + .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) + } + + @Test + fun captureButton_videoOnly_exists() { + composeTestRule.setContent { + CaptureButton( + modifier = Modifier.testTag("CaptureButtonVideoOnly"), + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) + ) + } + composeTestRule.onNodeWithTag("CaptureButtonVideoOnly").assertExists() + composeTestRule.onNodeWithTag("CaptureButtonVideoOnly").assertContentDescriptionEquals("Start Video Recording") + composeTestRule.onNodeWithTag("CaptureButtonVideoOnly", useUnmergedTree = true) + .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) + } + + @Test + fun captureButton_lockedRecording_exists() { + composeTestRule.setContent { + CaptureButton( + modifier = Modifier.testTag("CaptureButtonLocked"), + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording + ) + } + composeTestRule.onNodeWithTag("CaptureButtonLocked").assertExists() + composeTestRule.onNodeWithTag("CaptureButtonLocked").assertContentDescriptionEquals("Stop Video Recording") + composeTestRule.onNodeWithTag("CaptureButtonLocked", useUnmergedTree = true) + .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index a05d3f349..63877b76b 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -77,6 +77,7 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -503,9 +504,20 @@ private fun CaptureButton( Rect(0f, 0f, it.width.toFloat(), it.height.toFloat()) } .semantics { + role = androidx.compose.ui.semantics.Role.Button if (!captureButtonUiState.isEnabled) { disabled() } + contentDescription = when (val uiState = captureButtonUiState) { + is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { + CaptureMode.STANDARD -> "Capture Photo" + CaptureMode.IMAGE_ONLY -> "Capture Photo" + CaptureMode.VIDEO_ONLY -> "Start Video Recording" + } + CaptureButtonUiState.Enabled.Recording.PressedRecording -> "Recording Video" + CaptureButtonUiState.Enabled.Recording.LockedRecording -> "Stop Video Recording" + CaptureButtonUiState.Unavailable -> "Capture Button Unavailable" + } } .then(gestureModifier), captureButtonSize = captureButtonSize, From b8e750abac9a9f7602c769e434de0fb326c69af1 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 06:13:05 +0000 Subject: [PATCH 25/40] Update CaptureButton visuals and animations to match spec --- .../capture/CaptureButtonComponents.kt | 216 ++++++++---------- 1 file changed, 95 insertions(+), 121 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 63877b76b..7bbc0d263 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -51,6 +51,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.animation.animateColorAsState import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -78,6 +79,8 @@ import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -215,7 +218,7 @@ internal fun CaptureButton( var currentPressInteraction by remember { mutableStateOf(null) } LaunchedEffect(captureButtonUiState) { - if (captureButtonUiState is CaptureButtonUiState.Available.Idle) { + if (captureButtonUiState is CaptureButtonUiState.Enabled.Idle) { onLockVideoRecording(false) } else if (captureButtonUiState is CaptureButtonUiState.Enabled.Recording.LockedRecording @@ -228,7 +231,7 @@ internal fun CaptureButton( fun onLongPress() { if (!isLongPressing.value) { when (val current = currentUiState.value) { - is CaptureButtonUiState.Available.Idle -> when (current.captureMode) { + is CaptureButtonUiState.Enabled.Idle -> when (current.captureMode) { CaptureMode.STANDARD, CaptureMode.VIDEO_ONLY -> { isLongPressing.value = true @@ -284,7 +287,7 @@ internal fun CaptureButton( if (isLongPressing.value) { if (!isLocked && currentUiState.value is - CaptureButtonUiState.Available.Recording.PressedRecording + CaptureButtonUiState.Enabled.Recording.PressedRecording ) { onStopRecording() } @@ -292,7 +295,7 @@ internal fun CaptureButton( // on click else { when (val current = currentUiState.value) { - is CaptureButtonUiState.Available.Idle -> when (current.captureMode) { + is CaptureButtonUiState.Enabled.Idle -> when (current.captureMode) { CaptureMode.STANDARD, CaptureMode.IMAGE_ONLY -> onImageCapture() @@ -302,8 +305,8 @@ internal fun CaptureButton( } } - CaptureButtonUiState.Available.Recording.LockedRecording -> onStopRecording() - CaptureButtonUiState.Available.Recording.PressedRecording, + CaptureButtonUiState.Enabled.Recording.LockedRecording -> onStopRecording() + CaptureButtonUiState.Enabled.Recording.PressedRecording, CaptureButtonUiState.Unavailable -> { } } @@ -393,12 +396,24 @@ private fun CaptureButton( val isPressedInteraction by interactionSource.collectIsPressedAsState() val animatedColor by animateColorAsState( - targetValue = if (isVisuallyDisabled) { - LocalContentColor.current.copy(alpha = 0.38f) - } else { - LocalContentColor.current + targetValue = when { + isVisuallyDisabled -> { + if (currentUiState.value.let { + it is CaptureButtonUiState.Enabled.Idle && + it.captureMode == CaptureMode.STANDARD + }) { + LocalContentColor.current.copy(alpha = 0.2f) + } else { + Color.Transparent + } + } + currentUiState.value.let { + it is CaptureButtonUiState.Enabled.Idle && + it.captureMode == CaptureMode.STANDARD + } -> LocalContentColor.current + else -> Color.Transparent }, - animationSpec = tween(durationMillis = if (isVisuallyDisabled) 1000 else 300), + animationSpec = tween(durationMillis = if (isVisuallyDisabled) 500 else 150), label = "Capture Button Color" ) @@ -464,7 +479,7 @@ private fun CaptureButton( onDragCancel = {}, onDrag = { change, deltaOffset -> if (currentUiState.value == - CaptureButtonUiState.Available.Recording.PressedRecording + CaptureButtonUiState.Enabled.Recording.PressedRecording ) { val newPoint = change.position @@ -504,7 +519,7 @@ private fun CaptureButton( Rect(0f, 0f, it.width.toFloat(), it.height.toFloat()) } .semantics { - role = androidx.compose.ui.semantics.Role.Button + role = Role.Button if (!captureButtonUiState.isEnabled) { disabled() } @@ -545,19 +560,36 @@ private fun CaptureButton( } } +enum class ShutterBackgroundStyle { + WHITE_20, + BLACK_60 +} + +val LocalShutterBackgroundStyle = androidx.compose.runtime.compositionLocalOf { ShutterBackgroundStyle.WHITE_20 } + @Composable -private fun CaptureButtonRing( +internal fun CaptureButtonRing( modifier: Modifier = Modifier, captureButtonSize: Float, color: Color, borderWidth: Float = 3f, contents: (@Composable () -> Unit)? = null ) { + val backgroundStyle = LocalShutterBackgroundStyle.current + val targetBackgroundColor = when (backgroundStyle) { + ShutterBackgroundStyle.WHITE_20 -> Color.White.copy(alpha = 0.2f) + ShutterBackgroundStyle.BLACK_60 -> Color.Black.copy(alpha = 0.6f) + } + val backgroundColor by animateColorAsState( + targetValue = targetBackgroundColor, + animationSpec = androidx.compose.animation.core.tween(durationMillis = 150), + label = "backgroundColor" + ) Box(modifier = modifier, contentAlignment = Alignment.Center) { Box( modifier = Modifier .size(captureButtonSize.dp) - .background(Color.Black.copy(alpha = 0.5f), CircleShape) + .background(backgroundColor, CircleShape) ) contents?.invoke() // todo(): use a canvas instead of a box. @@ -606,7 +638,7 @@ private fun LockSwitchCaptureButtonNucleus( // grey cylinder offset to the left and fades in when pressed recording AnimatedVisibility( visible = captureButtonUiState == - CaptureButtonUiState.Available.Recording.PressedRecording, + CaptureButtonUiState.Enabled.Recording.PressedRecording, enter = fadeIn(), exit = ExitTransition.None ) { @@ -639,7 +671,7 @@ private fun LockSwitchCaptureButtonNucleus( // locked icon, matches cylinder offset AnimatedVisibility( visible = captureButtonUiState == - CaptureButtonUiState.Available.Recording.PressedRecording, + CaptureButtonUiState.Enabled.Recording.PressedRecording, enter = fadeIn(), exit = ExitTransition.None ) { @@ -681,7 +713,7 @@ private enum class NucleusState { * @param pressedVideoCaptureScale the scale factor for the pressed size of the video-only nucleus. Must be between 0 and 1. */ @Composable -private fun CaptureButtonNucleus( +internal fun CaptureButtonNucleus( modifier: Modifier = Modifier, captureButtonUiState: CaptureButtonUiState, isPressed: Boolean, @@ -689,9 +721,9 @@ private fun CaptureButtonNucleus( offsetX: Dp = 0.dp, recordingColor: Color = Color.Red, imageCaptureModeColor: Color = Color.White, - idleImageCaptureScale: Float = .74f, - idleVideoCaptureScale: Float = .26f, - pressedVideoCaptureScale: Float = .7f, + idleImageCaptureScale: Float = 0.86f, + idleVideoCaptureScale: Float = 0.64f, + pressedVideoCaptureScale: Float = 0.86f, isVisuallyDisabled: Boolean = false ) { require(idleImageCaptureScale in 0f..1f) { @@ -709,14 +741,14 @@ private fun CaptureButtonNucleus( // smoothly animate between the size changes of the capture button center val standardShapeSize by animateDpAsState( targetValue = when (val uiState = currentUiState.value) { - // inner circle fills white ring when locked - CaptureButtonUiState.Available.Recording.LockedRecording -> captureButtonSize.dp + // inner circle becomes a square when locked + CaptureButtonUiState.Enabled.Recording.LockedRecording -> (captureButtonSize * 0.51f).dp - CaptureButtonUiState.Available.Recording.PressedRecording -> + CaptureButtonUiState.Enabled.Recording.PressedRecording -> (captureButtonSize * pressedVideoCaptureScale).dp CaptureButtonUiState.Unavailable -> 0.dp - is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { + is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { // no inner circle will be visible on STANDARD CaptureMode.STANDARD -> 0.dp // large white circle will be visible on IMAGE_ONLY @@ -725,14 +757,14 @@ private fun CaptureButtonNucleus( CaptureMode.VIDEO_ONLY -> (captureButtonSize * idleVideoCaptureScale).dp } }, - animationSpec = tween(durationMillis = 500, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) ) val pressTransition = updateTransition( targetState = isPressed && currentUiState.value.let { - it is CaptureButtonUiState.Available.Idle && - it.captureMode == CaptureMode.IMAGE_ONLY + it is CaptureButtonUiState.Enabled.Idle && + (it.captureMode == CaptureMode.IMAGE_ONLY || it.captureMode == CaptureMode.STANDARD) }, label = "Press Size Transition" ) @@ -742,21 +774,35 @@ private fun CaptureButtonNucleus( if (targetState) { snap() } else { - tween(durationMillis = 200) + tween(durationMillis = 100) } }, label = "Nucleus Size" ) { isPressedImage -> if (isPressedImage) { - captureButtonSize.dp + (captureButtonSize * 0.93f).dp } else { standardShapeSize } } + val sizeFinal = (captureButtonSize * 0.51f).dp + val sizeInter = (captureButtonSize * 0.58f).dp + val cornerRadius = if (currentUiState.value is CaptureButtonUiState.Enabled.Recording.LockedRecording) { + if (centerShapeSize <= sizeInter) { + val fraction = (centerShapeSize - sizeFinal) / (sizeInter - sizeFinal) + val coercedFraction = fraction.coerceIn(0f, 1f) + 8.dp + (centerShapeSize / 2 - 8.dp) * coercedFraction + } else { + centerShapeSize / 2 + } + } else { + centerShapeSize / 2 + } + // used to fade between red/white in the center of the capture button val isPressableImageMode = currentUiState.value.let { - it is CaptureButtonUiState.Available.Idle && + it is CaptureButtonUiState.Enabled.Idle && (it.captureMode == CaptureMode.IMAGE_ONLY || it.captureMode == CaptureMode.STANDARD) } val nucleusState = when { @@ -772,31 +818,31 @@ private fun CaptureButtonNucleus( transitionSpec = { when { NucleusState.Disabled isTransitioningTo NucleusState.Idle -> tween( - durationMillis = 300 + durationMillis = 150 ) NucleusState.Idle isTransitioningTo NucleusState.Disabled -> tween( - durationMillis = 1000 + durationMillis = 500 ) NucleusState.Pressed isTransitioningTo NucleusState.Idle -> tween( - durationMillis = 100 + durationMillis = 50 ) else -> snap() } } ) { state -> when (state) { - NucleusState.Disabled -> LocalContentColor.current.copy(alpha = 0.38f) - NucleusState.Pressed -> MaterialTheme.colorScheme.primaryFixedDim + NucleusState.Disabled -> Color.Black.copy(alpha = 0.6f) + NucleusState.Pressed -> imageCaptureModeColor NucleusState.Idle -> { when (val uiState = currentUiState.value) { - is CaptureButtonUiState.Available.Idle -> when (uiState.captureMode) { + is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { CaptureMode.STANDARD -> imageCaptureModeColor CaptureMode.IMAGE_ONLY -> imageCaptureModeColor CaptureMode.VIDEO_ONLY -> if (isPressed) recordingColor else imageCaptureModeColor } - is CaptureButtonUiState.Available.Recording -> recordingColor + is CaptureButtonUiState.Enabled.Recording -> recordingColor is CaptureButtonUiState.Unavailable -> Color.Transparent } } @@ -811,27 +857,10 @@ private fun CaptureButtonNucleus( contentAlignment = Alignment.Center, modifier = Modifier .size(centerShapeSize) - .clip(CircleShape) + .clip(androidx.compose.foundation.shape.RoundedCornerShape(cornerRadius)) .background(animatedColor) ) {} } - // central "square" stop icon - AnimatedVisibility( - visible = currentUiState.value is - CaptureButtonUiState.Available.Recording.LockedRecording, - enter = scaleIn(initialScale = .5f) + fadeIn(), - exit = fadeOut() - ) { - val smallBoxSize = (captureButtonSize / 5f).dp - Canvas(modifier = Modifier) { - drawRoundRect( - color = Color.White, - topLeft = Offset(-smallBoxSize.toPx() / 2f, -smallBoxSize.toPx() / 2f), - size = Size(smallBoxSize.toPx(), smallBoxSize.toPx()), - cornerRadius = CornerRadius(smallBoxSize.toPx() * .15f) - ) - } - } } } @@ -844,14 +873,16 @@ private fun CaptureButtonUnavailablePreview() { } @Composable -private fun PreviewCaptureButton( +internal fun PreviewCaptureButton( captureButtonUiState: CaptureButtonUiState, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { MaterialTheme(colorScheme = darkColorScheme()) { - CompositionLocalProvider(LocalContentColor provides Color.White) { + CompositionLocalProvider( + LocalContentColor provides Color.White + ) { Box( modifier = modifier .background( @@ -875,14 +906,12 @@ private fun PreviewCaptureButton( } } } - } -} @Preview @Composable private fun IdleStandardCaptureButtonPreview() { PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.STANDARD) + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) ) } @@ -935,62 +964,7 @@ private fun IdleVideoOnlyCaptureButtonDisabledPreview() { ) } -@Preview -@Composable -private fun IdleVideoOnlyCaptureButtonPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } -} - -@Preview -@Composable -private fun IdleStandardCaptureButtonDisabledPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.STANDARD, - isEnabled = false - ), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } -} -@Preview -@Composable -private fun IdleImageCaptureButtonDisabledPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.IMAGE_ONLY, - isEnabled = false - ), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } -} - -@Preview -@Composable -private fun IdleVideoOnlyCaptureButtonDisabledPreview() { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Gray) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.VIDEO_ONLY, - isEnabled = false - ), - isPressed = false, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } -} @Preview @Composable @@ -1011,7 +985,7 @@ private fun PressedImageCaptureButtonPreview() { color = Color.White ) { CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Available.Idle( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( CaptureMode.IMAGE_ONLY ), isPressed = true, @@ -1027,7 +1001,7 @@ private fun PressedImageCaptureButtonPreview() { private fun LockSwitchUnlockedPressedRecordingPreview() { // box is here to account for the offset lock switch PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, modifier = Modifier.width(150.dp), contentAlignment = Alignment.CenterEnd ) @@ -1037,7 +1011,7 @@ private fun LockSwitchUnlockedPressedRecordingPreview() { @Composable private fun LockedRecordingPreview() { PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Available.Recording.LockedRecording + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording ) } @@ -1058,7 +1032,7 @@ private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, switchPosition = MINIMUM_LOCK_THRESHOLD, onToggleSwitchPosition = {}, @@ -1086,7 +1060,7 @@ private fun LockSwitchLockedPressedRecordingPreview() { CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, switchWidth = (DEFAULT_CAPTURE_BUTTON_SIZE * LOCK_SWITCH_WIDTH_SCALE).dp, switchPosition = 1f, onToggleSwitchPosition = {}, From ef6f3ca9f66cca8560f98dc90617c541d9d62592 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 06:13:22 +0000 Subject: [PATCH 26/40] Implement accurate overlap detection for dynamic background --- .../feature/preview/PreviewScreen.kt | 14 +++-- .../components/capture/CaptureButtonTest.kt | 43 ++++++++++--- .../capture/CaptureButtonComponents.kt | 20 +++--- .../ui/components/capture/CaptureLayout.kt | 63 +++++++++++++++---- .../capture/CaptureScreenComponents.kt | 4 +- 5 files changed, 102 insertions(+), 42 deletions(-) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index fff1e8175..b631e34ff 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -404,8 +404,9 @@ private fun ContentScreen( ) }, - viewfinder = { + viewfinder = { modifier -> PreviewDisplay( + modifier = modifier, previewDisplayUiState = captureUiState.previewDisplayUiState, onFlipCamera = onFlipCamera, onTapToFocus = cameraController?.let { it::tapToFocus } ?: { _, _ -> }, @@ -415,7 +416,7 @@ private fun ContentScreen( focusMeteringUiState = captureUiState.focusMeteringUiState ) }, - captureButton = { + captureButton = { modifier -> fun runCaptureAction(action: () -> Unit) { if ((captureUiState.quickSettingsUiState as? QuickSettingsUiState.Available) ?.quickSettingsIsOpen == true @@ -425,6 +426,7 @@ private fun ContentScreen( action() } CaptureButton( + modifier = modifier, captureButtonUiState = captureUiState.captureButtonUiState, isQuickSettingsOpen = ( captureUiState.quickSettingsUiState as? @@ -717,7 +719,7 @@ private fun ContentScreen_ImageOnly_Idle() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_READY.copy( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.IMAGE_ONLY) + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) ), screenFlashUiState = ScreenFlashUiState(), surfaceRequest = null @@ -731,7 +733,7 @@ private fun ContentScreen_VideoOnly_Idle() { MaterialTheme(colorScheme = darkColorScheme()) { ContentScreen( captureUiState = FAKE_PREVIEW_UI_STATE_READY.copy( - captureButtonUiState = CaptureButtonUiState.Available.Idle(CaptureMode.VIDEO_ONLY) + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) ), screenFlashUiState = ScreenFlashUiState(), surfaceRequest = null @@ -771,12 +773,12 @@ private val FAKE_PREVIEW_UI_STATE_READY = CaptureUiState.Ready( private val FAKE_PREVIEW_UI_STATE_PRESSED_RECORDING = FAKE_PREVIEW_UI_STATE_READY.copy( videoRecordingState = VideoRecordingState.Active.Recording(0, 0.0, 0), - captureButtonUiState = CaptureButtonUiState.Available.Recording.PressedRecording, + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, audioUiState = AudioUiState.Enabled.On(1.0) ) private val FAKE_PREVIEW_UI_STATE_LOCKED_RECORDING = FAKE_PREVIEW_UI_STATE_READY.copy( videoRecordingState = VideoRecordingState.Active.Recording(0, 0.0, 0), - captureButtonUiState = CaptureButtonUiState.Available.Recording.LockedRecording, + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording, audioUiState = AudioUiState.Enabled.On(1.0) ) diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt index d61359301..4653655c8 100644 --- a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -1,10 +1,30 @@ +/* + * 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.ui.components.capture import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.assertContentDescriptionEquals +import androidx.test.espresso.accessibility.AccessibilityChecks import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState @@ -12,11 +32,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import androidx.test.espresso.accessibility.AccessibilityChecks -import androidx.compose.ui.test.assert -import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.semantics.Role @RunWith(AndroidJUnit4::class) class CaptureButtonTest { @@ -43,7 +58,9 @@ class CaptureButtonTest { } composeTestRule.onNodeWithTag("CaptureButtonTestTag").assertExists() - composeTestRule.onNodeWithTag("CaptureButtonTestTag").assertContentDescriptionEquals("Capture Photo") + composeTestRule.onNodeWithTag( + "CaptureButtonTestTag" + ).assertContentDescriptionEquals("Capture Photo") composeTestRule.onNodeWithTag("CaptureButtonTestTag", useUnmergedTree = true) .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) } @@ -62,7 +79,9 @@ class CaptureButtonTest { ) } composeTestRule.onNodeWithTag("CaptureButtonImageOnly").assertExists() - composeTestRule.onNodeWithTag("CaptureButtonImageOnly").assertContentDescriptionEquals("Capture Photo") + composeTestRule.onNodeWithTag( + "CaptureButtonImageOnly" + ).assertContentDescriptionEquals("Capture Photo") composeTestRule.onNodeWithTag("CaptureButtonImageOnly", useUnmergedTree = true) .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) } @@ -81,7 +100,9 @@ class CaptureButtonTest { ) } composeTestRule.onNodeWithTag("CaptureButtonVideoOnly").assertExists() - composeTestRule.onNodeWithTag("CaptureButtonVideoOnly").assertContentDescriptionEquals("Start Video Recording") + composeTestRule.onNodeWithTag( + "CaptureButtonVideoOnly" + ).assertContentDescriptionEquals("Start Video Recording") composeTestRule.onNodeWithTag("CaptureButtonVideoOnly", useUnmergedTree = true) .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) } @@ -100,7 +121,9 @@ class CaptureButtonTest { ) } composeTestRule.onNodeWithTag("CaptureButtonLocked").assertExists() - composeTestRule.onNodeWithTag("CaptureButtonLocked").assertContentDescriptionEquals("Stop Video Recording") + composeTestRule.onNodeWithTag( + "CaptureButtonLocked" + ).assertContentDescriptionEquals("Stop Video Recording") composeTestRule.onNodeWithTag("CaptureButtonLocked", useUnmergedTree = true) .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 7bbc0d263..b6e46d445 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -27,8 +27,6 @@ import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -51,7 +49,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.animation.animateColorAsState import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -69,7 +66,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput @@ -77,10 +73,10 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -401,7 +397,8 @@ private fun CaptureButton( if (currentUiState.value.let { it is CaptureButtonUiState.Enabled.Idle && it.captureMode == CaptureMode.STANDARD - }) { + } + ) { LocalContentColor.current.copy(alpha = 0.2f) } else { Color.Transparent @@ -565,7 +562,9 @@ enum class ShutterBackgroundStyle { BLACK_60 } -val LocalShutterBackgroundStyle = androidx.compose.runtime.compositionLocalOf { ShutterBackgroundStyle.WHITE_20 } +val LocalShutterBackgroundStyle = androidx.compose.runtime.compositionLocalOf { + ShutterBackgroundStyle.WHITE_20 +} @Composable internal fun CaptureButtonRing( @@ -788,7 +787,8 @@ internal fun CaptureButtonNucleus( val sizeFinal = (captureButtonSize * 0.51f).dp val sizeInter = (captureButtonSize * 0.58f).dp - val cornerRadius = if (currentUiState.value is CaptureButtonUiState.Enabled.Recording.LockedRecording) { + val isLocked = currentUiState.value is CaptureButtonUiState.Enabled.Recording.LockedRecording + val cornerRadius = if (isLocked) { if (centerShapeSize <= sizeInter) { val fraction = (centerShapeSize - sizeFinal) / (sizeInter - sizeFinal) val coercedFraction = fraction.coerceIn(0f, 1f) @@ -964,8 +964,6 @@ private fun IdleVideoOnlyCaptureButtonDisabledPreview() { ) } - - @Preview @Composable private fun PressedImageCaptureButtonPreview() { diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt index 3084eec0c..7c62dae4f 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt @@ -34,10 +34,16 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -79,6 +85,20 @@ fun PreviewLayout( snackBar: @Composable (Modifier, snackbarHostState: SnackbarHostState) -> Unit ) { val snackbarHostState = remember { SnackbarHostState() } + + var viewfinderBottom by remember { mutableFloatStateOf(0f) } + var buttonTop by remember { mutableFloatStateOf(0f) } + var buttonBottom by remember { mutableFloatStateOf(0f) } + + val buttonHeight = buttonBottom - buttonTop + val isOverlapping = (viewfinderBottom - buttonTop) >= buttonHeight / 2 && + buttonHeight > 0 && + viewfinderBottom > 0 + android.util.Log.d( + "PreviewLayout", + "buttonTop: $buttonTop, buttonBottom: $buttonBottom, viewfinderBottom: $viewfinderBottom, isOverlapping: $isOverlapping" + ) + Scaffold( modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackbarHostState) } @@ -86,7 +106,11 @@ fun PreviewLayout( Box(modifier = modifier.background(Color.Black)) { Column { indicatorRow(Modifier.statusBarsPadding()) - viewfinder(Modifier) + viewfinder( + Modifier.onGloballyPositioned { coordinates -> + viewfinderBottom = coordinates.boundsInWindow().bottom + } + ) } Box( @@ -96,17 +120,29 @@ fun PreviewLayout( .safeDrawingPadding() ) { - debugVisibilityWrapper { - VerticalMaterialControls( - captureButton = captureButton, - imageWell = imageWell, - flipCameraButton = flipCameraButton, - quickSettingsToggleButton = quickSettingsButton, - captureModeToggleSwitch = captureModeToggle, - bottomSheetQuickSettings = quickSettingsOverlay, - zoomControls = zoomLevelDisplay, - elapsedTimeDisplay = elapsedTimeDisplay - ) + val backgroundStyle = if (isOverlapping) { + ShutterBackgroundStyle.BLACK_60 + } else { + ShutterBackgroundStyle.WHITE_20 + } + + CompositionLocalProvider(LocalShutterBackgroundStyle provides backgroundStyle) { + debugVisibilityWrapper { + VerticalMaterialControls( + buttonModifier = Modifier.onGloballyPositioned { coordinates -> + buttonTop = coordinates.boundsInWindow().top + buttonBottom = coordinates.boundsInWindow().bottom + }, + captureButton = captureButton, + imageWell = imageWell, + flipCameraButton = flipCameraButton, + quickSettingsToggleButton = quickSettingsButton, + captureModeToggleSwitch = captureModeToggle, + bottomSheetQuickSettings = quickSettingsOverlay, + zoomControls = zoomLevelDisplay, + elapsedTimeDisplay = elapsedTimeDisplay + ) + } } // controls overlay snackBar(Modifier, snackbarHostState) @@ -120,6 +156,7 @@ fun PreviewLayout( @Composable private fun VerticalMaterialControls( modifier: Modifier = Modifier, + buttonModifier: Modifier = Modifier, captureButton: @Composable (Modifier) -> Unit, zoomControls: @Composable (Modifier) -> Unit, imageWell: @Composable (Modifier) -> Unit, @@ -156,7 +193,7 @@ private fun VerticalMaterialControls( imageWell(Modifier) } } - captureButton(Modifier) + captureButton(buttonModifier) // right capturebutton item Box( diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index 59578e45a..b046089eb 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -478,7 +478,7 @@ fun PreviewDisplay( surfaceRequest?.let { BoxWithConstraints( - modifier + Modifier .testTag(PREVIEW_DISPLAY) .fillMaxSize() .background(Color.Black), @@ -513,7 +513,7 @@ fun PreviewDisplay( } Box( - modifier = Modifier + modifier = modifier .width(width) .height(height) .transformable(state = transformableState) From afe1a4e6232e6b1b416fc5883642674fa7f24172 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 06:13:40 +0000 Subject: [PATCH 27/40] Add screenshot tests and previews for all permutations --- .../capture/CaptureButtonScreenshotTest.kt | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt diff --git a/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt new file mode 100644 index 000000000..ff96e221e --- /dev/null +++ b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt @@ -0,0 +1,338 @@ +/* + * 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.ui.components.capture + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.tooling.preview.Preview +import com.android.tools.screenshot.PreviewTest +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState + +// --- Standard Mode --- + +@PreviewTest +@Preview +@Composable +fun IdleStandardCaptureButtonScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) + ) +} + +@PreviewTest +@Preview +@Composable +fun IdleStandardCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) + ) + } +} + +@PreviewTest +@Preview +@Composable +fun DisabledStandardCaptureButtonScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.STANDARD, + isEnabled = false + ) + ) +} + +@PreviewTest +@Preview +@Composable +fun DisabledStandardCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.STANDARD, + isEnabled = false + ) + ) + } +} + +@PreviewTest +@Preview +@Composable +fun PressedStandardCaptureButtonScreenshotPreview() { + androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { + androidx.compose.foundation.layout.Box( + modifier = androidx.compose.ui.Modifier + .background( + androidx.compose.ui.graphics.Brush.verticalGradient( + colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + ) + ), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CaptureButtonRing( + captureButtonSize = 76f, + color = androidx.compose.ui.graphics.Color.White + ) { + androidx.compose.foundation.layout.Box( + contentAlignment = androidx.compose.ui.Alignment.Center, + modifier = androidx.compose.ui.Modifier + .size((76f * 0.93f).dp) + .clip(CircleShape) + .background(androidx.compose.ui.graphics.Color.White) + ) {} + } + } + } +} + +@PreviewTest +@Preview +@Composable +fun PressedStandardCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { + androidx.compose.foundation.layout.Box( + modifier = androidx.compose.ui.Modifier + .background( + androidx.compose.ui.graphics.Brush.verticalGradient( + colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + ) + ), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CaptureButtonRing( + captureButtonSize = 76f, + color = androidx.compose.ui.graphics.Color.White + ) { + androidx.compose.foundation.layout.Box( + contentAlignment = androidx.compose.ui.Alignment.Center, + modifier = androidx.compose.ui.Modifier + .size((76f * 0.93f).dp) + .clip(CircleShape) + .background(androidx.compose.ui.graphics.Color.White) + ) {} + } + } + } + } +} + +// --- Image Only Mode --- + +@PreviewTest +@Preview +@Composable +fun IdleImageCaptureButtonScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + ) +} + +@PreviewTest +@Preview +@Composable +fun IdleImageCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + ) + } +} + +@PreviewTest +@Preview +@Composable +fun DisabledImageCaptureButtonScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.IMAGE_ONLY, + isEnabled = false + ) + ) +} + +@PreviewTest +@Preview +@Composable +fun DisabledImageCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.IMAGE_ONLY, + isEnabled = false + ) + ) + } +} + +@PreviewTest +@Preview +@Composable +fun PressedImageCaptureButtonScreenshotPreview() { + androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { + androidx.compose.foundation.layout.Box( + modifier = androidx.compose.ui.Modifier + .background( + androidx.compose.ui.graphics.Brush.verticalGradient( + colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + ) + ), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CaptureButtonRing( + captureButtonSize = 76f, + color = androidx.compose.ui.graphics.Color.Transparent + ) { + androidx.compose.foundation.layout.Box( + contentAlignment = androidx.compose.ui.Alignment.Center, + modifier = androidx.compose.ui.Modifier + .size((76f * 0.93f).dp) + .clip(CircleShape) + .background(androidx.compose.ui.graphics.Color.White) + ) {} + } + } + } +} + +@PreviewTest +@Preview +@Composable +fun PressedImageCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { + androidx.compose.foundation.layout.Box( + modifier = androidx.compose.ui.Modifier + .background( + androidx.compose.ui.graphics.Brush.verticalGradient( + colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + ) + ), + contentAlignment = androidx.compose.ui.Alignment.Center + ) { + CaptureButtonRing( + captureButtonSize = 76f, + color = androidx.compose.ui.graphics.Color.Transparent + ) { + androidx.compose.foundation.layout.Box( + contentAlignment = androidx.compose.ui.Alignment.Center, + modifier = androidx.compose.ui.Modifier + .size((76f * 0.93f).dp) + .clip(CircleShape) + .background(androidx.compose.ui.graphics.Color.White) + ) {} + } + } + } + } +} + +// --- Video Only Mode --- + +@PreviewTest +@Preview +@Composable +fun IdleVideoOnlyCaptureButtonScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) + ) +} + +@PreviewTest +@Preview +@Composable +fun IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) + ) + } +} + +@PreviewTest +@Preview +@Composable +fun DisabledVideoOnlyCaptureButtonScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.VIDEO_ONLY, + isEnabled = false + ) + ) +} + +@PreviewTest +@Preview +@Composable +fun DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.VIDEO_ONLY, + isEnabled = false + ) + ) + } +} + +// --- Recording States --- + +@PreviewTest +@Preview +@Composable +fun PressedRecordingScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording + ) +} + +@PreviewTest +@Preview +@Composable +fun LockedRecordingScreenshotPreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording + ) +} + +@PreviewTest +@Preview +@Composable +fun PressedRecordingBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording + ) + } +} + +@PreviewTest +@Preview +@Composable +fun LockedRecordingBlack60ScreenshotPreview() { + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording + ) + } +} From e3a84ad5584cd4878e6f59770d68f6c7cc2398c1 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 06:13:50 +0000 Subject: [PATCH 28/40] Add generated reference screenshots for all permutations --- ...geCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 7341 bytes ...bledImageCaptureButtonScreenshotPreview_0.png | Bin 0 -> 7465 bytes ...rdCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 7070 bytes ...dStandardCaptureButtonScreenshotPreview_0.png | Bin 0 -> 7347 bytes ...lyCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 6707 bytes ...VideoOnlyCaptureButtonScreenshotPreview_0.png | Bin 0 -> 6778 bytes ...geCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 7871 bytes ...IdleImageCaptureButtonScreenshotPreview_0.png | Bin 0 -> 7668 bytes ...rdCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 8118 bytes ...eStandardCaptureButtonScreenshotPreview_0.png | Bin 0 -> 7894 bytes ...lyCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 7108 bytes ...VideoOnlyCaptureButtonScreenshotPreview_0.png | Bin 0 -> 6896 bytes ...LockedRecordingBlack60ScreenshotPreview_0.png | Bin 0 -> 6221 bytes .../LockedRecordingScreenshotPreview_0.png | Bin 0 -> 6102 bytes ...geCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 7124 bytes ...ssedImageCaptureButtonScreenshotPreview_0.png | Bin 0 -> 6926 bytes ...ressedRecordingBlack60ScreenshotPreview_0.png | Bin 0 -> 7480 bytes .../PressedRecordingScreenshotPreview_0.png | Bin 0 -> 7649 bytes ...rdCaptureButtonBlack60ScreenshotPreview_0.png | Bin 0 -> 4626 bytes ...dStandardCaptureButtonScreenshotPreview_0.png | Bin 0 -> 4613 bytes 20 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledStandardCaptureButtonScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleStandardCaptureButtonScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleVideoOnlyCaptureButtonScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/LockedRecordingScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonBlack60ScreenshotPreview_0.png create mode 100644 ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonBlack60ScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..7748076b73abfa175f35744c5905d78eb47c6c14 GIT binary patch literal 7341 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 literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledImageCaptureButtonScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b4e34ecc88b6e152228b0becb2a097486b6fe493 GIT binary patch literal 7465 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 xWhpF-_$cI54 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 literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..bf4741c3b6bfaf147cea5ef4d0bafe489fd54c49 GIT binary patch literal 6707 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 literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/DisabledVideoOnlyCaptureButtonScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..716fe005e813f9ef133737c370568982f71b7bc8 GIT binary patch literal 6778 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 Wb2D~=oq>N!M2{Zms8^_22K^6eZ!L@f literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonBlack60ScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..2cf1bf5a3cad04ff308564fee30eb918fd0bdeba GIT binary patch literal 7871 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 L>v5UltKk0!!#3Ew literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/IdleImageCaptureButtonScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..352406ae733877346de9a73917d5cda87e1823d3 GIT binary patch literal 7668 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 w!4e{e<6vmnxzXZ6eQJBkd3catnlvLwb5VHnA- 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 kU6lOo=C1b#_!WL>_o=ieX45n#K+I0=6(Wwqs#o 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 literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..06fb9c9936836272e95170d6f5d72c0a3b906ec2 GIT binary patch literal 7124 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
|+&P3wQJ`rpl12*2OaIzgg#+ZYbT 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 literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..8c668ca5ef11f83484e46a1fb797d52cf7bfa8fd GIT binary patch literal 6926 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 literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..db1237df103b526b85da90a9c7ee72fec3dc4877 GIT binary patch literal 7649 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 b>#m~3uXRo=;fodUt%nM$qM=-Xe)jHv#BCfL literal 0 HcmV?d00001 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png new file mode 100644 index 0000000000000000000000000000000000000000..f39338c7a50cfed993741c33589388ea2281dfc0 GIT binary patch literal 4613 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 literal 0 HcmV?d00001 From 8b4eee54d4f1dc47c1aa1a1a3cf148d09ce22426 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 18:38:08 +0000 Subject: [PATCH 29/40] Extract magic numbers to constants and update styleguide Extracted hardcoded scale factors and animation durations to private constants at the top of the file in CaptureButtonComponents.kt. Updated styleguide.md to include best practices for handling magic numbers in Compose based on Duckie's advice. TAG=agy CONV=2a1a6024-c966-475a-a087-7c94cda18542 --- .gemini/styleguide.md | 1 + .../capture/CaptureButtonComponents.kt | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index 3072470e0..3dcc8d48a 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -25,6 +25,7 @@ When reviewing a pull request, focus on the following key areas: * **Remove Unused Imports:** Check for and remove any unused import statements to maintain code cleanliness. * Look for potential null-safety issues, improper error handling, or resource leaks. * **Promote Reusability (DRY Principle):** Identify duplicated or highly similar blocks of code. If a pattern of logic is repeated—even with minor variations—suggest extracting it into a reusable function, composable, or helper class. + * **Avoid Magic Numbers:** Avoid scattering literal dimension values or scales directly in the layout code. Instead, group them into a `private object Tokens` at the top of the file if file-scoped, or in a separate `Dimensions.kt` or `Tokens.kt` file if shared across features. Use semantic naming (e.g., `SmallPadding`) rather than value-based naming (e.g., `Dp16`). 3. **Performance and Efficiency** * Scan for inefficient operations, especially within Composable functions (e.g., expensive calculations, improper state management leading to excessive recompositions). diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index b6e46d445..26b8c1985 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -91,6 +91,18 @@ import kotlinx.coroutines.launch private const val TAG = "CaptureButton" private const val DEFAULT_CAPTURE_BUTTON_SIZE = 76f +private const val IDLE_IMAGE_CAPTURE_SCALE = 0.86f +private const val IDLE_VIDEO_CAPTURE_SCALE = 0.64f +private const val PRESSED_IMAGE_CAPTURE_SCALE = 0.93f +private const val LOCKED_RECORDING_NUCLEUS_SCALE = 0.51f +private const val MORPH_INTERMEDIATE_SCALE = 0.58f +private const val BORDER_WIDTH = 3f +private const val ANIMATION_DURATION_SIZE = 250 +private const val ANIMATION_DURATION_COLOR = 150 +private const val ANIMATION_DURATION_NUCLEUS_RELEASE = 100 + +private val LOCKED_CORNER_RADIUS = 8.dp + // scales against the size of the capture button private const val LOCK_SWITCH_PRESSED_NUCLEUS_SCALE = .5f @@ -571,7 +583,7 @@ internal fun CaptureButtonRing( modifier: Modifier = Modifier, captureButtonSize: Float, color: Color, - borderWidth: Float = 3f, + borderWidth: Float = BORDER_WIDTH, contents: (@Composable () -> Unit)? = null ) { val backgroundStyle = LocalShutterBackgroundStyle.current @@ -581,7 +593,7 @@ internal fun CaptureButtonRing( } val backgroundColor by animateColorAsState( targetValue = targetBackgroundColor, - animationSpec = androidx.compose.animation.core.tween(durationMillis = 150), + animationSpec = androidx.compose.animation.core.tween(durationMillis = ANIMATION_DURATION_COLOR), label = "backgroundColor" ) Box(modifier = modifier, contentAlignment = Alignment.Center) { @@ -720,9 +732,9 @@ internal fun CaptureButtonNucleus( offsetX: Dp = 0.dp, recordingColor: Color = Color.Red, imageCaptureModeColor: Color = Color.White, - idleImageCaptureScale: Float = 0.86f, - idleVideoCaptureScale: Float = 0.64f, - pressedVideoCaptureScale: Float = 0.86f, + idleImageCaptureScale: Float = IDLE_IMAGE_CAPTURE_SCALE, + idleVideoCaptureScale: Float = IDLE_VIDEO_CAPTURE_SCALE, + pressedVideoCaptureScale: Float = IDLE_IMAGE_CAPTURE_SCALE, isVisuallyDisabled: Boolean = false ) { require(idleImageCaptureScale in 0f..1f) { @@ -741,7 +753,7 @@ internal fun CaptureButtonNucleus( val standardShapeSize by animateDpAsState( targetValue = when (val uiState = currentUiState.value) { // inner circle becomes a square when locked - CaptureButtonUiState.Enabled.Recording.LockedRecording -> (captureButtonSize * 0.51f).dp + CaptureButtonUiState.Enabled.Recording.LockedRecording -> (captureButtonSize * LOCKED_RECORDING_NUCLEUS_SCALE).dp CaptureButtonUiState.Enabled.Recording.PressedRecording -> (captureButtonSize * pressedVideoCaptureScale).dp @@ -756,7 +768,7 @@ internal fun CaptureButtonNucleus( CaptureMode.VIDEO_ONLY -> (captureButtonSize * idleVideoCaptureScale).dp } }, - animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = ANIMATION_DURATION_SIZE, easing = FastOutSlowInEasing) ) val pressTransition = updateTransition( @@ -773,26 +785,26 @@ internal fun CaptureButtonNucleus( if (targetState) { snap() } else { - tween(durationMillis = 100) + tween(durationMillis = ANIMATION_DURATION_NUCLEUS_RELEASE) } }, label = "Nucleus Size" ) { isPressedImage -> if (isPressedImage) { - (captureButtonSize * 0.93f).dp + (captureButtonSize * PRESSED_IMAGE_CAPTURE_SCALE).dp } else { standardShapeSize } } - val sizeFinal = (captureButtonSize * 0.51f).dp - val sizeInter = (captureButtonSize * 0.58f).dp + val sizeFinal = (captureButtonSize * LOCKED_RECORDING_NUCLEUS_SCALE).dp + val sizeInter = (captureButtonSize * MORPH_INTERMEDIATE_SCALE).dp val isLocked = currentUiState.value is CaptureButtonUiState.Enabled.Recording.LockedRecording val cornerRadius = if (isLocked) { if (centerShapeSize <= sizeInter) { val fraction = (centerShapeSize - sizeFinal) / (sizeInter - sizeFinal) val coercedFraction = fraction.coerceIn(0f, 1f) - 8.dp + (centerShapeSize / 2 - 8.dp) * coercedFraction + LOCKED_CORNER_RADIUS + (centerShapeSize / 2 - LOCKED_CORNER_RADIUS) * coercedFraction } else { centerShapeSize / 2 } From cbfcbe9ef0eec269499717bed83bcff133fe3824 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 19:49:25 +0000 Subject: [PATCH 30/40] Add tests for PressedRecording and Disabled states of CaptureButton Added tests to verify that CaptureButton has correct content descriptions and roles in PressedRecording and Disabled states. Also added reference screenshot for new previews. TAG=agy CONV=2a1a6024-c966-475a-a087-7c94cda18542 --- .../components/capture/CaptureButtonTest.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt index 4653655c8..cd340cb48 100644 --- a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertContentDescriptionEquals +import androidx.compose.ui.test.isNotEnabled import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.accessibility.AccessibilityChecks @@ -127,4 +128,42 @@ class CaptureButtonTest { composeTestRule.onNodeWithTag("CaptureButtonLocked", useUnmergedTree = true) .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) } + + @Test + fun captureButton_pressedRecording_exists() { + composeTestRule.setContent { + CaptureButton( + modifier = Modifier.testTag("CaptureButtonPressedRecording"), + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording + ) + } + composeTestRule.onNodeWithTag("CaptureButtonPressedRecording").assertExists() + composeTestRule.onNodeWithTag( + "CaptureButtonPressedRecording" + ).assertContentDescriptionEquals("Recording Video") + composeTestRule.onNodeWithTag("CaptureButtonPressedRecording", useUnmergedTree = true) + .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) + } + + @Test + fun captureButton_disabled_exists() { + composeTestRule.setContent { + CaptureButton( + modifier = Modifier.testTag("CaptureButtonDisabled"), + onImageCapture = {}, + onStartRecording = {}, + onStopRecording = {}, + onLockVideoRecording = {}, + onIncrementZoom = {}, + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD, isEnabled = false) + ) + } + composeTestRule.onNodeWithTag("CaptureButtonDisabled").assertExists() + composeTestRule.onNodeWithTag("CaptureButtonDisabled").assert(androidx.compose.ui.test.isNotEnabled()) + } } From 08496f46b6f4eb2f459a20ae6985c33ee89fafd5 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 23:23:04 +0000 Subject: [PATCH 31/40] Enable automated accessibility checks and make CaptureButton focusable Added ui-test-junit4-accessibility and accessibility-test-framework dependencies. Configured CaptureButtonTest to use enableAccessibilityChecks() and tryPerformAccessibilityChecks(). Added .focusable() to CaptureButton to enable accessibility checks on it. TAG=agy CONV=2a1a6024-c966-475a-a087-7c94cda18542 --- gradle/libs.versions.toml | 2 ++ ui/components/capture/build.gradle.kts | 2 ++ .../ui/components/capture/CaptureButtonTest.kt | 18 +++++++++++++++--- .../capture/CaptureButtonComponents.kt | 2 ++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c6cdc2b97..e86fc2fd3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -88,6 +88,8 @@ camera-video = { module = "androidx.camera:camera-video", version.ref = "android camera-compose = { module = "androidx.camera:camera-compose", version.ref = "androidxCamera" } compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } compose-junit = { module = "androidx.compose.ui:ui-test-junit4" } +compose-accessibility = { module = "androidx.compose.ui:ui-test-junit4-accessibility", version = "1.8.0-beta03" } +accessibility-test-framework = { module = "com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework", version = "4.1.1" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial" } compose-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } screenshot-validation-api = { module = "com.android.tools.screenshot:screenshot-validation-api", version.ref = "composeScreenshot" } diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 82fe6a4a3..44ff4fce4 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -104,6 +104,8 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.accessibility) + androidTestImplementation(libs.compose.accessibility) + androidTestImplementation(libs.accessibility.test.framework) implementation(project(":ui:uistate")) implementation(project(":ui:uistate:capture")) diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt index cd340cb48..fc3934b62 100644 --- a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -23,10 +23,17 @@ import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.isNotEnabled -import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.tryPerformAccessibilityChecks +import androidx.activity.ComponentActivity import androidx.test.espresso.accessibility.AccessibilityChecks import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType +import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState import org.junit.Before @@ -37,11 +44,15 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class CaptureButtonTest { @get:Rule - val composeTestRule = createComposeRule() + val composeTestRule = createAndroidComposeRule() @Before fun setUp() { - AccessibilityChecks.enable() + composeTestRule.enableAccessibilityChecks( + AccessibilityValidator().setRunChecksFromRootView(true).also { + it.setThrowExceptionFor(AccessibilityCheckResultType.ERROR) + } + ) } @Test @@ -64,6 +75,7 @@ class CaptureButtonTest { ).assertContentDescriptionEquals("Capture Photo") composeTestRule.onNodeWithTag("CaptureButtonTestTag", useUnmergedTree = true) .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) + composeTestRule.onRoot().tryPerformAccessibilityChecks() } @Test diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 26b8c1985..c494a86d1 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -28,6 +28,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.foundation.Canvas +import androidx.compose.foundation.focusable import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -543,6 +544,7 @@ private fun CaptureButton( CaptureButtonUiState.Unavailable -> "Capture Button Unavailable" } } + .focusable() .then(gestureModifier), captureButtonSize = captureButtonSize, color = animatedColor From 74fc3fc60d7feb94902e35301deeaeaf58b3003c Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Tue, 19 May 2026 23:26:25 +0000 Subject: [PATCH 32/40] Update styleguide with explicit focusability rule for custom components Added a rule to section 10 advising to use Modifier.focusable() when building custom components that handle input manually via low-level gestures, to ensure they are accessible and analyzed by automated checks. TAG=agy CONV=2a1a6024-c966-475a-a087-7c94cda18542 --- .gemini/styleguide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index 3dcc8d48a..64435ffb2 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -88,6 +88,7 @@ When reviewing a pull request, focus on the following key areas: * **Apply Proper Semantics:** When building custom UI components from the ground up (e.g., a custom button made of an `Icon` and a `Text`), apply the correct semantics to ensure they are accessible. * Use `semantics { role = Role.Button }` (or `Role.Checkbox`, etc.) to define the component's logical purpose for screen readers. * For components made of multiple parts that should be read as a single, coherent unit, use `semantics { mergeDescendants = true }`. This prevents screen readers from announcing inner elements (like an icon and its text label) as separate, unrelated items. + * **Explicit Focusability:** When building custom components that handle input manually via low-level gestures (e.g., using `pointerInput` or `detectTapGestures`) rather than `Modifier.clickable()`, they may not automatically become focusable. In such cases, explicitly add `Modifier.focusable()` to ensure they are reachable via keyboard navigation and analyzed by automated accessibility checks. * **Content vs State Descriptions:** * Use `contentDescription` to describe the **identity** or **action** of the component (e.g., "Capture Photo", "Start Video Recording"). * Use `stateDescription` to describe the **current state** of the component (e.g., "Locked", "Selected"). From 0d89719eea1a35abb5ca49f8d045e261c6ed8224 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 20 May 2026 01:30:49 +0000 Subject: [PATCH 33/40] Fix accessibility issues in CaptureButton: - Add focusability to custom button ring. - Add content description to lock icon. - Fix touch target size issue by expanding container width dynamically. - Add missing tryPerformAccessibilityChecks to tests. - Add screenshot tests for pressed recording state. --- .../components/capture/CaptureButtonTest.kt | 5 +++ .../capture/CaptureButtonComponents.kt | 35 +++++++++++------- ...sedRecordingBlack60ScreenshotPreview_0.png | Bin 7480 -> 7481 bytes .../PressedRecordingScreenshotPreview_0.png | Bin 7649 -> 7647 bytes 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt index fc3934b62..2d588c48f 100644 --- a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -91,6 +91,7 @@ class CaptureButtonTest { captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) ) } + composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag("CaptureButtonImageOnly").assertExists() composeTestRule.onNodeWithTag( "CaptureButtonImageOnly" @@ -112,6 +113,7 @@ class CaptureButtonTest { captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) ) } + composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag("CaptureButtonVideoOnly").assertExists() composeTestRule.onNodeWithTag( "CaptureButtonVideoOnly" @@ -133,6 +135,7 @@ class CaptureButtonTest { captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording ) } + composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag("CaptureButtonLocked").assertExists() composeTestRule.onNodeWithTag( "CaptureButtonLocked" @@ -154,6 +157,7 @@ class CaptureButtonTest { captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording ) } + composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag("CaptureButtonPressedRecording").assertExists() composeTestRule.onNodeWithTag( "CaptureButtonPressedRecording" @@ -175,6 +179,7 @@ class CaptureButtonTest { captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD, isEnabled = false) ) } + composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag("CaptureButtonDisabled").assertExists() composeTestRule.onNodeWithTag("CaptureButtonDisabled").assert(androidx.compose.ui.test.isNotEnabled()) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index c494a86d1..1babfe35f 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -29,6 +29,8 @@ import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.foundation.Canvas import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.material3.IconButton import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -644,7 +646,7 @@ private fun LockSwitchCaptureButtonNucleus( Box( contentAlignment = Alignment.CenterStart, modifier = Modifier - .width(switchWidth) + .width(switchWidth + 4.dp) .height(switchHeight) .offset(x = -(switchWidth - pressedNucleusSize) / 2) ) { @@ -688,23 +690,30 @@ private fun LockSwitchCaptureButtonNucleus( enter = fadeIn(), exit = ExitTransition.None ) { - Icon( + Box( modifier = Modifier - .size(switchHeight * .75f) .align(Alignment.CenterStart) .padding(start = 8.dp) .offset(x = -(switchWidth - pressedNucleusSize)) - .clickable(indication = null, interactionSource = null) { - onToggleSwitchPosition() + .size(32.dp) + .semantics { contentDescription = "Lock Video Recording" } + .pointerInput(Unit) { + detectTapGestures { + onToggleSwitchPosition() + } + } + ) { + Icon( + modifier = Modifier.size(switchHeight * .75f).align(Alignment.Center), + tint = Color.White, + painter = if (shouldBeLocked()) { + painterResource(R.drawable.ic_lock) + } else { + painterResource(R.drawable.ic_lock_open) }, - tint = Color.White, - painter = if (shouldBeLocked()) { - painterResource(R.drawable.ic_lock) - } else { - painterResource(R.drawable.ic_lock_open) - }, - contentDescription = null - ) + contentDescription = null + ) + } } } } diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedRecordingBlack60ScreenshotPreview_0.png index 1a51bd7189ec4d3b6a61f4c82395b72c3b0002a2..87cb2bbe4533fadefbb2896952c9836bcde97e15 100644 GIT binary patch delta 7074 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

4HDALHFI85Y**RK_fEoN(oiiNX%-xC5 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 Date: Wed, 20 May 2026 01:34:05 +0000 Subject: [PATCH 34/40] Fix ring visibility in previews in CaptureButtonComponents.kt --- .../ui/components/capture/CaptureButtonComponents.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 1babfe35f..1c3560953 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -1003,7 +1003,7 @@ private fun PressedImageCaptureButtonPreview() { ) { CaptureButtonRing( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - color = Color.White + color = Color.Transparent ) { CaptureButtonNucleus( captureButtonUiState = CaptureButtonUiState.Enabled.Idle( @@ -1050,7 +1050,7 @@ private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { ), contentAlignment = Alignment.CenterEnd ) { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Transparent) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, @@ -1078,7 +1078,7 @@ private fun LockSwitchLockedPressedRecordingPreview() { ), contentAlignment = Alignment.CenterEnd ) { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.White) { + CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Transparent) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, From 27b68202d3160d0a972c98bc88871234dfd6d556 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 20 May 2026 04:00:26 +0000 Subject: [PATCH 35/40] Consolidate previews and apply style guide fixes in CaptureButtonScreenshotTest.kt --- .../components/capture/CaptureButtonTest.kt | 15 +- .../capture/CaptureButtonComponents.kt | 56 +++--- .../capture/CaptureButtonScreenshotTest.kt | 170 +++++++----------- 3 files changed, 109 insertions(+), 132 deletions(-) diff --git a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt index 2d588c48f..866547906 100644 --- a/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt +++ b/ui/components/capture/src/androidTest/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonTest.kt @@ -15,6 +15,7 @@ */ package com.google.jetpackcamera.ui.components.capture +import androidx.activity.ComponentActivity import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -23,15 +24,12 @@ import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertContentDescriptionEquals import androidx.compose.ui.test.isNotEnabled -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.accessibility.enableAccessibilityChecks +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.tryPerformAccessibilityChecks -import androidx.activity.ComponentActivity -import androidx.test.espresso.accessibility.AccessibilityChecks import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType import com.google.android.apps.common.testing.accessibility.framework.integrations.espresso.AccessibilityValidator import com.google.jetpackcamera.model.CaptureMode @@ -176,11 +174,16 @@ class CaptureButtonTest { onStopRecording = {}, onLockVideoRecording = {}, onIncrementZoom = {}, - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD, isEnabled = false) + captureButtonUiState = CaptureButtonUiState.Enabled.Idle( + CaptureMode.STANDARD, + isEnabled = false + ) ) } composeTestRule.onRoot().tryPerformAccessibilityChecks() composeTestRule.onNodeWithTag("CaptureButtonDisabled").assertExists() - composeTestRule.onNodeWithTag("CaptureButtonDisabled").assert(androidx.compose.ui.test.isNotEnabled()) + composeTestRule.onNodeWithTag( + "CaptureButtonDisabled" + ).assert(androidx.compose.ui.test.isNotEnabled()) } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 1c3560953..61c74d124 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -28,12 +28,9 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.foundation.Canvas -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.requiredSizeIn -import androidx.compose.material3.IconButton import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -597,7 +594,9 @@ internal fun CaptureButtonRing( } val backgroundColor by animateColorAsState( targetValue = targetBackgroundColor, - animationSpec = androidx.compose.animation.core.tween(durationMillis = ANIMATION_DURATION_COLOR), + animationSpec = androidx.compose.animation.core.tween( + durationMillis = ANIMATION_DURATION_COLOR + ), label = "backgroundColor" ) Box(modifier = modifier, contentAlignment = Alignment.Center) { @@ -764,7 +763,8 @@ internal fun CaptureButtonNucleus( val standardShapeSize by animateDpAsState( targetValue = when (val uiState = currentUiState.value) { // inner circle becomes a square when locked - CaptureButtonUiState.Enabled.Recording.LockedRecording -> (captureButtonSize * LOCKED_RECORDING_NUCLEUS_SCALE).dp + CaptureButtonUiState.Enabled.Recording.LockedRecording -> + (captureButtonSize * LOCKED_RECORDING_NUCLEUS_SCALE).dp CaptureButtonUiState.Enabled.Recording.PressedRecording -> (captureButtonSize * pressedVideoCaptureScale).dp @@ -779,14 +779,20 @@ internal fun CaptureButtonNucleus( CaptureMode.VIDEO_ONLY -> (captureButtonSize * idleVideoCaptureScale).dp } }, - animationSpec = tween(durationMillis = ANIMATION_DURATION_SIZE, easing = FastOutSlowInEasing) + animationSpec = tween( + durationMillis = ANIMATION_DURATION_SIZE, + easing = FastOutSlowInEasing + ) ) val pressTransition = updateTransition( targetState = isPressed && currentUiState.value.let { it is CaptureButtonUiState.Enabled.Idle && - (it.captureMode == CaptureMode.IMAGE_ONLY || it.captureMode == CaptureMode.STANDARD) + ( + it.captureMode == CaptureMode.IMAGE_ONLY || + it.captureMode == CaptureMode.STANDARD + ) }, label = "Press Size Transition" ) @@ -889,7 +895,7 @@ internal fun CaptureButtonNucleus( @Preview @Composable -private fun CaptureButtonUnavailablePreview() { +internal fun CaptureButtonUnavailablePreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Unavailable ) @@ -932,7 +938,7 @@ internal fun PreviewCaptureButton( @Preview @Composable -private fun IdleStandardCaptureButtonPreview() { +internal fun IdleStandardCaptureButtonPreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) ) @@ -940,7 +946,7 @@ private fun IdleStandardCaptureButtonPreview() { @Preview @Composable -private fun IdleImageCaptureButtonPreview() { +internal fun IdleImageCaptureButtonPreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) ) @@ -948,7 +954,7 @@ private fun IdleImageCaptureButtonPreview() { @Preview @Composable -private fun IdleVideoOnlyCaptureButtonPreview() { +internal fun IdleVideoOnlyCaptureButtonPreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) ) @@ -956,7 +962,7 @@ private fun IdleVideoOnlyCaptureButtonPreview() { @Preview @Composable -private fun IdleStandardCaptureButtonDisabledPreview() { +internal fun IdleStandardCaptureButtonDisabledPreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Idle( CaptureMode.STANDARD, @@ -967,7 +973,7 @@ private fun IdleStandardCaptureButtonDisabledPreview() { @Preview @Composable -private fun IdleImageCaptureButtonDisabledPreview() { +internal fun IdleImageCaptureButtonDisabledPreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Idle( CaptureMode.IMAGE_ONLY, @@ -978,7 +984,7 @@ private fun IdleImageCaptureButtonDisabledPreview() { @Preview @Composable -private fun IdleVideoOnlyCaptureButtonDisabledPreview() { +internal fun IdleVideoOnlyCaptureButtonDisabledPreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Idle( CaptureMode.VIDEO_ONLY, @@ -989,7 +995,7 @@ private fun IdleVideoOnlyCaptureButtonDisabledPreview() { @Preview @Composable -private fun PressedImageCaptureButtonPreview() { +internal fun PressedImageCaptureButtonPreview() { // Manually constructed preview to verify visual state without relying on interaction source MaterialTheme(colorScheme = darkColorScheme()) { Box( @@ -1019,7 +1025,7 @@ private fun PressedImageCaptureButtonPreview() { @Preview @Composable -private fun LockSwitchUnlockedPressedRecordingPreview() { +internal fun LockSwitchUnlockedPressedRecordingPreview() { // box is here to account for the offset lock switch PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, @@ -1030,7 +1036,7 @@ private fun LockSwitchUnlockedPressedRecordingPreview() { @Preview @Composable -private fun LockedRecordingPreview() { +internal fun LockedRecordingPreview() { PreviewCaptureButton( captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording ) @@ -1038,7 +1044,7 @@ private fun LockedRecordingPreview() { @Preview @Composable -private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { +internal fun LockSwitchLockedAtThresholdPressedRecordingPreview() { // box is here to account for the offset lock switch Box( modifier = Modifier @@ -1050,7 +1056,10 @@ private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { ), contentAlignment = Alignment.CenterEnd ) { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Transparent) { + CaptureButtonRing( + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, + color = Color.Transparent + ) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, @@ -1066,7 +1075,7 @@ private fun LockSwitchLockedAtThresholdPressedRecordingPreview() { @Preview @Composable -private fun LockSwitchLockedPressedRecordingPreview() { +internal fun LockSwitchLockedPressedRecordingPreview() { // box is here to account for the offset lock switch Box( modifier = Modifier @@ -1078,7 +1087,10 @@ private fun LockSwitchLockedPressedRecordingPreview() { ), contentAlignment = Alignment.CenterEnd ) { - CaptureButtonRing(captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, color = Color.Transparent) { + CaptureButtonRing( + captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, + color = Color.Transparent + ) { LockSwitchCaptureButtonNucleus( captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording, diff --git a/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt index ff96e221e..745b620ea 100644 --- a/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt +++ b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt @@ -16,15 +16,21 @@ package com.google.jetpackcamera.ui.components.capture import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.android.tools.screenshot.PreviewTest -import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState // --- Standard Mode --- @@ -33,9 +39,7 @@ import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState @Preview @Composable fun IdleStandardCaptureButtonScreenshotPreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) - ) + IdleStandardCaptureButtonPreview() } @PreviewTest @@ -43,9 +47,7 @@ fun IdleStandardCaptureButtonScreenshotPreview() { @Composable fun IdleStandardCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) - ) + IdleStandardCaptureButtonPreview() } } @@ -53,12 +55,7 @@ fun IdleStandardCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun DisabledStandardCaptureButtonScreenshotPreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.STANDARD, - isEnabled = false - ) - ) + IdleStandardCaptureButtonDisabledPreview() } @PreviewTest @@ -66,12 +63,7 @@ fun DisabledStandardCaptureButtonScreenshotPreview() { @Composable fun DisabledStandardCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.STANDARD, - isEnabled = false - ) - ) + IdleStandardCaptureButtonDisabledPreview() } } @@ -79,26 +71,26 @@ fun DisabledStandardCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun PressedStandardCaptureButtonScreenshotPreview() { - androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { - androidx.compose.foundation.layout.Box( - modifier = androidx.compose.ui.Modifier + MaterialTheme(colorScheme = darkColorScheme()) { + Box( + modifier = Modifier .background( - androidx.compose.ui.graphics.Brush.verticalGradient( - colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) ) ), - contentAlignment = androidx.compose.ui.Alignment.Center + contentAlignment = Alignment.Center ) { CaptureButtonRing( captureButtonSize = 76f, - color = androidx.compose.ui.graphics.Color.White + color = Color.White ) { - androidx.compose.foundation.layout.Box( - contentAlignment = androidx.compose.ui.Alignment.Center, - modifier = androidx.compose.ui.Modifier + Box( + contentAlignment = Alignment.Center, + modifier = Modifier .size((76f * 0.93f).dp) .clip(CircleShape) - .background(androidx.compose.ui.graphics.Color.White) + .background(Color.White) ) {} } } @@ -110,26 +102,26 @@ fun PressedStandardCaptureButtonScreenshotPreview() { @Composable fun PressedStandardCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { - androidx.compose.foundation.layout.Box( - modifier = androidx.compose.ui.Modifier + MaterialTheme(colorScheme = darkColorScheme()) { + Box( + modifier = Modifier .background( - androidx.compose.ui.graphics.Brush.verticalGradient( - colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) ) ), - contentAlignment = androidx.compose.ui.Alignment.Center + contentAlignment = Alignment.Center ) { CaptureButtonRing( captureButtonSize = 76f, - color = androidx.compose.ui.graphics.Color.White + color = Color.White ) { - androidx.compose.foundation.layout.Box( - contentAlignment = androidx.compose.ui.Alignment.Center, - modifier = androidx.compose.ui.Modifier + Box( + contentAlignment = Alignment.Center, + modifier = Modifier .size((76f * 0.93f).dp) .clip(CircleShape) - .background(androidx.compose.ui.graphics.Color.White) + .background(Color.White) ) {} } } @@ -143,9 +135,7 @@ fun PressedStandardCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun IdleImageCaptureButtonScreenshotPreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) - ) + IdleImageCaptureButtonPreview() } @PreviewTest @@ -153,9 +143,7 @@ fun IdleImageCaptureButtonScreenshotPreview() { @Composable fun IdleImageCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) - ) + IdleImageCaptureButtonPreview() } } @@ -163,12 +151,7 @@ fun IdleImageCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun DisabledImageCaptureButtonScreenshotPreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.IMAGE_ONLY, - isEnabled = false - ) - ) + IdleImageCaptureButtonDisabledPreview() } @PreviewTest @@ -176,12 +159,7 @@ fun DisabledImageCaptureButtonScreenshotPreview() { @Composable fun DisabledImageCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.IMAGE_ONLY, - isEnabled = false - ) - ) + IdleImageCaptureButtonDisabledPreview() } } @@ -189,26 +167,26 @@ fun DisabledImageCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun PressedImageCaptureButtonScreenshotPreview() { - androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { - androidx.compose.foundation.layout.Box( - modifier = androidx.compose.ui.Modifier + MaterialTheme(colorScheme = darkColorScheme()) { + Box( + modifier = Modifier .background( - androidx.compose.ui.graphics.Brush.verticalGradient( - colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) ) ), - contentAlignment = androidx.compose.ui.Alignment.Center + contentAlignment = Alignment.Center ) { CaptureButtonRing( captureButtonSize = 76f, - color = androidx.compose.ui.graphics.Color.Transparent + color = Color.Transparent ) { - androidx.compose.foundation.layout.Box( - contentAlignment = androidx.compose.ui.Alignment.Center, - modifier = androidx.compose.ui.Modifier + Box( + contentAlignment = Alignment.Center, + modifier = Modifier .size((76f * 0.93f).dp) .clip(CircleShape) - .background(androidx.compose.ui.graphics.Color.White) + .background(Color.White) ) {} } } @@ -220,26 +198,26 @@ fun PressedImageCaptureButtonScreenshotPreview() { @Composable fun PressedImageCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - androidx.compose.material3.MaterialTheme(colorScheme = androidx.compose.material3.darkColorScheme()) { - androidx.compose.foundation.layout.Box( - modifier = androidx.compose.ui.Modifier + MaterialTheme(colorScheme = darkColorScheme()) { + Box( + modifier = Modifier .background( - androidx.compose.ui.graphics.Brush.verticalGradient( - colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.DarkGray) + Brush.verticalGradient( + colors = listOf(Color.Gray, Color.DarkGray) ) ), - contentAlignment = androidx.compose.ui.Alignment.Center + contentAlignment = Alignment.Center ) { CaptureButtonRing( captureButtonSize = 76f, - color = androidx.compose.ui.graphics.Color.Transparent + color = Color.Transparent ) { - androidx.compose.foundation.layout.Box( - contentAlignment = androidx.compose.ui.Alignment.Center, - modifier = androidx.compose.ui.Modifier + Box( + contentAlignment = Alignment.Center, + modifier = Modifier .size((76f * 0.93f).dp) .clip(CircleShape) - .background(androidx.compose.ui.graphics.Color.White) + .background(Color.White) ) {} } } @@ -253,9 +231,7 @@ fun PressedImageCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun IdleVideoOnlyCaptureButtonScreenshotPreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) - ) + IdleVideoOnlyCaptureButtonPreview() } @PreviewTest @@ -263,9 +239,7 @@ fun IdleVideoOnlyCaptureButtonScreenshotPreview() { @Composable fun IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY) - ) + IdleVideoOnlyCaptureButtonPreview() } } @@ -273,12 +247,7 @@ fun IdleVideoOnlyCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun DisabledVideoOnlyCaptureButtonScreenshotPreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.VIDEO_ONLY, - isEnabled = false - ) - ) + IdleVideoOnlyCaptureButtonDisabledPreview() } @PreviewTest @@ -286,12 +255,7 @@ fun DisabledVideoOnlyCaptureButtonScreenshotPreview() { @Composable fun DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.VIDEO_ONLY, - isEnabled = false - ) - ) + IdleVideoOnlyCaptureButtonDisabledPreview() } } @@ -310,9 +274,7 @@ fun PressedRecordingScreenshotPreview() { @Preview @Composable fun LockedRecordingScreenshotPreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording - ) + LockedRecordingPreview() } @PreviewTest From 409ef19f105f4da3ec55f8bf1fadb1ee4cd23087 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 20 May 2026 06:14:07 +0000 Subject: [PATCH 36/40] Refactor capture button previews to use CompositionLocal for pressed state --- .../capture/CaptureButtonComponents.kt | 50 +++---- .../capture/CaptureButtonScreenshotTest.kt | 130 ++++-------------- ...aptureButtonBlack60ScreenshotPreview_0.png | Bin 7124 -> 7859 bytes ...dImageCaptureButtonScreenshotPreview_0.png | Bin 6926 -> 7654 bytes ...sedRecordingBlack60ScreenshotPreview_0.png | Bin 7481 -> 10410 bytes .../PressedRecordingScreenshotPreview_0.png | Bin 7647 -> 10529 bytes ...aptureButtonBlack60ScreenshotPreview_0.png | Bin 4626 -> 5254 bytes ...andardCaptureButtonScreenshotPreview_0.png | Bin 4613 -> 5195 bytes 8 files changed, 46 insertions(+), 134 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 61c74d124..1a07d87a2 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -52,6 +52,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -385,8 +386,9 @@ private fun CaptureButton( captureButtonSize: Float = DEFAULT_CAPTURE_BUTTON_SIZE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { - var isCaptureButtonPressed by remember { - mutableStateOf(false) + val initialPressed = LocalInitialPressedState.current + var isCaptureButtonPressed by remember(initialPressed) { + mutableStateOf(initialPressed) } var switchPosition by remember { @@ -570,15 +572,27 @@ private fun CaptureButton( } } +/** + * Enum representing the background style of the shutter button. + */ enum class ShutterBackgroundStyle { WHITE_20, BLACK_60 } -val LocalShutterBackgroundStyle = androidx.compose.runtime.compositionLocalOf { +/** + * CompositionLocal to provide the [ShutterBackgroundStyle] to the capture button. + */ +val LocalShutterBackgroundStyle = compositionLocalOf { ShutterBackgroundStyle.WHITE_20 } +/** + * CompositionLocal to provide the initial pressed state of the capture button. + * This is primarily used for previews and screenshot tests to force the pressed state. + */ +internal val LocalInitialPressedState = compositionLocalOf { false } + @Composable internal fun CaptureButtonRing( modifier: Modifier = Modifier, @@ -905,7 +919,7 @@ internal fun CaptureButtonUnavailablePreview() { internal fun PreviewCaptureButton( captureButtonUiState: CaptureButtonUiState, modifier: Modifier = Modifier, - contentAlignment: Alignment = Alignment.Center, + contentAlignment: Alignment = Alignment.CenterEnd, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) { MaterialTheme(colorScheme = darkColorScheme()) { @@ -996,30 +1010,10 @@ internal fun IdleVideoOnlyCaptureButtonDisabledPreview() { @Preview @Composable internal fun PressedImageCaptureButtonPreview() { - // Manually constructed preview to verify visual state without relying on interaction source - MaterialTheme(colorScheme = darkColorScheme()) { - Box( - modifier = Modifier - .background( - Brush.verticalGradient( - colors = listOf(Color.Gray, Color.DarkGray) - ) - ), - contentAlignment = Alignment.Center - ) { - CaptureButtonRing( - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE, - color = Color.Transparent - ) { - CaptureButtonNucleus( - captureButtonUiState = CaptureButtonUiState.Enabled.Idle( - CaptureMode.IMAGE_ONLY - ), - isPressed = true, - captureButtonSize = DEFAULT_CAPTURE_BUTTON_SIZE - ) - } - } + CompositionLocalProvider(LocalInitialPressedState provides true) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + ) } } diff --git a/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt index 745b620ea..9bb9386b6 100644 --- a/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt +++ b/ui/components/capture/src/screenshotTest/kotlin/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTest.kt @@ -15,22 +15,14 @@ */ package com.google.jetpackcamera.ui.components.capture -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.tools.screenshot.PreviewTest +import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState // --- Standard Mode --- @@ -71,29 +63,10 @@ fun DisabledStandardCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun PressedStandardCaptureButtonScreenshotPreview() { - MaterialTheme(colorScheme = darkColorScheme()) { - Box( - modifier = Modifier - .background( - Brush.verticalGradient( - colors = listOf(Color.Gray, Color.DarkGray) - ) - ), - contentAlignment = Alignment.Center - ) { - CaptureButtonRing( - captureButtonSize = 76f, - color = Color.White - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size((76f * 0.93f).dp) - .clip(CircleShape) - .background(Color.White) - ) {} - } - } + CompositionLocalProvider(LocalInitialPressedState provides true) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) + ) } } @@ -101,31 +74,13 @@ fun PressedStandardCaptureButtonScreenshotPreview() { @Preview @Composable fun PressedStandardCaptureButtonBlack60ScreenshotPreview() { - CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - MaterialTheme(colorScheme = darkColorScheme()) { - Box( - modifier = Modifier - .background( - Brush.verticalGradient( - colors = listOf(Color.Gray, Color.DarkGray) - ) - ), - contentAlignment = Alignment.Center - ) { - CaptureButtonRing( - captureButtonSize = 76f, - color = Color.White - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size((76f * 0.93f).dp) - .clip(CircleShape) - .background(Color.White) - ) {} - } - } - } + CompositionLocalProvider( + LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60, + LocalInitialPressedState provides true + ) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.STANDARD) + ) } } @@ -167,29 +122,10 @@ fun DisabledImageCaptureButtonBlack60ScreenshotPreview() { @Preview @Composable fun PressedImageCaptureButtonScreenshotPreview() { - MaterialTheme(colorScheme = darkColorScheme()) { - Box( - modifier = Modifier - .background( - Brush.verticalGradient( - colors = listOf(Color.Gray, Color.DarkGray) - ) - ), - contentAlignment = Alignment.Center - ) { - CaptureButtonRing( - captureButtonSize = 76f, - color = Color.Transparent - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size((76f * 0.93f).dp) - .clip(CircleShape) - .background(Color.White) - ) {} - } - } + CompositionLocalProvider(LocalInitialPressedState provides true) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + ) } } @@ -197,31 +133,11 @@ fun PressedImageCaptureButtonScreenshotPreview() { @Preview @Composable fun PressedImageCaptureButtonBlack60ScreenshotPreview() { - CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { - MaterialTheme(colorScheme = darkColorScheme()) { - Box( - modifier = Modifier - .background( - Brush.verticalGradient( - colors = listOf(Color.Gray, Color.DarkGray) - ) - ), - contentAlignment = Alignment.Center - ) { - CaptureButtonRing( - captureButtonSize = 76f, - color = Color.Transparent - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size((76f * 0.93f).dp) - .clip(CircleShape) - .background(Color.White) - ) {} - } - } - } + CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60, + LocalInitialPressedState provides true) { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY) + ) } } @@ -266,6 +182,7 @@ fun DisabledVideoOnlyCaptureButtonBlack60ScreenshotPreview() { @Composable fun PressedRecordingScreenshotPreview() { PreviewCaptureButton( + modifier = Modifier.width(150.dp), captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording ) } @@ -283,6 +200,7 @@ fun LockedRecordingScreenshotPreview() { fun PressedRecordingBlack60ScreenshotPreview() { CompositionLocalProvider(LocalShutterBackgroundStyle provides ShutterBackgroundStyle.BLACK_60) { PreviewCaptureButton( + modifier = Modifier.width(150.dp), captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording ) } diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonBlack60ScreenshotPreview_0.png index 06fb9c9936836272e95170d6f5d72c0a3b906ec2..704e42c1c200c87abc56e6e87367bd9e79ea0b66 100644 GIT binary patch literal 7859 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 literal 7124 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

|+&P3wQJ`rpl12*2OaIzgg#+ZYbT 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 diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedImageCaptureButtonScreenshotPreview_0.png index 8c668ca5ef11f83484e46a1fb797d52cf7bfa8fd..f8ec851aab9c1b3731a9ba2e4e9b6f98adb69bcd 100644 GIT binary patch literal 7654 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ý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 F{6Ck^C+q+K literal 7481 zcmW+*cRZWl7gp5XYE|vD+QwF!*cG%^t)O;LVpChG)u0-+cd4MZ*rT@Cvuba(H{aUh z_xATsKFRx@+;i@8?>*-^&qZo!sF0I9B*DSKAyEPhpqXEA65D@^Mpf#rj92^!L zRV8^{Z_~YucPPDX&+f7?IowW3(*7WGDg_#@G*2plj-U~;opOFlYV%MzTqX`;7^a%h zXLLoaOZ%}IX8JZY*V6U}NCQ!SJeYu4%{}HovTTH~NUasdr%%DV)tfx_%d#V`hJHFH zTP%#tpZTA=nl4H;ojEO@$d)Dj3WyV1@a|~3SWi>5lh{8lDr>gS^xKy>>7><6j^ox$ z{jTyQRWX7hCE)5bR9nhto#6>c&k zp`d!dGpDQm&r^N)lLOUn2*<;U*}B+hE+?C`b|P*@A|Td78B(?cgoJ~5l+q|#16Vd+ z0qu5p3OCHzKFz@~N{Ll>*6`}MpFd65c4+FI-sFHmp`w;W>uE(dt8iJ-|a{kPV`f;I4bIUB}+J{6Gj`_n^xGN#VIwr%EkQ%nw0 z3%F<{gqTx2692Z!MjqObH}u2LM>c*YPQYN8w7RW|MT(; z^0+e<$35WB(;-HsuT$m-C(@QiU>g%|?tODn-E1G4wr|Fcr}`9wm41-FK*Nd?JuSP~ z>cr-_d=}#?vV(J9qClS4=dNk1>=_0}r2Euok$z7OzPq`D{sn}Z9*V6S4E_2Jg%WJHlN zMXyL7W~<9wvt}yvp7Wq`CKfi!nY}b}{JDRiXJmLt^1EXHa-yn@)Q4OD{XmBcEPR$T zmas_s^yE(%>QB69r@aO&G?Q{wdRFnP7w5s<=j-nS8;QH?0;$SyS3yM9zCg{Zv2;KU)ir__)kGW>)3D z=F6^K+76*Nu0ixl#OC098ERF@U5w9&lv8jwy_HWH`fwP+_99Kpc}U`m)mmRdy_IjX z|AwmR>W|>D4^Dr~1hX{fdNH#MrrvFk*$Ig#qv{UW#1@Jed{a<7LGTrGa(mXYh?pvG zlSJl3Kkz45jJ?%;Nkc$)wmWS_$k1=+qu&>BHg8MMV@O3u%E0#`BO@bGIf_1|alfJ8 z?QGsF&F|pnq=%0`%J9F3igo4+AG*uywhME{P8`;R)#-8_S zvD(MR_hQ1^k;;3UJn~(h!5B^*zu&AfzEuhcc!ek`p7`Yqb(qk5Mq`eGI(g%@=lPmh zHMn3}GznJ?A-B)hk1h=vKy1-Vol`?9&aNMV{WzaMACna+%aE zsEi|v5CQ~e1H!|G*$Kgg!J*eAK0Mw8X1y$PqC;Ha!^HU5dD=sR7LMP|JCXfX{+=4jm%2U=&;bY71S1F ziK9msCmY7yy3BcKD}^rr5+ z4y)a1Et+@XEo*nu8MljV|H*jsU))-?@!m!;u?Dn<^ik(jQuxfBooM5K{kt={@kqNj zWNNsoCRKCZHR8heA&&{FfUaqS`_~yHBk(0Jt~%4l-rhKF_Z+fO0ye10!zK>}cr)yU zbEd?|giWB;w(dzn72VMZ?$IPEWT$`2HjwjX_vT#`+~V@>pEG9S=oIKt7d|p>7;=RO zd&U}* z6LFXzt5!3dW5}rv2IlYd8Ft<>}xB zpzA^|wRbGXb^IF@xdy|H6K9DN9ACOU0teG2)=BRXP!Ug#1eEj(&;9*%3Ce(gkVZ_7#{;BD)k${@OFt6uclAE-De3{T=HVcOGPm89rKD%8kox!{ z^S`9oeED&sj_SX8yu3d1u;kv}NG(p~=SP398$5RqMM{ynE2|gnaw}50bsnWcF7L2p zC*MCAar|C-$e%a)ylioqzzj`g71TuYY>bMC4}}r~^6Xf>WuC*n$w@sCR^es8?UUC+ z>SGQ`fA2XpDUk?zcn2}Jd#paro?5~sp2dgeN2dIJXOPDVE6A2>fXqw4_**F>A6Swd z=0flv(h#>C61{jZ`}=o-dx7Ef!h%zC66`=ne?fJ}T*Zh16UeCE&PJ8xkkM&QNI-xv zCB`N10;>K4Ffe7Y6KT3vcZQO>(T|)RKiliu6l*bXk4eVk1^Y%* zCV|#r0{Cf@QOis7%*d?2SREDffQw%+-NqG@L&m~-wx!f399oH+cIRKXKfkeU};9b7NZ&Tk*Bhc3A(@ z#Imu^Q`9+`18wfP<#J!uI|AWQ^GlRsY;=@5Vei$|>7D7y_?gkk=PEu1w5R&b zSA&%X@oeKdm*tQ39$R1OipwSlXtFF8snfYcO|v9LxC!70t^Dzl+1I@EQ~CI9m(gdz zp`Un^86}H63v2m+SG=ABnU8f)+3mN|xdU8=W+i~Z5%sgqkqd_m^bh6_qOLh5Ry5G_&VDY_C(}-F?vzt;tuFT%C;O>G-3UYVCza zN;_$fc}+$Z~k z3@IsuG7vUL-DzCnfT|weMoYz}Y5GHinItF`QG}Knvh}IHds8Ax+J!Om_sj zLR#1=oX(Q$A4S)1YF>u3XA^VZXL|lY+|_cZA_3bj!~WJZQb-A!Fiet6A4$1p?cyT9 z{l})#|Kd2EJ;ZwG(FqF++dIkVIDw_-%1oR<`%&_I-6k=vcEJ}6*maBpc;3SxCW_PF zfCnw!+S+QwbF}Ae!UV4@<7yWJrT*D0eSHA`^vLdAuSC_@qZss=BH++)B+@&8>tf%l z#i6U83pb!bcPhQgIEYcPhJTadXyH)j;rJ0)OrNF@ZbQ3_kwt2XmcXsLc4(z_a5%z; z4z}&hoX`KnKhuxS?y%JF++jwNg3?4C3#r_N`=te60}gIaYi8XjN?t^?)Qh^)N&?-7 zeqAyC5J~;?TBzKL(r(n=?(P6mNU2#e(0?d9Vf~Pj_Q@Y5CeGZK;?!5xv3NPHZE{ph~venJje>|MiJV!9%DFxZ6VSsxXw z+Fma#Ie_Cx5H*k`E6d$SbrB3{#4;0Y14Fg@GKc!@&q5>23Xc^UARzAd1?ZP0>Xf87qFwg1X}~`x`cdqANWn)pGU^;cntDh_Y({0dY6*7%K%D% zfhaKP|4vWW3HTJ(IObKGw-B!zlXXxW3X#`-i&qOI)KaN$HT=myt-j3vuy*~r-hSTG zexDRz6x0pZu%k<*6$f^XMm->a|0*&DLmIf-w1EJ6>=STL!#eNNG|)m|>jTuw#s4wL zcobcL01K5+r*aYNe)lYsNvv2J6%^PnaZ2;>`&-RO*{eeM)wysHkz`IBsWex)!2d}?SqqnIVey#+2#WM zhL#g9<=)0Auy-*&-;GYBbBQf@{8>7Si~*WT9`Z^jnxj&~_%ZqN#xsXs4eJp^)R_W!aYaE=h!;zTfkFp2q8^Q`2(v>yFUM$i@2(o9}W#7K*FAFYilE+zOtds!c#@8 zg396RuN)J`skdunORO41J_*prut?b`?|^|Uurpb9Hg-HN^9(z(%X?l4A4u#xc~rHEtm`u~gHZi9%R+{ZlHl8#!p6B z#);?;O@H-`^T+qv!${qM0$xLbY7bLk^#OSm!#3i)7JTv*#vwC|-KyWp&|TNXau518 z&llqts`!5Vnj(O`i?vnWDSF-fADSOlarp`um||}wIwL<%gQZYYX9dlu7~Va_slTWS zx!Ka(2;E^WZhY4=f%rUniym5;dtqJP;F~M5LI9hVWG83@ns~78*lIvc@BVzFd;8y5 zpObZ%Ei$&Nq-5Rrvh_&&hI?3C@DjB>5oq^IDnK9LtlAs^s}B&i z-PW-G9@LOCWO7q+Mp{A*6v5?DT2K!B{*HqLlSUIHVX_z&M{9;aWC(TuEbPTU7Ur2y z(l}+r<>5fV29YlI4tP#k4OJ*7%T+zP@$^X zm>H^&T~q|-ZesM2w{?2Yd8u>& z6)WabA`z-9^3Ws-M4;Y)H~$b{O~{A{5D`#IlPZ#Lig(i>^AHXB$U?+5dr64QySF`; z!9znD;O6AkVQM=lJ_FR7uiPUqd0N~(??MBaAATqN^OQVgI)x_}A4-kINt0I3W|eRQ zc@5;BwB4uqB$1!{X^oe6{Q&!A7|HX#&ZhM31ff7{gWYIOI+0>Tyf^m@3V{mFMU>Da z&rjGuAU>E2<=`dD5C>~Q?^4)A2p5)*95LS@!aYU>*oDeZ#>B*=yC(#1#Eo7jI_-Q^I`LD8H>Fl1a zeZ%YB{#~?3(4-V-e&w7zi4Qf;wvwox-MH5mO{A>EEFO=U!r zuSpL(OL`wJp`I2Y^^CHH2w9*(U9Xij3~d@E_n!D~pU(a?!a4sQznCIET3kmj7uoD3 z(!W&rz~WWSKY~3rf;bEg)hD)kES|2@TRfrfZa(v3R_($yxNe6$tJjw8RSVvta$rkr zt7zIMUj5C?6BieJCL!XGL?R*UezF)3IrP^U-5#lYO)-AWOavq?@+!?USz%qqr*r_k zWqSzo&%0**9^9$;tZB-8m^!pa&Y=g`LX1mE>XJD^(xh*)omKDO0n}r?L^W=}>{YUP zv;R)Xx*7Jf@S;XY`HOeah8?GKG)pP@Jj zmOuD!LrYM0Z`hJ$O)(Wf zfyddz=sCOep7{Fu7LW8WBd2HNbO>DRwl>vn%wL8(ML*C_F+ZgO%SoLU8gxX1d5S&(TZQmN*T|JBHvc0C&pX$j(pZpfsMA4% zeyZnE#ixbfv|Gu)2-0n5^H-49(Gv z06~@X^wn6PIN4C@1ahNCd@cg(*}+yG2O@62j)e7K9Bq_O9;rq@jFZGWSPqXk(_N} zjI^0KWO{$qMmk1AGYF2*y{8V4`;{TW}9$xwD*O; z`T{c%%9CRHeX7pR&O`h!w`JiH&c>kQFV!Zng*w;ouYV*_)Nha;IYv27|1XwU`Gp{gb}Z#tvLEk3i%v$TUh+(=abjfU5@o)X z#LpRVp0_FF%%XqxYiF1NdkkTGz1e z|E6Y@B+)b!b5i@u0PAxLvdUG@c)wzDG;bMnA(mI>N+Yf7!RojDZ&e~Gi z)t|ys$NH1_Cxgc13LLi6OY@r8j7kHhYoLW0<8F@q_zzBz@N!K+!DUWHT1X}}B>pX1 zyG)2M0@*X$s`}^D&reUM(0+7tr;-CJAF-Z#OC{af@R zCRF~ZX@O(reK)OS7)n7qGP`ujyYD_K$5(Gw{NJS#2YFDMV4OQH>v$v z>>LhorY66-ETqZ9{oCRt&a3G<}k z-evW!?Z1m8i}vJucwap3c}IhuEyLO_@fRJBR=Q3bk25azkA340p?0Dt3XanBjAqN! zBxD+6j}@9qjmL@3c|JbOdcA}E);8o;TCM%x&)?w=j_klK2yYOeq|VNf61Xu%eT(X> zL?rRREb%BRxtzeg2=gVzjdSMP#Ji5WH9(79h&u~BY$J5fD(W1(!>c&LA5d|UN_(i+`FThJ%lF=mfu)@-TFL>i$Sfh zk7F*@rDg4jRUaxn2~bk?~=^8A;gv_s@Wx0W63 z>F=(E*I)y%>YozZtbgj1^dgrZQvqBCyLONI*vj0z3hTBi$XIx&HFwkSs`FDBKnsE3NeS2(< z^mVto5IIK}5#-A%oYqU!KhRf`$pORG7pX_+?ts8HMjX#Msv1iW!_bHv0`GS+7+}~KgivD2tT1dd=0EE`QXa3DIsJurZ zp077Az1ph$*=g2BL>CSxCa%_`UgM-v-eaO;w`$L9$^tId(F8|)jG1`U-Zk-b#TV|a z=sM>ynR@jg(ZnH%3Crq`V23H#!mT6SdE4=cu`%#%K1QVRL5iZUU@kXOgf6Nh%042O zL2KR_;FwyJ?6za*8WQ`1{uS2Ap-&DS`;yqvv(D1*v+(;!Zo3qQr>spU3l_V#FSoUV4iI6KA1^g$Sln-$ksf`8cY){BJz0@ljsm=JVn6rA+ay_c&oqgzodfV0VUf(w6#MW$~HA|$dwNF@QJ(^yw5f> zxrDd&d`NXqFdeB=TORfjw`h{+50|FU1TrBOS z8Hlf-pqBb%>Ca*(_+MlSJjA@4k|_~;k5`R?yd>Qf@qjR`K@;z^^SX;8*?4Jl^o&4Y zu;+|Yv!{+k&s3TR38>g-8?q;!TJ+oAlfkn}9;*}`7i3q8mzMMDl|csSCxUf5G*6=P zQ@OXoIH%KIHx$1vf$OrM9{WC4Q2W#UNa#~nriLQ4<}Fe6c3sao8sR79tB7k&7;5j- zUSm+AsdI=swd}P`jgdgukNeB^ID^3@B;&TBH*o_A(8nB<&TCxyKf>=}f362V&zQNa8T6U7cjd#|^5j!WlT&qZG+VPd-ApzDfVuw9%4Pt%X6X^t{#h9#g*U-+uqxCar4Gu%rIZ z(g)}rzJg*+bo>%)nQzzcQk$4A=j$-1QXpd$x0NMhBBv5V z35-pdx1;wln#%9Y$e8q=xcVy{^OW$^SG&pGCN z+R5-VJMR=v##k?1PTJ!X3;0?6-k~|e1Dp;e4f}nXJSn>gXmN*%Ic*>6c@?Zb;-@~3 zOxNaT-pYlnFJr7&z#ODOPL6v zUftfOZ(6@BYd#t(FZbhqmeP}G`&j@#@#&SDFOM)3O8#Mv!9g|a>Ht(qz!Nov)TE&l zG3cC#qru6sj)t1QcImCNi|n9>ZqD>)2x+f%@lpS>^My$&)=YKj|9oBw(JDmb*bG7o zQVf6N+|!Pehh5}k?g};m9@Gf^+$#{ zy|Vr!usx7r&@d>9O2J3Dg*+-aQD^$LVe*q#di;+SVpu*g#?8$9(_v9szTv=mHh6>; z^=%G#E*~DxJx-8!#8xS?d%<4pKerq#6I|l4t!*lqT2l)6e%B0 zk@@s}oNfkEAySD|^3b3=!FRo}Zn()~3bKwOnE4hli+h4Bqwb-n^52HMZ09L)R00f2 zYPBYb(k8Gtk`Y=W^Tf4fEi8Umn|TZjxnE3IFI{j3cx3cN#WjlCy>Da0pPC4Imbv*6 znhXh99FS^~ew`J9aP3)!_*=1y#C0TgEiF)%B>|pAJa#qb!&z0A9GkBR;lfXmaRnC8 zt}}a`f_&<9PosvNShCK(QaaYeh!}dI z63s&l>=^}WECy2ci@&tY34AYUb$ulKrFY{7C_#7zWM*xMM`Gi5DKet&y5n(}gTt*j zc17<`wKaarObbf>#I%KcNlY@rZ~9?Ba^+N4&8xFmIMQI+Kzu!Uo&ae> zzoSj6sLy5Q#bqo}1}$Iq!~Nn|E6U+Fuki~CKGSWUF=F5D7B@<_E=1P@CdrSG3e720NcbxF7x zT=-YD2ZiS=zxb{>JwkA15PUpW5%N;Uz;SniIE4J_$G6uOTvvv-S)_0HvF?L)@^koh ziR#hVl#$|Pp2F#(0+{BvUlx8j^S2C|k)P1D9CM;Kj=fM%OY;s!*4qEs4c+Ljm@UJH zmU|X>W-c5T6=d(GuJ$L3tI=X#sIAvcn#N=2cfv1&(l{P0+yUmT$f5f?$h6{O4_vnxdI!iw*F5zOr^&`0<} z&%ej`CHU=qK&ZCtWB60Id`}O1JJ6gL{0@2qx6WK)ab>rtkmt+Yg)PqrO5K!+7MT?$ zLgg9F(Ui%n$}trY@LK=MR)9owejG zv&b%FJLCwVN0?#dH1NG#S;Dt6P)rqTx0!Fe}5qHoW#~_qW>bco}mVU6E zpNumby*iWCp-f_Yv|!(1#il3VO*hW`;y?36)l;$_if@ zl(txQ#QnM(ln87&e6{#B<%cIQi3C&ZQ(X)h`OOqTYnZOsC%r%CXHL#;sv6HBndK(^ zyqs!+lgUO<-YSb)0q{+D{^fB2AmDonZDyQUE#czrgGq#_kn$?hn?k7OXlH?c0h^f; z$%UZdDZ4Si>hFkJyUF@yKpoFytW*Bv+#BS`ca=fzNiQhbG1+!-S>H$#re*y}zgyvd zj>OL%?6=;eepc~Z^Ie4vK7`)%O*Hibzq?BDR2knayMAd-iL^pjXI!JcI4^92UKfpM z#H|NO6$82Z)PRlqqbxEJoF?kO#bWtzPSRua%@Cw!0!aTUg62`#0tet~4q4u&Te`w7 zcL!MX47kO1A3azz6;?6k@XTq@u)&^iW6eNHT~sI)(}Woq z+j&)*?r(UX%pYObA1+M(IbFEKo;m*xFZm)NRhts+GXSTfW9v3bvy=X;=K|DG4Dr^l zLI@+wW z6|RhBIcj)F)EN~B{0=$s8=O#n} zIsWMAG; zQ|{}zhVeal#278DRO?+idrmmlT3~Kt<7|o}rNrgL7cr6ocam~Rr&EyAFUQ%>gTSXs z2A$DS)rk-E1!5_le$Cd!WCcerHTxVDw497r?Y#YSJSF%0dLjt zlu>d&~Ve}xpcmf)8J$5iLY5XKEC0r1#+U? zlp?Bje@z&2oy?Z;$fIIbU{ZG84+u^)suAyd-*z#>N1^brGYKRH9v&r4(d7o1cX80X z;}NNX=lgLpX)>;W=~3D7>Cs??+%Oa$<_m&j+~c=c+Zqu2PV@A&|n@*LPl zYZ{Fw>1mqrQX19nTMV`C2eO)b=oXChNp?Xtm??Al-@*KNiL>1f93oGdT5>;T-K@U<8h(9~GY!`1j$zHZ!z?zIqQn5BQ7gkw+h%zT zeQ}J!WPe4rUhA$*aQ!n7%hqM*gpepL1&LM>&D3vQ0Jl3@WqP^5O+rAj8s^XCy*+E8_%0^pU_* zm{)f)gV4MGIJ#;Im@LkIHO{YslCvUN!$%bw&>S8;^K7lTE}>4lgXP z%(B4?EU!e$hzZain#bd>=9jKW1j1ww36NIOktAIFx=FTCU!zq}ogV~(w3VXF& zaeuGRJnVTWCZXd}eCf7jt%SC)mGx5gz9K7N0M_hjU@4JGZCEs`O6X!&8yL<#YD0-@hFoiRzv5+&k}Bm;dqr_%${}L8(fZ z7h_!Z9kxuxZBy)mkCEyuP%sawPPlcwrT}OE1mN^(5wPoz;JByFt5GU=9ln?K>y@|D zLucj#I&{D&p9!RE7ej?HvS>1(&^;Az0k1ZzV@B_+0D6h5;zI!AuI0)=`*IS`M$xfcCp)bdwF|hqF|UN9$hn z9A1vokeElv&K$zNmf{s-ZgyDKtTFM>(&7N6G!J(c>C?^BfLQh1wR~i4r`0Ep6(xT= z?`3;3yKte0^=NQjkg9Qg;wwz>XdeF`4H$3)w3-kveN1zu0cPzP!gF`0o4jh4W2B0> z!F{2r(;y{7fEGvRpG|&$I#~O@xZ{ANWnvu&bbyRHK0d*@%dO1rc*R^fc>*_*1 zueDU$6@Z3J6hV32*XXY)+l&=yQH3(`=tqMVuk>7WAZx*_A*b5=z~Rbmqzc(a18&21 zASEtSzf{cPe}(iwGIqa@w*sJ}tumeJ)H>Fp+s(kr=5?<(oqH{|59 zXZ3~ow3d_tW&vq|0nd4yBFtpXyT(R^(YgAr6cSPV1>Z)>>ONlxzv7T%ibmU}W2A`t z_?<1n@ff}ksPRS;{Q@NX`Ch5X#LXG%sgf27C#Y)ydhn@TPWtdF5i7S}GAIxKACVZa zR@xxRpr%mwB1>bK?X;kc>YU>fwyuG`*K0A>-3NiWjs_DlXyrmMz|HyH<6(bOtjXwp z{jl$lm#;JMqXDa#lKaFvIZWeW^SECGA=}@gcj+5d0TT~paWeK7#XK!-B=?D+gE0r=!q+74&_sLPz{Hqxk_?w>r+`|J03Z?SW#s0q5y8H;n- zo55_Sv`?R!R}0SumjeZ)E9yJF+SKkXJ)Lh#fBg~*Si;Q`fN=2TCXKjP&duZd<|6_8 zG2)s4xf7&cSyCKnBbn2rPJyt^31lMc4wI;^eSDXdrCIMsj%cnE*>-*}5tOFh1IhJt zx5zkm?JF9j9ImX|adnzDdXZ${=8jL6r4`I)5iOLi%lNuz_eYonVqb=a$)GQiOC!@* zltzVX2{81g7?-PUn<;Bt_Mh3@Rx8c%7Y~JWW^PV;`)#2|%lUz5yxacBpHD~x8e!8z zuRIf{;nOUfXtS^Hr}eR-*T+{#EESQkvOzG-Qfwp|%Ih({?m6ox<9ZfaP)k~V&>2p{ zTtk(3urVoanz9~ay$aNb9KzM42@D9--nZzS+O*uqF$alP%Pp~5%yj5LE(QOXkv!?2 za~V<$1#%MIYoD?SoirXbYhddFz~F%cImA-u_P+dIEEd)X7@Ps(?~*NqKs)`CAZ))C z?<&~mzjXQGvSu{8X{9FWZv93jll3GQC>5y0#4yFR@&Er)bV2_2&>^^7sNJXBs5egf z9v-P)`YDclBLnQdBU~Q=q&1ByE8+7-TM#Le5I*?-Vq_D4U#B=_(mm%YaLYSP8~1rf z3#(6A{SXBmAB3dQxB3Y1^)US8|+-V#QOF>XfsPb+7jgnGbS5XFGu{LC-HgVro zqR>ZrR!{!c}-wVI5W2Yia$meNU}gs1CBwoG)D6~5vu_c0>= z=)}fd@P9*3jQWaBO)MwUeDOs+f5MqdT~lGSSf7CSYU{hDUwMN-g+rit7&Hese#-_! z+1|n$gA;_t#T5RmlSGQgji+q7R>%Li9|3cY;%we+rx-duDcILB7>!a+9&=EsU`AD1 zdc$_6-uAHJbEU^5QpMw-(<6y-uW&~wT^-Foh>@)w2zxDLp4LaE9^;QBz!e`PjwkOG|T%eX_BZAU3yNwY}x>y^{x~Scsw8Nbr zE$pOMI9H~J2~VZ`NgDd}%#20s;?IdkzDS!86J$y)O+-G2@ik&#)c3wA z|4$>A!Oer^?jJxk_OOPEfkph^^P$PV3GK=Kej|CI8axs;4%e6eUUhy(98PnKTVHn@ zh`DZGUga`gzh2NXKD5QM$msH^dh+S#nSVx<)7RfhbsaE<43kToKSc^|X^ZLEPZkE9 zexpYuuxvWz{&#x#r6ptDXQz2B=LMljc0?;r;rSXP)9kPqudihH!$eWDHtBM$L-aJ3 z%)XXQ&IxgnJ`8m3?*7`|7O~0gq=?_iuj=Yy-tlj-jO}CkA!7EQTeDu7!6HyV?8ZLA zyGX?wdrn*1u>Z3RDq4N~S6kb~o!=&a%l-gUl;o0aa=p*(6LBz1M+5{SQVeI)Vf6=URmYGDD`&Egvy zB-GIj+!1ce4_5)i^ATegBNK9>%mu|qdp}caP~m+aDxhXqY8Ldt!Po)$;OyTbw0g;d zkC*$l{x)m0N_kWd6rkrFy!Je8QbLbczPjQi`Blwwd3yq zBF>R1?eACaA96p9F@!3=F9>t;Q6q6&*-ueDs zm)Vfm2H&_+0@qLFAT34)aaXGb@1L#v+bt)h`-?|ST)v;Fs9{Tzm!A&jkH#*jFZeGs zj-Pwq6so{fb&+UP_7jUofKsHYZ7Bc*$i3`3N0b7$4Qfkqw685_G=ufei<2X+taLx3 z$qvl_-Trs{@ciND{Yw^?mnUfhVmgjBJGP3y_zA(jiBem0Ubp^s@Q#$dCo4YrbhfpJvYXiy_&$|r zq?36WTwah}L|(?xAsdK$mr$AQ`TidFg)EzZWf!GT%r%-%H#~=e_cK6Bkm1c}o=#M_ zQ_D97+w|{dvW#K(bi_Mjf7SFbCUmWK?<+!PXV8VhqXw4fbix^D=oBrjNaZa2y?q3u z#JfrX7NnHtt7XU%0D827;-S2apIp2I{B40wW>H!Z$6-ofRgbf5%my?fH^?HYv9U2~ zdL=cs>!KShw3n8e)RpnU{A3@$*6WUYX&HkL%3(&FsHnO$D`;7R9?0t`OhhI?Dv>hW zt8KY1V~?uW3?G0&Ub;p52~$dc;DezKUyk9yduA^Rv-}zy{)Mse531Np3okr1{#yAu z1+F{c`cdjW5T9hdXjp=+6cx;6VIWEXMi3ChNKV~TQ|0S`l#-v{$4Ogow1I%`yl&># zkYl@mdn98|4W4PD(K!Fge@KUG3sRNQHsfYD*qn}Vq87ji$-k8=%(|HR-{^ zT;KRXgaM0K+|LAT+Z%m{yAmb3j5ZH)h-kl5y)*Wtzs+y^%`RHd!0oQcd>QVG7p(Dg zbmupNBy5H=O^!JW_>3ulevEV{izqEE3^{_I8z09EYG<~ef$Y73|C>4qQ)L7mHElyD z^n5<6Za^@i&#~yxG{EIdB49AOl>uV*UkRy$qU`ich=Uu(4Lz>zrT)(#4=DGOd=a)d z@=YHxfCnfu6R>j73~4b3#+#&f%F>dwS>!?~1&&_Y2Qvx`lAnpK)sxZQ0p;KxX7+Gh zu9dzFY^%zdX@QacTSqkz5g5c6CM<|K->qb~Pk;xlJ$?ll$dX=ght0Oj)Y)PniT(?4 z9UV=pw+=tBDeXbh#%uOwW$k2gaAOg|)S95( z%2Z&oAr;Z88fpo$o!IAN{6aUX-?8v|e!(pfz}$1dRe|Az^#984fngfE8H<&bgBr_=vb~zScs%d|nE8QvV#J)pot?y+OG+qs2zH7NNcr;T)kzD)=}Lw+4F$$l!|-{DC!H!&zn#HV2rONZ_%c|EdFY zID2=(2%O=}7L$~Nlc;+B0@TD-TtSX5X(+WuhS>PAY3<2*F$`O*)e{8F=)eSGs;t2I z!uL2{e_bCCk6R#(ngg&hP57s3T<@SAy*(!^y|Vm4-p`ez7f{62_);H}YL?U-fKFR# zE&b=yUjtq7)sIj%c>eM4+)cvTdk&SPu_$@yoz+a-0C?w%!&O@GcOdA`qftH?;y)zu zP^PGk$ogbTqN8UE^l3DZ-sXazQs$U^b~FsagBM$y^PX$X>to&u)6$kH6<2jJigpR> zk2tbXP)40cJL$wXWicK}sXJzulo*w%_E{)dPbBD`>ulePaZ5bXIA{f$DG|A~BwIfu zHm=KQp9OU%3a0DKi(;G~ONb_4Q90q!9!$2@_TtR@TQ)FawbmNRDm`DXQF5LK@R4@4 z)r{=!OgjJ;5#MaplM=AtbCq_>2Q}hX<7)HYY8M5?BEsf+WaX9*JB^X$_2=WS-2H{O zeH}RJfd84T%6VUjDQ+X@b@f0#KxD ziLcH{kilvqlCR>6!|DG}9@ZPrl@_f>sezX?7BA0fDu>xBD@20KujZg;3sui77|85{ zVN%5PVbMudQ^j#tAkswSx|7WEh1`R=m5CYCpp(BU5E{5wwfVuJ)U5NdIRFp8h}sZX zG8Tj@rtaw^m2{?yPyVOuN;t9G)Nmvx2L_kmbq}rUjFN8Wj835K%KDxQ)+UC@_ikkW zF8qu4T|JCo;lL@-V2)W>BOy#m(%N{!qAE{Ca8CB>;l}GjaY936~#VU2HB| zukeSuJFJT}dkn{Ab8~ZRHT^`bc6J64$CRrPIV#1}I90a?>_K+b76KA8r>Ttr=3ibh zw8;m&xV!9XgjyrZwvn^9@5kMy2_%LtFESGfP(Uj2)G}_L!-$!kD0N;BdN3cs%)V5ShGgk^R+%F9@gQjLSDmu#ZSLKQOV-uO!C}b5 zwb<)d30Z-6d8@(SSND4`=!Nj5&-HLGrY_eT2IvGo$v(6P?^cv?jY*v)245~XkDQIk zdIg{KlaFZfes_?0ycx?5ySp^M&T{Adb{rKOG4ObHKBWt}mcAcVr4O5yxt_q-LIYDJn0bZ z{HBGSxzl!pCn%YGOLq4380m_YB$O9&__ZTl$)z3jr*o;JU;S0tmTktI1z6){Uy2wW zYJP6WnpHb%aJ~kO1HRPp+kf(&*Xf7PHYwxAm_JQ(E%U;y{-h7xnNrubK$WnF>5??* zFkdtnV;yFNT&2bnu`K+%TJ?(1qke}b8{Cds)Y?@F+SB&@I@_xDTe&5-1=Id$<+}+R z=9lES=~+mI5yhX>i;C7WV3_8x=r;GvHLShDoR#SPEyiVqhV~VcQ3*WC++#~ds==LlaUOmPyjzTQhPQR9W*0ezTy351e zLrq*eaJ|Fzs;obb6>!~s`Yg?R@qT-b2s%miC%Z|11mBLCCP+kz>E>5jVIj06w!Pvd zdM#Ls|!#Na zW5(h_jh3AO3-r@;pB?L=|M+;IVbHmiU``<+~F?kuxZ1{q*3!8BV^_BZ?ZA~L^QD$|yfe8;4(rY()S|J+$p z&QbZ4=jV+z1z6^)oD_tA&;Od>mJRvbDHQUrz%aWDt)oGJZko&!j<4}rtY97uW}qGS zjFMbKdSN+m)iXj5(tHkcg%W7FRe6dlsl(qdUe#!WACxby9*3chsL4OkV7D_DH+HTh zyt}La!a@3%ifCWwq}bwPcq!OH%HWh-$w0CPc^D#6kjvp%%nY5k&NcV~0ZmpJ`Ym?? z^9|m{u6l*_V#RtdO1WmShifQi0h?0QxJ4w{i1v9eGoiA$SsZ1CR;7+e@Tr5KBZ9=x z80Yo&F#p-km~MP54*6$h&Gbx1WO^1Ur%9%9l}Xl2m1*_|YSGUR58<5}$YmF?FAc?$xrYd9_gEV|VTpjs=L<_Z9}Stu>opu{;FtA$K5y z&ecib^L`UjcVcx|CcnLkJPl&KA@s&q(i>?-5>vAOo?L~ooAluIw1*VG_hNFHVnf=~Tl^j}Cg52@rm2y)` z0JBj(g%cuOQ%Y@*c7c{Ily zGobWeJ;lesBZ&#)317YxVoQXGqQ^%SP~!K{#b(!76jKT0?Zw`K1P;4^GwtacjxDZY zFR;wht=5)56fCR;=8DLOtntf}JZ%3N5GYpELvBsFmPeU%uAD`s&PrrbJDMvwhu*lz z$7Kj_*2!F#cQ9_?GEwvfPmy>wLSrhdqD-;y(d`$6;sz^G4SO+pl^e6Yy1weXWxuYr zf%R0y41Cef-aj;FZ#VL>q_KXhl~J~~M5fNfFVfhMHkK#dNzkm3$=9UVWF60AY=*Yq z$67;D*39^n^D4}oTpj8;Y=6peP4yZ0RrGS{F!94ZJVEMcL|8au4h0t1%7h+kW{ba& zf=SWAgfDmN3vd{}$O%AW1XUdLIHH@_>Y8$iSu%H2J_<$nPAG3fcJkcAy>~0N2k0N_ zgRibPN5(#i!!x;>js@q!(=x(jA4TsW5Bbh#kAV!2wD&wWckiFC`o?`Qr(Ko!eL;bw zblCpE7W1cC(F*^&6jW&g&)WnYqd`{0De)!&hgZmJ8>@#IYSMFbe|8@zyErqG($}j{ zredcxbV^0Sj-c=)g2s?m3t+;;ogVfVoB{*eZZvui!goZ-@HX?_+^L#br z*BpDCZzi`gmd03Qk8F~(%m{aJ_=GcSPSL2C7W$yXH4OGGwZgs4UNZ;jFH7q3Bf4%p zF(7pBvGWh8f4a31N<{n-C+H zI?GsBX4LftL2`yq$U-m_I4ZMruD$Zu*w}NJPrPq#cZic@I7qcHj}V8^%fDLr_Eo1~ zG()7>H(RYlEsWYtTu>M_LlHfA6JwD>Hzf^n>cc!sDE`P1$1B`EP^S8k>sNPl>3L#? z8sis~syC%-_6!K6UJet=m+Var@E2z_uSeN2dHlO%JTlF_k=(JjZ%Z{Z7Eg%S`{vi{ zU1EmJx%n6V5MfD%I_i}UcYMow+)UNsh^ywPf0=)nQ%%nBwv@o|ZEg>y5KAK#F^gFw z+)i9UAKj?(eCO(Z%Kj5~T$S5>pcTeHY3bNo+g#}0Mm-r5DROiT$if!DWX}Ypc+$P3 zIV2YbrB=o1+Ql5JFsGnVdBXdS4(y9&x0%X!c7B6<{Ap8;?qHLrL*0mYdPl^ln3BiZe-V zkhZPh8q=Jmsc8gPp|gAjuLgbB`c}(0(((#j#A9BM)kYn2YXVF(SL0KpIdRyvj()~@ zFiJzP>vDH};(syFR?Lf-uYvsCRif$mz>+*(!{}n*c-9Bq-XS$(n&b$7GtBSw`eAEh z_mgs{RxYT|r~vs+&z@oL@81Rm+~vudl(B3}lYpE2Ddf9j`A2{rFaY z$MK;dKRUOfGE9#Hb+pSx@q9Vu+UO?WIrR}@BYP*uuu2uKak;->$f}}REi*JhUc)5R zovx?Gh>HzGLNjgg&4UxR)>WB(c&J}n1pj9e> zjOV2pfx|E#9%DZ>kw~&%VBj2<7)kz|HiJx-mfdBG@MQSQDu=mROOu#t?h|Fo;<4sd z;)Bn^?m!r$^2I0Kxlq#kfV6sQk;KKVDvS8kceC0#F44cB0s%|DgJG(PgHQtI%BFRF%&<@H2o}GCVc6jE^sWLffl%*@Lb^C83$&hGFW}Tc7 z%!Zs;SIjwbA3+Jr6`|xb-2Z9}68Vd(%p}L~b!8T~Ag$F5gJ$crod)BD1RkKjBiW^b z&ZEtsG$3`ow+MG$(N2{Ikgz8)>*9Y!ghfqw!P4ia4-XF#Czlr&!p)9p7};v`CrfH2 z{EM=8eXQ8eYW>i6^L&VaywA-`l~IJ(kDjdt)K4en%;#Pae-xkU`G5Xn^UoC$bC){< zA?xqd0)FC(nytTc5|=yJHAf>$m8sv5UZ!C?II z!_d^y7}KM)NlMn&K|8MDrw7&s?gs{9sARD)T35isP3#X737oODNDMl*I02RU*PA#& zTL_r^HRaJHOo4}b)CCrmm_jKyEGZ#lAd#3_f20yGpc164EXben*E!_skQ?)>huSMN zeU-tKdl?5k&MohV73QR(L_$3A(|(%x5v#C^nW!Wr3rxv3(A1Q%0OlLS5kY5=#^30< zB*M8eKDhE`kDGI)!F~YU-cTJpwug1nbBxI3H;H>y4K~>$8j2_7jA{(#rsiBpvN5hA zaZC)o*rex0Z8pHGvkGk>wcqzx?OKi(6%xo$tL=Wcl>qE|jfbU*d0mt^@p$+bD1sL3 z?~ifC@y6UH_`uD(IM)EW_Li}R-GvG6CJK$_ zaZrcb^S@~N@WHzC?_E7Vn&c82ekODic)2(TB>VaM!w&DDN-VIzJR+8C(7yNyB3ry7a z#|1n|f$*;vrlDX;E&_5qV}_vnX5U^?5l^wyYwnaJ8(Jvuv{~?33^Deznza9>y1T^e zW1sd&0(cjmNJ*UN==lE-95K0A6sRzp0R_~s>A6SP^Mq~3FH`# zO$5YPyaOpf0A&JiwqoC!Al7k!>OtU`n63m;rimI+&;rx6{7IN3%$7~UsC(g3N<%l6 z|DL#*NhQCSjIfq>gW#U4PpLN~B1|5RXw1t~kOR);<76q|BGMJd{im1!rv=$9t=J&_ z6$P-);dZ)WyKI2Q5Yw%+>3;uD@yO*vIHwYsU%$s(4a7ON(`BNlz(WNj|BbOHIl&;U zp-514>!+a29G6`pEuMwSx5DJuOf@sz-d&Rziq5c`6isXZWT&roZ^T=GpvwY0k>2Dy zFDcTVEMM#U6F0NU7vRBI4;vL2UxNJKZsVG#yu%hw<_20vak0*95b#8h(6X`Eu7)e(ECITW%G&8Dd7!x7f`fmVz^V54%&O_vIQ!&^NT3<$j09C(rw>7*st zki)njhySei160Fx1%PRMNdrWn5(q)(a=mS|dA&^}=_?lzreubdaSG8_Ii$8kT6rET zlYuCKIr+c+Wg=FjID1L{?Cmx6;wBF>r;LSe3(A|1van+c*mVW1$HvN;F5>u)`c;rP zRC^x|&<6gpjivrPq6UteTkm_>s(Ymp&+nA(ADF!=43HKeb8DVSIKPy~7BC^GCaz;5 zkxd0aS9$itp>M!5O?mjRb#BNj#x>ZboaI>_BRkw|p~K%j_boNlVshxzqh85C3H$Bf@hF6px#dh_N)cN-6Aj zjz-0b39dEVZ*CWympZ;cHkBwlw`u~q;uM?A2ca>wpMVnRK_jRFVfKvaSRS8obQ;U^ zRL;cqVyM`UE=A8wr8?U8I} zH0nxV|6M_}h1Z({pX!JUnVcf@!*yk4cgv$N(+cHt4cexKqf_I?*Oe%Xy8{juKfBNt z-ehTl3wz%XStnH-ZvZ7Jrr-tcZd%9pf0s*pN|ch#wV@O5)I`icl;XY)>lS55*DN(c ziTQcfx_$#0VcxrjuB$JO_iMjXJ%QCjqnYHamKl`)5^IV_XBo8f1s?wDZD~X_1+Ip* z=}a?Pz}0iu3>(9O-S_5hi%uBNkW8P1I9J`jHU{*O+wP}lv0675F7AI2U8%Rj%{n}M z#rch7f@gGmA>?MO<<>zK8>AHr(0NG1JDpaq)vhRS#ptb5gSYG_?`pZlJ*=d6axA1L zsn&<_P}>0&4a_ywh3P+;@$d^? z9<%0H6vIeUU506jL6t-beb#xsFq5wu&%P*Gn`Y#d1Yqi!PG?oguCVv*aREVf0!mZ> zI)dDzJX|C*Nbxl5I{@)Md?;4Uew^x=`%i&Z-!y!=be8HFIzI;R%Y@e`6GddsT$sQzN~|Z++G9+^-E!&}Lmo2-rFV;4qQ>t_DHur~CcnpL9h8X2Zzx&G@-)ovjCD; z@`J`){fB(3k=1n{zAAIr9@SpiD|z$1Zg-bs8s~V_oK>c)zmy@&j$>!b(4d?qfuSze zh|Ure2@nC2C=N%mL~7@UlQctYkR~ml0cKo0Yg>6!wY=Im6`oY9W?W(;#^NL8`mY+> zgHo8|Z!O*xTp9r|qC&D>%?~5lq7YlSTEK|#oS7}$$+GJA_ElF)rGxXf`Z*{4U8bcW zV=M*dFe3CO3TK%6$R9$zEShdkrKH-%B8;@1X>uBA>`fe5R3hTs2mYl>#Az+h*};?! zXPkgY`}=H-Xh8;`oJd>`c%VOYMk1^pXx9F2Rb6XT;(~9~=NEFN zc|(9Iq0OX{NiK82ezut9{JCw>JBg_K%4aymzJHOHWWG?HL7JNRbIYMvc9U1)Zd}>5CH?;9)9m@LoQS4CcOm;{`G@bmk*qBJK^T>Y|t?ytL5fsEpTX6mc%TRSvi9$C*Tu-Aw%bGWmPqS zecQv2Dk9!QD4)|0AznMp8G&VFt@)^dd|U%$36u-~5rJcl{|uoRDT9^9RoDQjJDubN zKzEI2vRDqA{RO4qMC6>9?F>hqm7D_}Ysd6Gz)ff2T59JHkpI0d0Jm?u2tUtz;=d*L#a=Nfx9P)X;?$q&gzl~i2y_WL$~@M7Xpv4j8<*yf{x#RJU#hhQe6 z5Oyhc zEg0Qa+lI&a3}fpnojgGm9N!#p3fV5|gA$!MS8H>!!Ai8o2WBs+ zln?A@#>0a+jZZVr)p*VT7vm&`O#OA+se7=wH^oSf`0IU9 zwuJu7FXzhZdXH<3^IG2X`<4*r^Y>v)ZZme~{hXUh2jT9-&`FB6XWIIie^X8hzv*s&!OYgb@V(R? zEHJ42Z)%ffo@;Z%X0wcAt5q5F*l>FxGw_>#hBaDCojOl^@0Ej8x%Ig_H&IJ81l^Xh zCZp#|3D#YFW`C+rL2Coh#1Rh?`;oa_`$CEtH>fqI29)0^AVw)Ec_V+&1M~>O!opg9 z7-qlQ%9{KD8-pA~0;U7nKmKUhg63HU%6YW@Ke4bp=Bdn)ytdouyB46oL8nDnXWsOM zhYc3AwlHA{d+KxS0_u{=jI3Y&Hs6R{mfke}QlKT}jTG=i_VvfbLI@c*zvCZ_n#E(T zvA9jp)|p|*^(+ffssH%cb~XNU6D<6XiVhtHGNO(y4mrpnd^d=Da2XZ`#>s&yN)`89 zBYK=@@6b+3tMJoHVfB4^U1HB_{OES`oEh>HCM^U$Zk_R#ufEBb0XIp~ykkm@zdbmq zaNqqE=8kt6I!)i<1p$v>&v@17D%d7{3hWiZ4!?MRXxP>)SMF~9xZ5^X*P=EZFiQ0M z+hlS)4XiMC{#N!qTP)-#PJ93X|rcofGB0dLoFk3x9+%J*?XsN zcV?gX81?V0RQyMlJanrsOS8&C6sYgnp1us{>T%YafN~ilr3L(vwg!!K)9+jbA2z+o zXYiNGN_(@|XnrWZrhj&8mpHORHy??V_VGFmpTS?P(*$ee@cn~VF`3Kua1y_)Vph8L z2vw2c^s3x!Lf+|bFzp>4w#t@kBO8lS4+#XR{h;&J@Sy+PO$q!uxdrvWFW{msuE*vS zU|7DPC)Ja;CB|tS*HUmjhVdo@U@H#Vsi)n`5_MzMl3nQ+du`+j lgw?<3%3G_(liQEazBqW{M{{nR1C3i0b!8o;S_PY^{{bULyj%bP diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonBlack60ScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonBlack60ScreenshotPreview_0.png index 77e09b59f297ff36ca88736f6646e272844c827e..92946e3f5ba55de5ce47ae2c7d213afc656396c6 100644 GIT binary patch literal 5254 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 WMG%)7&4KrefRda#szk=@<^KVJ)*M;@ literal 4626 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 b>#m~3uXRo=;fodUt%nM$qM=-Xe)jHv#BCfL diff --git a/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png b/ui/components/capture/src/screenshotTestStableDebug/reference/com/google/jetpackcamera/ui/components/capture/CaptureButtonScreenshotTestKt/PressedStandardCaptureButtonScreenshotPreview_0.png index f39338c7a50cfed993741c33589388ea2281dfc0..bb61aff4a5345a1274b0111f759b389fbe1229be 100644 GIT binary patch literal 5195 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 literal 4613 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 From 837e4e20eb07f74f7fe575dad9d69b7c3d1be76d Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 20 May 2026 06:39:58 +0000 Subject: [PATCH 37/40] Use string resources for CaptureButton content descriptions --- .../capture/CaptureButtonComponents.kt | 19 +++++++++++++------ .../capture/src/main/res/values/strings.xml | 7 +++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index 1a07d87a2..bbdd55ccd 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -74,6 +74,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.disabled @@ -523,6 +524,12 @@ private fun CaptureButton( } else { Modifier } + val capturePhotoDesc = stringResource(R.string.capture_button_capture_photo) + val startVideoDesc = stringResource(R.string.capture_button_start_video_recording) + val recordingVideoDesc = stringResource(R.string.capture_button_recording_video) + val stopVideoDesc = stringResource(R.string.capture_button_stop_video_recording) + val unavailableDesc = stringResource(R.string.capture_button_unavailable) + CaptureButtonRing( modifier = modifier .onSizeChanged { @@ -536,13 +543,13 @@ private fun CaptureButton( } contentDescription = when (val uiState = captureButtonUiState) { is CaptureButtonUiState.Enabled.Idle -> when (uiState.captureMode) { - CaptureMode.STANDARD -> "Capture Photo" - CaptureMode.IMAGE_ONLY -> "Capture Photo" - CaptureMode.VIDEO_ONLY -> "Start Video Recording" + CaptureMode.STANDARD -> capturePhotoDesc + CaptureMode.IMAGE_ONLY -> capturePhotoDesc + CaptureMode.VIDEO_ONLY -> startVideoDesc } - CaptureButtonUiState.Enabled.Recording.PressedRecording -> "Recording Video" - CaptureButtonUiState.Enabled.Recording.LockedRecording -> "Stop Video Recording" - CaptureButtonUiState.Unavailable -> "Capture Button Unavailable" + CaptureButtonUiState.Enabled.Recording.PressedRecording -> recordingVideoDesc + CaptureButtonUiState.Enabled.Recording.LockedRecording -> stopVideoDesc + CaptureButtonUiState.Unavailable -> unavailableDesc } } .focusable() diff --git a/ui/components/capture/src/main/res/values/strings.xml b/ui/components/capture/src/main/res/values/strings.xml index b551cfec0..d79c17597 100644 --- a/ui/components/capture/src/main/res/values/strings.xml +++ b/ui/components/capture/src/main/res/values/strings.xml @@ -141,4 +141,11 @@ View recently saved media + + + Capture Photo + Start Video Recording + Recording Video + Stop Video Recording + Capture Button Unavailable \ No newline at end of file From 5ba312f39a0d86de4626a78fb0647b8fefe35fb4 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Wed, 20 May 2026 07:10:40 +0000 Subject: [PATCH 38/40] Address review comments: move alpha values and durations to constants, and move preview function --- .../capture/CaptureButtonComponents.kt | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt index bbdd55ccd..e68c8fdeb 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureButtonComponents.kt @@ -102,6 +102,11 @@ private const val BORDER_WIDTH = 3f private const val ANIMATION_DURATION_SIZE = 250 private const val ANIMATION_DURATION_COLOR = 150 private const val ANIMATION_DURATION_NUCLEUS_RELEASE = 100 +private const val ANIMATION_DURATION_DISABLED = 500 +private const val ANIMATION_DURATION_NUCLEUS_PRESSED = 50 +private const val ALPHA_DISABLED_NUCLEUS = 0.6f +private const val ALPHA_WHITE_20 = 0.2f +private const val ALPHA_BLACK_60 = 0.6f private val LOCKED_CORNER_RADIUS = 8.dp @@ -425,7 +430,10 @@ private fun CaptureButton( } -> LocalContentColor.current else -> Color.Transparent }, - animationSpec = tween(durationMillis = if (isVisuallyDisabled) 500 else 150), + animationSpec = tween( + durationMillis = + if (isVisuallyDisabled) ANIMATION_DURATION_DISABLED else ANIMATION_DURATION_COLOR + ), label = "Capture Button Color" ) @@ -610,8 +618,8 @@ internal fun CaptureButtonRing( ) { val backgroundStyle = LocalShutterBackgroundStyle.current val targetBackgroundColor = when (backgroundStyle) { - ShutterBackgroundStyle.WHITE_20 -> Color.White.copy(alpha = 0.2f) - ShutterBackgroundStyle.BLACK_60 -> Color.Black.copy(alpha = 0.6f) + ShutterBackgroundStyle.WHITE_20 -> Color.White.copy(alpha = ALPHA_WHITE_20) + ShutterBackgroundStyle.BLACK_60 -> Color.Black.copy(alpha = ALPHA_BLACK_60) } val backgroundColor by animateColorAsState( targetValue = targetBackgroundColor, @@ -868,20 +876,20 @@ internal fun CaptureButtonNucleus( transitionSpec = { when { NucleusState.Disabled isTransitioningTo NucleusState.Idle -> tween( - durationMillis = 150 + durationMillis = ANIMATION_DURATION_COLOR ) NucleusState.Idle isTransitioningTo NucleusState.Disabled -> tween( - durationMillis = 500 + durationMillis = ANIMATION_DURATION_DISABLED ) NucleusState.Pressed isTransitioningTo NucleusState.Idle -> tween( - durationMillis = 50 + durationMillis = ANIMATION_DURATION_NUCLEUS_PRESSED ) else -> snap() } } ) { state -> when (state) { - NucleusState.Disabled -> Color.Black.copy(alpha = 0.6f) + NucleusState.Disabled -> Color.Black.copy(alpha = ALPHA_DISABLED_NUCLEUS) NucleusState.Pressed -> imageCaptureModeColor NucleusState.Idle -> { when (val uiState = currentUiState.value) { @@ -914,14 +922,6 @@ internal fun CaptureButtonNucleus( } } -@Preview -@Composable -internal fun CaptureButtonUnavailablePreview() { - PreviewCaptureButton( - captureButtonUiState = CaptureButtonUiState.Unavailable - ) -} - @Composable internal fun PreviewCaptureButton( captureButtonUiState: CaptureButtonUiState, @@ -957,6 +957,14 @@ internal fun PreviewCaptureButton( } } +@Preview +@Composable +internal fun CaptureButtonUnavailablePreview() { + PreviewCaptureButton( + captureButtonUiState = CaptureButtonUiState.Unavailable + ) +} + @Preview @Composable internal fun IdleStandardCaptureButtonPreview() { From 38f01e45d8d5e6baaa1c3204ab0e2918be45de2f Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 22 May 2026 00:48:49 +0000 Subject: [PATCH 39/40] Clean up unnecessary changes and debug logs --- .../java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt | 1 + .../com/google/jetpackcamera/VideoRecordingDeviceTest.kt | 1 + .../jetpackcamera/ui/components/capture/CaptureLayout.kt | 5 +---- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt index 4d1f218ce..3bda5787f 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/ImageCaptureDeviceTest.kt @@ -92,6 +92,7 @@ internal class ImageCaptureDeviceTest { composeTestRule.onNodeWithTag(CAPTURE_BUTTON) .assertExists() .performClick() + verifyImageCaptureSuccess() } diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index 369af347d..3dfc40ffa 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -79,6 +79,7 @@ internal class VideoRecordingDeviceTest { // Wait for the capture button to be displayed composeTestRule.waitForCaptureButton() composeTestRule.longClickForVideoRecordingCheckingElapsedTime() + verifyVideoCaptureSuccess() deleteFilesInDirAfterTimestamp(MOVIES_DIR_PATH, instrumentation, timeStamp) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt index 7c62dae4f..aa195a3d2 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt @@ -94,10 +94,7 @@ fun PreviewLayout( val isOverlapping = (viewfinderBottom - buttonTop) >= buttonHeight / 2 && buttonHeight > 0 && viewfinderBottom > 0 - android.util.Log.d( - "PreviewLayout", - "buttonTop: $buttonTop, buttonBottom: $buttonBottom, viewfinderBottom: $viewfinderBottom, isOverlapping: $isOverlapping" - ) + Scaffold( modifier = Modifier.fillMaxSize(), From 68cc38184160f8c1879bddec42e21ca346f28dea Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 22 May 2026 02:29:28 +0000 Subject: [PATCH 40/40] Apply spotless formatting to CaptureLayout.kt --- .../google/jetpackcamera/ui/components/capture/CaptureLayout.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt index aa195a3d2..db46f2dc6 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureLayout.kt @@ -95,7 +95,6 @@ fun PreviewLayout( buttonHeight > 0 && viewfinderBottom > 0 - Scaffold( modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackbarHostState) }