From 3c8aab9be177c0d2687ea1b2221d665124a75e15 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 19 Aug 2026 13:16:02 -0300 Subject: [PATCH] Preserve the selected flash mode across camera rebinds. --- .../signal/camera/CameraScreenViewModel.kt | 7 +++++- .../camera/CameraScreenViewModelTest.kt | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt b/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt index 6595d8ba81..cd0b14c13e 100644 --- a/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt +++ b/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt @@ -531,10 +531,15 @@ class CameraScreenViewModel : ViewModel() { .build() .also { it.surfaceProvider = event.surfaceProvider } - // Image capture with 16:9 aspect ratio (optimized for speed) + // Image capture with 16:9 aspect ratio (optimized for speed). + // The flash mode must be seeded from state: this builds a brand new ImageCapture, and a fresh one + // defaults to FLASH_MODE_OFF. Rebinds happen on every camera switch and every re-entry into the + // camera, so without this the HUD would keep showing the user's flash selection while the hardware + // flash silently stopped firing. val imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .setResolutionSelector(resolutionSelector) + .setFlashMode(_state.value.flashMode.cameraxMode) .build() val videoCapture: VideoCapture? = if (event.captureMode == CameraCaptureMode.ImageAndVideoSimultaneous && !FORCE_LIMITED_BINDING) { diff --git a/feature/camera/src/test/java/org/signal/camera/CameraScreenViewModelTest.kt b/feature/camera/src/test/java/org/signal/camera/CameraScreenViewModelTest.kt index c83ee1e67c..3636e5da47 100644 --- a/feature/camera/src/test/java/org/signal/camera/CameraScreenViewModelTest.kt +++ b/feature/camera/src/test/java/org/signal/camera/CameraScreenViewModelTest.kt @@ -11,6 +11,7 @@ import androidx.camera.core.CameraControl import androidx.camera.core.CameraInfo import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture import androidx.camera.core.Preview import androidx.camera.core.ZoomState import androidx.camera.lifecycle.ProcessCameraProvider @@ -135,6 +136,7 @@ class CameraScreenViewModelTest { private fun List.hasVideoCapture() = any { it is VideoCapture<*> } private fun List.hasImageAnalysis() = any { it is ImageAnalysis } + private fun List.imageCapture() = filterIsInstance().firstOrNull() private fun setupZoomState(minZoom: Float, maxZoom: Float) { val mockZoomState: ZoomState = mockk() @@ -340,6 +342,26 @@ class CameraScreenViewModelTest { assertThat(viewModel.state.value.flashMode).isEqualTo(FlashMode.Off) } + @Test + fun `rebinding after the flash mode was set carries that flash mode onto the new ImageCapture`() { + bindCamera() + viewModel.onEvent(CameraScreenEvents.SetFlashMode(FlashMode.On)) + + // A camera switch or a re-entry into the camera rebinds, building a brand new ImageCapture. + val attempts = captureBindingAttempts() + bindCamera() + + assertThat(attempts.last().imageCapture()?.flashMode).isEqualTo(ImageCapture.FLASH_MODE_ON) + } + + @Test + fun `binding with the default flash mode leaves the ImageCapture flash off`() { + val attempts = captureBindingAttempts() + bindCamera() + + assertThat(attempts.last().imageCapture()?.flashMode).isEqualTo(ImageCapture.FLASH_MODE_OFF) + } + // =========================================================================== // Camera switching // ===========================================================================