diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index 0c949e17a..1eab24623 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -34,7 +34,7 @@ android { defaultConfig { //Our app has a minSDK of 21, but in order for the benchmark tool to function, it must be 23 - minSdk = 23 + minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() // allows the benchmark to be run on an emulator 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/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt index 50e4e6b17..3ed8e101d 100644 --- a/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt +++ b/core/camera/testing/src/main/java/com/google/jetpackcamera/core/camera/testing/FakeCameraSystem.kt @@ -129,6 +129,8 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti screenFlashEvents.trySend(event) } + var numVideoRecordingStarts = 0 + override suspend fun startVideoRecording( saveLocation: SaveLocation, onVideoRecord: (OnVideoRecordEvent) -> Unit @@ -136,6 +138,7 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti if (!useCasesBinded) { throw IllegalStateException("Usecases not bound") } + numVideoRecordingStarts++ recordingInProgress = true } diff --git a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt index fbb878e5c..e3e603d1a 100644 --- a/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt +++ b/feature/preview/src/test/java/com/google/jetpackcamera/feature/preview/PreviewViewModelTest.kt @@ -114,6 +114,22 @@ class PreviewViewModelTest { assertThat(cameraSystem.recordingInProgress).isFalse() } + @Test + fun fastStartAndStopVideoRecording_cancelsRecording() = runTest(StandardTestDispatcher()) { + startCameraUntilRunning() + + // Start and stop immediately without advancing the dispatcher + previewViewModel.captureController.startVideoRecording() + previewViewModel.captureController.stopVideoRecording() + + // Let the coroutines execute + advanceUntilIdle() + + // Verify that because we cancelled the job synchronously, the start implementation + // was bypassed completely to protect us from the channel race condition! + assertThat(cameraSystem.numVideoRecordingStarts).isEqualTo(0) + } + @Test fun setFlash() = runTest(StandardTestDispatcher()) { previewViewModel.cameraController.startCamera() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69f1d6c6d..a2317d598 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ compileSdk = "36" desugar_jdk_libs = "2.1.5" orchestrator = "1.6.1" -minSdk = "23" +minSdk = "24" targetSdk = "35" # Used below in dependency definitions 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..88ce0c69f 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 @@ -250,8 +250,12 @@ internal fun CaptureButton( if (firstKeyPressed.value == captureSource) { if (isLongPressing.value) { if (!isLocked && - currentUiState.value is - CaptureButtonUiState.Enabled.Recording.PressedRecording + ( + currentUiState.value is + CaptureButtonUiState.Enabled.Recording.PressedRecording || + currentUiState.value is + CaptureButtonUiState.Enabled.Recording.Starting + ) ) { Log.d(TAG, "Stopping recording") onStopRecording() @@ -272,6 +276,7 @@ internal fun CaptureButton( } CaptureButtonUiState.Enabled.Recording.LockedRecording -> onStopRecording() + CaptureButtonUiState.Enabled.Recording.Starting, CaptureButtonUiState.Enabled.Recording.PressedRecording, CaptureButtonUiState.Unavailable -> { } @@ -668,6 +673,7 @@ private fun CaptureButtonNucleus( // inner circle fills white ring when locked CaptureButtonUiState.Enabled.Recording.LockedRecording -> captureButtonSize.dp + CaptureButtonUiState.Enabled.Recording.Starting, CaptureButtonUiState.Enabled.Recording.PressedRecording -> (captureButtonSize * pressedVideoCaptureScale).dp diff --git a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/CaptureControllerImpl.kt b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/CaptureControllerImpl.kt index 90c1a4af3..539b3b583 100644 --- a/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/CaptureControllerImpl.kt +++ b/ui/controller/impl/src/main/java/com/google/jetpackcamera/ui/controller/impl/CaptureControllerImpl.kt @@ -192,9 +192,10 @@ class CaptureControllerImpl( override fun stopVideoRecording() { Log.d(TAG, "stopVideoRecording") + recordingJob?.cancel() + recordingJob = null scope.launch { cameraSystem.stopVideoRecording() - recordingJob?.cancel() } } 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..85139ae5f 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 starting. + */ + data object Starting : Recording + /** * The user is actively pressing the button to record video (press-and-hold). */ diff --git a/ui/uistate/postcapture/build.gradle.kts b/ui/uistate/postcapture/build.gradle.kts index f21c4190d..4fda15af5 100644 --- a/ui/uistate/postcapture/build.gradle.kts +++ b/ui/uistate/postcapture/build.gradle.kts @@ -23,7 +23,7 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 23 + minSdk = libs.versions.minSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") 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..03abefb43 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.Starting } } 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..340659801 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.Starting::class.java) assertThat(uiState.isEnabled).isTrue() } } diff --git a/ui/uistateadapter/postcapture/build.gradle.kts b/ui/uistateadapter/postcapture/build.gradle.kts index 632e4f4c3..889e3a51c 100644 --- a/ui/uistateadapter/postcapture/build.gradle.kts +++ b/ui/uistateadapter/postcapture/build.gradle.kts @@ -23,7 +23,7 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - minSdk = 23 + minSdk = libs.versions.minSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro")