From 11472bfb3ce6e71e72f2e48f05a6b456972fb7e1 Mon Sep 17 00:00:00 2001 From: davidjiagoogle Date: Tue, 21 Jul 2026 23:32:24 +0000 Subject: [PATCH] Fix crash on rapid double-clicking video record button Add CaptureButtonUiState.Enabled.Recording.Pending state to represent the window when video recording is starting. Map VideoRecordingState.Starting to Pending instead of Idle, preventing rapid double-clicks from sending duplicate StartRecordingEvents. Additionally, update CameraSession to log a warning instead of throwing an unhandled IllegalStateException when receiving duplicate StartRecordingEvents during active recording. Bug: 535645067 Test: ./gradlew :ui:uistateadapter:capture:test --- .../com/google/jetpackcamera/core/camera/CameraSession.kt | 5 ++++- .../ui/components/capture/CaptureButtonComponents.kt | 2 ++ .../jetpackcamera/ui/uistate/capture/CaptureButtonUiState.kt | 5 +++++ .../ui/uistateadapter/capture/CaptureButtonUiStateAdapter.kt | 3 +-- .../capture/CaptureButtonUiStateAdapterTest.kt | 5 +++-- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt index 8dd60c4a7..8c3869e9b 100644 --- a/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt +++ b/core/camera/src/main/java/com/google/jetpackcamera/core/camera/CameraSession.kt @@ -1224,7 +1224,10 @@ private suspend fun runVideoRecording( for (event in videoControlEvents) { when (event) { is VideoCaptureControlEvent.StartRecordingEvent -> - throw IllegalStateException("A recording is already in progress") + Log.w( + TAG, + "A recording is already in progress, ignoring extra StartRecordingEvent" + ) VideoCaptureControlEvent.StopRecordingEvent -> { recordingSettingsUpdater.cancel() 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 820d16678..0c9dc3054 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 @@ -272,6 +272,7 @@ internal fun CaptureButton( } CaptureButtonUiState.Enabled.Recording.LockedRecording -> onStopRecording() + CaptureButtonUiState.Enabled.Recording.Pending, CaptureButtonUiState.Enabled.Recording.PressedRecording, CaptureButtonUiState.Unavailable -> { } @@ -668,6 +669,7 @@ private fun CaptureButtonNucleus( // inner circle fills white ring when locked CaptureButtonUiState.Enabled.Recording.LockedRecording -> captureButtonSize.dp + CaptureButtonUiState.Enabled.Recording.Pending, CaptureButtonUiState.Enabled.Recording.PressedRecording -> (captureButtonSize * pressedVideoCaptureScale).dp 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 04d3837b4..0e7132a38 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 @@ -61,6 +61,11 @@ sealed interface CaptureButtonUiState { sealed interface Recording : Enabled { override val isEnabled: Boolean get() = true + /** + * The video recording request has been sent and is pending initialization. + */ + data object Pending : Recording + /** * The user is actively pressing the button to record video (press-and-hold). */ 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 108b65e34..55566aa42 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 @@ -55,8 +55,7 @@ fun CaptureButtonUiState.Companion.from( } is VideoRecordingState.Starting -> - CaptureButtonUiState - .Enabled.Idle(captureMode = cameraAppSettings.captureMode) + CaptureButtonUiState.Enabled.Recording.Pending } } else { CaptureButtonUiState 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 index 70c755780..f442e868e 100644 --- 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 @@ -95,7 +95,7 @@ class CaptureButtonUiStateAdapterTest { } @Test - fun from_cameraRunning_recordingStarting_returnsIdleAndEnabled() { + fun from_cameraRunning_recordingStarting_returnsPendingRecording() { val cameraState = defaultCameraState.copy( videoRecordingState = VideoRecordingState.Starting(null) ) @@ -105,7 +105,8 @@ class CaptureButtonUiStateAdapterTest { lockedState = false ) - assertThat(uiState).isInstanceOf(CaptureButtonUiState.Enabled.Idle::class.java) + assertThat(uiState) + .isInstanceOf(CaptureButtonUiState.Enabled.Recording.Pending::class.java) assertThat(uiState.isEnabled).isTrue() } }