From bdcc2f0d21233e3e8410f5d112cd1edb4f90abb2 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Thu, 14 May 2026 21:35:14 +0000 Subject: [PATCH 01/16] Fix intent extras merging and usage in VideoRecordingDeviceTest Fixed runMainActivityScenarioTest to merge compatMainActivityExtras with provided extras instead of ignoring them when extras are present. Updated VideoRecordingDeviceTest to use runMainActivityScenarioTestForResult instead of runScenarioTestForResult to ensure compatMainActivityExtras are applied. TAG=agy CONV=ede7015c-6915-452c-91bf-1734f3aecca3 --- .../jetpackcamera/VideoRecordingDeviceTest.kt | 14 ++++----- .../google/jetpackcamera/utils/UiTestUtil.kt | 31 +++++++++++++------ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index 3dfc40ffa..37b553c19 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -44,7 +44,7 @@ import com.google.jetpackcamera.utils.longClickForVideoRecording import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedTime import com.google.jetpackcamera.utils.pressAndDragToLockVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest -import com.google.jetpackcamera.utils.runScenarioTestForResult +import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult import com.google.jetpackcamera.utils.tapStartLockedVideoRecording import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag @@ -111,9 +111,9 @@ internal class VideoRecordingDeviceTest { val timeStamp = System.currentTimeMillis() val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4") val result = - runScenarioTestForResult( + runMainActivityScenarioTestForResult( getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), - activityExtras = cacheParam.extras + extras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitForCaptureButton() @@ -129,9 +129,9 @@ internal class VideoRecordingDeviceTest { val timeStamp = System.currentTimeMillis() val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4") val result = - runScenarioTestForResult( + runMainActivityScenarioTestForResult( getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), - activityExtras = cacheParam.extras + extras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitForCaptureButton() @@ -151,9 +151,9 @@ internal class VideoRecordingDeviceTest { fun video_capture_external_illegal_uri() { val uri = Uri.parse("asdfasdf") val result = - runScenarioTestForResult( + runMainActivityScenarioTestForResult( getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), - activityExtras = cacheParam.extras + extras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitForCaptureButton() 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 bf1a9a2f6..da062db77 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -32,6 +32,7 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import androidx.test.services.storage.TestStorage import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 @@ -64,15 +65,24 @@ val isEmulatorWithFakeFrontCamera: Boolean (Build.VERSION.SDK_INT == 28 || Build.VERSION.SDK_INT == 34) val compatMainActivityExtras: Bundle? - get() = if (isEmulatorWithFakeFrontCamera) { - // The GMD API 28 and 34 emulators' PackageInfo reports it has front and back cameras, but - // GMD is only configured for a back camera. This causes CameraX to take a long time - // to initialize. Set the device to use single lens mode to work around this issue. - Bundle().apply { - putString(MainActivity.KEY_DEBUG_SINGLE_LENS_MODE, "back") + get() { + val extras = Bundle() + if (isEmulatorWithFakeFrontCamera) { + // The GMD API 28 and 34 emulators' PackageInfo reports it has front and back cameras, but + // GMD is only configured for a back camera. This causes CameraX to take a long time + // to initialize. Set the device to use single lens mode to work around this issue. + extras.putString(MainActivity.KEY_DEBUG_SINGLE_LENS_MODE, "back") } - } else { - null + + val resolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver + val args = TestStorage(resolver).getInputArgs() + Log.d("UiTestUtil", "TestStorage Args: $args") + val disableAnimations = args["disable_animations"]?.toBoolean() ?: false + if (disableAnimations) { + extras.putBoolean("KEY_DISABLE_ANIMATIONS", true) + } + + return if (extras.size() == 0) null else extras } val debugExtra: Bundle = Bundle().apply { putBoolean("KEY_DEBUG_MODE", true) } @@ -179,7 +189,10 @@ inline fun runMainActivityMediaStoreAutoDeleteScenarioTest( inline fun runMainActivityScenarioTest( extras: Bundle? = null, crossinline block: ActivityScenario.() -> Unit -) = runScenarioTest(extras ?: compatMainActivityExtras, block) +) { + val activityExtras = compatMainActivityExtras?.apply { extras?.let { putAll(it) } } ?: extras + runScenarioTest(activityExtras, block) +} inline fun runScenarioTest( activityExtras: Bundle? = null, From a9964496c4dd31c2ca4ca5cbb88e42f54aa96048 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Thu, 14 May 2026 21:35:20 +0000 Subject: [PATCH 02/16] Round elapsedTimeNanos to nearest second in CaptureUiStateAdapter --- .../capture/compound/CaptureUiStateAdapter.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index 25a964024..7d21cf24f 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -75,6 +75,19 @@ fun captureUiState( cameraSystem.getCurrentCameraState(), trackedCaptureUiState ) { cameraAppSettings, systemConstraints, cameraState, trackedUiState -> + val videoRecordingState = cameraState.videoRecordingState + val roundedVideoRecordingState = when (videoRecordingState) { + is VideoRecordingState.Active -> { + val seconds = videoRecordingState.elapsedTimeNanos / 1_000_000_000L + val roundedNanos = seconds * 1_000_000_000L + when (videoRecordingState) { + is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + } + } + else -> videoRecordingState + } + val captureModeUiState = CaptureModeUiState.from( systemConstraints, cameraAppSettings, @@ -108,7 +121,7 @@ fun captureUiState( CaptureUiState.Ready( externalCaptureMode = externalCaptureMode, - videoRecordingState = cameraState.videoRecordingState, + videoRecordingState = roundedVideoRecordingState, flipLensUiState = flipLensUiState, aspectRatioUiState = aspectRatioUiState, previewDisplayUiState = PreviewDisplayUiState( @@ -140,10 +153,10 @@ fun captureUiState( cameraAppSettings, cameraState ), - elapsedTimeUiState = ElapsedTimeUiState.from(cameraState), + elapsedTimeUiState = ElapsedTimeUiState.from(cameraState.copy(videoRecordingState = roundedVideoRecordingState)), captureButtonUiState = CaptureButtonUiState.from( cameraAppSettings, - cameraState, + cameraState.copy(videoRecordingState = roundedVideoRecordingState), trackedUiState.isRecordingLocked ), zoomUiState = ZoomUiState.from( @@ -167,7 +180,7 @@ fun captureUiState( focusMeteringUiState = focusMeteringUiState, imageWellUiState = ImageWellUiState.from( trackedUiState.recentCapturedMedia, - cameraState.videoRecordingState + roundedVideoRecordingState ), screenFlashUiState = ScreenFlashUiState.from(trackedUiState) ) From 385de585d91b91e5500ecf765701deb0784c2bb1 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Thu, 14 May 2026 23:12:45 +0000 Subject: [PATCH 03/16] feat: make elapsed time rounding configurable in CaptureUiStateAdapter and add tests --- .../capture/compound/CaptureUiStateAdapter.kt | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index 7d21cf24f..71bad514f 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import com.google.jetpackcamera.core.camera.VideoRecordingState /** * Creates a [Flow] of [CaptureUiState] by combining the latest values from various sources. @@ -64,7 +65,8 @@ fun captureUiState( cameraSystem: CameraSystem, constraintsRepository: ConstraintsRepository, trackedCaptureUiState: MutableStateFlow, - externalCaptureMode: ExternalCaptureMode + externalCaptureMode: ExternalCaptureMode, + timePrecision: java.util.concurrent.TimeUnit = java.util.concurrent.TimeUnit.SECONDS ): Flow { var flashModeUiState: FlashModeUiState? = null var focusMeteringUiState: FocusMeteringUiState? = null @@ -76,17 +78,7 @@ fun captureUiState( trackedCaptureUiState ) { cameraAppSettings, systemConstraints, cameraState, trackedUiState -> val videoRecordingState = cameraState.videoRecordingState - val roundedVideoRecordingState = when (videoRecordingState) { - is VideoRecordingState.Active -> { - val seconds = videoRecordingState.elapsedTimeNanos / 1_000_000_000L - val roundedNanos = seconds * 1_000_000_000L - when (videoRecordingState) { - is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) - is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) - } - } - else -> videoRecordingState - } + val roundedVideoRecordingState = roundVideoRecordingState(videoRecordingState, timePrecision) val captureModeUiState = CaptureModeUiState.from( systemConstraints, @@ -186,3 +178,28 @@ fun captureUiState( ) } } + +internal fun roundVideoRecordingState( + videoRecordingState: VideoRecordingState, + timePrecision: java.util.concurrent.TimeUnit +): VideoRecordingState { + return when (videoRecordingState) { + is VideoRecordingState.Active -> { + val nanos = videoRecordingState.elapsedTimeNanos + val roundedNanos = when (timePrecision) { + java.util.concurrent.TimeUnit.NANOSECONDS -> nanos + java.util.concurrent.TimeUnit.MICROSECONDS -> (nanos / 1000L) * 1000L + java.util.concurrent.TimeUnit.MILLISECONDS -> (nanos / 1_000_000L) * 1_000_000L + java.util.concurrent.TimeUnit.SECONDS -> (nanos / 1_000_000_000L) * 1_000_000_000L + java.util.concurrent.TimeUnit.MINUTES -> (nanos / 60_000_000_000L) * 60_000_000_000L + java.util.concurrent.TimeUnit.HOURS -> (nanos / 3_600_000_000_000L) * 3_600_000_000_000L + else -> nanos + } + when (videoRecordingState) { + is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + } + } + else -> videoRecordingState + } +} From 55a136452023a04fa65da7c3fd9f3ff27426083f Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Thu, 14 May 2026 23:12:50 +0000 Subject: [PATCH 04/16] test: add unit tests for CaptureUiStateAdapter rounding --- .../capture/CaptureUiStateAdapterTest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt new file mode 100644 index 000000000..f8763dbe1 --- /dev/null +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt @@ -0,0 +1,34 @@ +package com.google.jetpackcamera.ui.uistateadapter.capture + +import com.google.common.truth.Truth.assertThat +import com.google.jetpackcamera.core.camera.VideoRecordingState +import com.google.jetpackcamera.ui.uistateadapter.capture.compound.roundVideoRecordingState +import java.util.concurrent.TimeUnit +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class CaptureUiStateAdapterTest { + + @Test + fun roundVideoRecordingState_nanoseconds_noRounding() { + val state = VideoRecordingState.Active.Recording(0L, 0.0, 1234567890L) + val rounded = roundVideoRecordingState(state, TimeUnit.NANOSECONDS) + assertThat((rounded as VideoRecordingState.Active).elapsedTimeNanos).isEqualTo(1234567890L) + } + + @Test + fun roundVideoRecordingState_milliseconds_roundsToMillis() { + val state = VideoRecordingState.Active.Recording(0L, 0.0, 1234567890L) + val rounded = roundVideoRecordingState(state, TimeUnit.MILLISECONDS) + assertThat((rounded as VideoRecordingState.Active).elapsedTimeNanos).isEqualTo(1234000000L) + } + + @Test + fun roundVideoRecordingState_seconds_roundsToSeconds() { + val state = VideoRecordingState.Active.Recording(0L, 0.0, 1234567890L) + val rounded = roundVideoRecordingState(state, TimeUnit.SECONDS) + assertThat((rounded as VideoRecordingState.Active).elapsedTimeNanos).isEqualTo(1000000000L) + } +} From d4ae74f715a8ace08ed41404fd95743f96476278 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 15 May 2026 00:15:54 +0000 Subject: [PATCH 05/16] docs: add state conflation guideline to styleguide --- .gemini/styleguide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md index dd76e4e19..3a5b03c4a 100644 --- a/.gemini/styleguide.md +++ b/.gemini/styleguide.md @@ -30,6 +30,7 @@ When reviewing a pull request, focus on the following key areas: * Scan for inefficient operations, especially within Composable functions (e.g., expensive calculations, improper state management leading to excessive recompositions). * Analyze camera configurations and use cases for potential performance bottlenecks. * Ensure coroutines and asynchronous operations are used efficiently. + * **State Conflation in Adapters:** High-frequency stream data (e.g., nanosecond timestamps) should be rounded or conflated at the `UiStateAdapter` level before reaching the UI state, to avoid unnecessary recompositions. [Introduced in PR #514] 4. **Jetpack Compose & CameraX Usage** * Verify that Compose and CameraX APIs are used correctly and effectively. From 25844e901ff8aa980cb109c81f3c483cb62598d5 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 15 May 2026 00:22:42 +0000 Subject: [PATCH 06/16] refactor: apply review comments to simplify rounding and avoid redundant copy --- .../capture/compound/CaptureUiStateAdapter.kt | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index 71bad514f..bc0a17d58 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -110,7 +110,7 @@ fun captureUiState( ) ?: FocusMeteringUiState.from(cameraState) } - + val roundedCameraState = cameraState.copy(videoRecordingState = roundedVideoRecordingState) CaptureUiState.Ready( externalCaptureMode = externalCaptureMode, videoRecordingState = roundedVideoRecordingState, @@ -145,10 +145,10 @@ fun captureUiState( cameraAppSettings, cameraState ), - elapsedTimeUiState = ElapsedTimeUiState.from(cameraState.copy(videoRecordingState = roundedVideoRecordingState)), + elapsedTimeUiState = ElapsedTimeUiState.from(roundedCameraState), captureButtonUiState = CaptureButtonUiState.from( cameraAppSettings, - cameraState.copy(videoRecordingState = roundedVideoRecordingState), + roundedCameraState, trackedUiState.isRecordingLocked ), zoomUiState = ZoomUiState.from( @@ -179,27 +179,20 @@ fun captureUiState( } } +/** + * Rounds the elapsed time of a [VideoRecordingState] to the given [timePrecision] to reduce UI recomposition frequency. + */ internal fun roundVideoRecordingState( videoRecordingState: VideoRecordingState, timePrecision: java.util.concurrent.TimeUnit ): VideoRecordingState { + if (videoRecordingState !is VideoRecordingState.Active) return videoRecordingState + + val stepNanos = timePrecision.toNanos(1) + val roundedNanos = (videoRecordingState.elapsedTimeNanos / stepNanos) * stepNanos + return when (videoRecordingState) { - is VideoRecordingState.Active -> { - val nanos = videoRecordingState.elapsedTimeNanos - val roundedNanos = when (timePrecision) { - java.util.concurrent.TimeUnit.NANOSECONDS -> nanos - java.util.concurrent.TimeUnit.MICROSECONDS -> (nanos / 1000L) * 1000L - java.util.concurrent.TimeUnit.MILLISECONDS -> (nanos / 1_000_000L) * 1_000_000L - java.util.concurrent.TimeUnit.SECONDS -> (nanos / 1_000_000_000L) * 1_000_000_000L - java.util.concurrent.TimeUnit.MINUTES -> (nanos / 60_000_000_000L) * 60_000_000_000L - java.util.concurrent.TimeUnit.HOURS -> (nanos / 3_600_000_000_000L) * 3_600_000_000_000L - else -> nanos - } - when (videoRecordingState) { - is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) - is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) - } - } - else -> videoRecordingState + is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) } } From 282d8482639366f1a7a5ae470a8540c784dccb1a Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 15 May 2026 00:32:03 +0000 Subject: [PATCH 07/16] refactor: apply remaining review comments --- .../java/com/google/jetpackcamera/utils/UiTestUtil.kt | 1 - .../uistateadapter/capture/compound/CaptureUiStateAdapter.kt | 3 ++- 2 files changed, 2 insertions(+), 2 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 da062db77..b4656861b 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -76,7 +76,6 @@ val compatMainActivityExtras: Bundle? val resolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver val args = TestStorage(resolver).getInputArgs() - Log.d("UiTestUtil", "TestStorage Args: $args") val disableAnimations = args["disable_animations"]?.toBoolean() ?: false if (disableAnimations) { extras.putBoolean("KEY_DISABLE_ANIMATIONS", true) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index bc0a17d58..ee2a467b2 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import com.google.jetpackcamera.core.camera.VideoRecordingState +import java.util.concurrent.TimeUnit /** * Creates a [Flow] of [CaptureUiState] by combining the latest values from various sources. @@ -184,7 +185,7 @@ fun captureUiState( */ internal fun roundVideoRecordingState( videoRecordingState: VideoRecordingState, - timePrecision: java.util.concurrent.TimeUnit + timePrecision: TimeUnit ): VideoRecordingState { if (videoRecordingState !is VideoRecordingState.Active) return videoRecordingState From b6fd9fe618ad26f6c2865e847f3115ac7ac463d0 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 15 May 2026 00:44:01 +0000 Subject: [PATCH 08/16] refactor: move roundedCameraState definition to match bot suggestion --- .../uistateadapter/capture/compound/CaptureUiStateAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index ee2a467b2..5d9db4cf3 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -67,7 +67,7 @@ fun captureUiState( constraintsRepository: ConstraintsRepository, trackedCaptureUiState: MutableStateFlow, externalCaptureMode: ExternalCaptureMode, - timePrecision: java.util.concurrent.TimeUnit = java.util.concurrent.TimeUnit.SECONDS + timePrecision: TimeUnit = TimeUnit.SECONDS ): Flow { var flashModeUiState: FlashModeUiState? = null var focusMeteringUiState: FocusMeteringUiState? = null @@ -80,6 +80,7 @@ fun captureUiState( ) { cameraAppSettings, systemConstraints, cameraState, trackedUiState -> val videoRecordingState = cameraState.videoRecordingState val roundedVideoRecordingState = roundVideoRecordingState(videoRecordingState, timePrecision) + val roundedCameraState = cameraState.copy(videoRecordingState = roundedVideoRecordingState) val captureModeUiState = CaptureModeUiState.from( systemConstraints, @@ -111,7 +112,6 @@ fun captureUiState( ) ?: FocusMeteringUiState.from(cameraState) } - val roundedCameraState = cameraState.copy(videoRecordingState = roundedVideoRecordingState) CaptureUiState.Ready( externalCaptureMode = externalCaptureMode, videoRecordingState = roundedVideoRecordingState, From cf32cfa0dda3a8104b6d18c3fd58c917b10b3606 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 15 May 2026 00:49:37 +0000 Subject: [PATCH 09/16] docs: add timePrecision parameter to captureUiState KDoc --- .../ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index 5d9db4cf3..ea2618f24 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -58,6 +58,7 @@ import java.util.concurrent.TimeUnit * needs to be tracked across recompositions (e.g., whether quick settings is open). * @param externalCaptureMode The [ExternalCaptureMode] influencing UI behavior based on how the * camera is launched (e.g., from an external intent). + * @param timePrecision The precision to use for rounding the elapsed time of video recording. * * @return A [Flow] that emits a new [CaptureUiState] whenever any of its underlying * data sources change. From 8bdc11f3ed13fca0b83664a91d7debb079762eca Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 15 May 2026 00:51:25 +0000 Subject: [PATCH 10/16] docs: add KDocs to modified test helper functions in UiTestUtil --- .../com/google/jetpackcamera/utils/UiTestUtil.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 b4656861b..14fa7b5c0 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -185,6 +185,13 @@ inline fun runMainActivityMediaStoreAutoDeleteScenarioTest( } } +/** + * Runs a test scenario for [MainActivity] with optional extras, properly merging them with + * compatibility extras required for emulators. + * + * @param extras Optional bundle of extras to pass to the activity. + * @param block The test block to execute within the scenario. + */ inline fun runMainActivityScenarioTest( extras: Bundle? = null, crossinline block: ActivityScenario.() -> Unit @@ -217,6 +224,15 @@ inline fun runScenarioTest( } } +/** + * Runs a test scenario for [MainActivity] expecting a result, properly merging optional extras + * with compatibility extras required for emulators. + * + * @param intent The intent to launch the activity with. + * @param extras Optional bundle of extras to merge with the intent. + * @param block The test block to execute within the scenario. + * @return The [Instrumentation.ActivityResult] containing result code and data. + */ inline fun runMainActivityScenarioTestForResult( intent: Intent, extras: Bundle? = null, From 8135752f111546e99e92867dd110ed959f91b10f Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 22 May 2026 07:14:30 +0000 Subject: [PATCH 11/16] Apply spotless formatting --- .../capture/compound/CaptureUiStateAdapter.kt | 15 ++++++++++----- .../capture/CaptureUiStateAdapterTest.kt | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index ea2618f24..e2e0b0ac2 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -16,6 +16,7 @@ package com.google.jetpackcamera.ui.uistateadapter.capture.compound import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.core.camera.VideoRecordingState import com.google.jetpackcamera.model.ExternalCaptureMode import com.google.jetpackcamera.settings.ConstraintsRepository import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -39,12 +40,11 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.PreviewDisplayUiStat import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState import com.google.jetpackcamera.ui.uistateadapter.capture.from import com.google.jetpackcamera.ui.uistateadapter.capture.updateFrom +import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull -import com.google.jetpackcamera.core.camera.VideoRecordingState -import java.util.concurrent.TimeUnit /** * Creates a [Flow] of [CaptureUiState] by combining the latest values from various sources. @@ -80,7 +80,8 @@ fun captureUiState( trackedCaptureUiState ) { cameraAppSettings, systemConstraints, cameraState, trackedUiState -> val videoRecordingState = cameraState.videoRecordingState - val roundedVideoRecordingState = roundVideoRecordingState(videoRecordingState, timePrecision) + val roundedVideoRecordingState = + roundVideoRecordingState(videoRecordingState, timePrecision) val roundedCameraState = cameraState.copy(videoRecordingState = roundedVideoRecordingState) val captureModeUiState = CaptureModeUiState.from( @@ -194,7 +195,11 @@ internal fun roundVideoRecordingState( val roundedNanos = (videoRecordingState.elapsedTimeNanos / stepNanos) * stepNanos return when (videoRecordingState) { - is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) - is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + is VideoRecordingState.Active.Recording -> videoRecordingState.copy( + elapsedTimeNanos = roundedNanos + ) + is VideoRecordingState.Active.Paused -> videoRecordingState.copy( + elapsedTimeNanos = roundedNanos + ) } } diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt index f8763dbe1..c9e4a189a 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt @@ -1,3 +1,18 @@ +/* + * 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 From 72f6888cb119bb1e960bf85d5454dd4d7ef707e9 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 22 May 2026 16:52:28 +0000 Subject: [PATCH 12/16] Resolve PR 514 comments: skip rounding if NANOSECONDS, add test for paused state, move animation disabling to PR 519 --- .../java/com/google/jetpackcamera/utils/UiTestUtil.kt | 7 +------ .../capture/compound/CaptureUiStateAdapter.kt | 1 + .../uistateadapter/capture/CaptureUiStateAdapterTest.kt | 8 ++++++++ 3 files changed, 10 insertions(+), 6 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 14fa7b5c0..8ad13f437 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -74,12 +74,7 @@ val compatMainActivityExtras: Bundle? extras.putString(MainActivity.KEY_DEBUG_SINGLE_LENS_MODE, "back") } - val resolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver - val args = TestStorage(resolver).getInputArgs() - val disableAnimations = args["disable_animations"]?.toBoolean() ?: false - if (disableAnimations) { - extras.putBoolean("KEY_DISABLE_ANIMATIONS", true) - } + return if (extras.size() == 0) null else extras } diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index e2e0b0ac2..d2d204300 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -190,6 +190,7 @@ internal fun roundVideoRecordingState( timePrecision: TimeUnit ): VideoRecordingState { if (videoRecordingState !is VideoRecordingState.Active) return videoRecordingState + if (timePrecision == TimeUnit.NANOSECONDS) return videoRecordingState val stepNanos = timePrecision.toNanos(1) val roundedNanos = (videoRecordingState.elapsedTimeNanos / stepNanos) * stepNanos diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt index c9e4a189a..df28f5ab2 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt @@ -46,4 +46,12 @@ class CaptureUiStateAdapterTest { val rounded = roundVideoRecordingState(state, TimeUnit.SECONDS) assertThat((rounded as VideoRecordingState.Active).elapsedTimeNanos).isEqualTo(1000000000L) } + + @Test + fun roundVideoRecordingState_pausedState_roundsToSeconds() { + val state = VideoRecordingState.Active.Paused(0L, 0.0, 1234567890L) + val rounded = roundVideoRecordingState(state, TimeUnit.SECONDS) + assertThat((rounded as VideoRecordingState.Active).elapsedTimeNanos).isEqualTo(1000000000L) + assertThat(rounded).isInstanceOf(VideoRecordingState.Active.Paused::class.java) + } } From 69ef5d552f57356f6578a82953b6925dfae350f5 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Fri, 22 May 2026 17:19:59 +0000 Subject: [PATCH 13/16] Apply spotless formatting to PR 514 --- .../java/com/google/jetpackcamera/utils/UiTestUtil.kt | 3 --- 1 file changed, 3 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 8ad13f437..0fbda940e 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -32,7 +32,6 @@ import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import androidx.test.services.storage.TestStorage import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 @@ -74,8 +73,6 @@ val compatMainActivityExtras: Bundle? extras.putString(MainActivity.KEY_DEBUG_SINGLE_LENS_MODE, "back") } - - return if (extras.size() == 0) null else extras } From 485936157fe323e0c9a1362c9a20890ac0703a3d Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Sat, 30 May 2026 06:52:18 +0000 Subject: [PATCH 14/16] Refine VideoRecordingState rounding logic and remove intent extras test modifications --- .../jetpackcamera/VideoRecordingDeviceTest.kt | 14 +++---- .../google/jetpackcamera/utils/UiTestUtil.kt | 38 +++++-------------- .../capture/compound/CaptureUiStateAdapter.kt | 34 +++++++---------- .../capture/CaptureUiStateAdapterTest.kt | 1 - 4 files changed, 30 insertions(+), 57 deletions(-) diff --git a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt index 37b553c19..3dfc40ffa 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/VideoRecordingDeviceTest.kt @@ -44,7 +44,7 @@ import com.google.jetpackcamera.utils.longClickForVideoRecording import com.google.jetpackcamera.utils.longClickForVideoRecordingCheckingElapsedTime import com.google.jetpackcamera.utils.pressAndDragToLockVideoRecording import com.google.jetpackcamera.utils.runMainActivityMediaStoreAutoDeleteScenarioTest -import com.google.jetpackcamera.utils.runMainActivityScenarioTestForResult +import com.google.jetpackcamera.utils.runScenarioTestForResult import com.google.jetpackcamera.utils.tapStartLockedVideoRecording import com.google.jetpackcamera.utils.waitForCaptureButton import com.google.jetpackcamera.utils.waitForNodeWithTag @@ -111,9 +111,9 @@ internal class VideoRecordingDeviceTest { val timeStamp = System.currentTimeMillis() val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4") val result = - runMainActivityScenarioTestForResult( + runScenarioTestForResult( getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), - extras = cacheParam.extras + activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitForCaptureButton() @@ -129,9 +129,9 @@ internal class VideoRecordingDeviceTest { val timeStamp = System.currentTimeMillis() val uri = getTestUri(MOVIES_DIR_PATH, timeStamp, "mp4") val result = - runMainActivityScenarioTestForResult( + runScenarioTestForResult( getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), - extras = cacheParam.extras + activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitForCaptureButton() @@ -151,9 +151,9 @@ internal class VideoRecordingDeviceTest { fun video_capture_external_illegal_uri() { val uri = Uri.parse("asdfasdf") val result = - runMainActivityScenarioTestForResult( + runScenarioTestForResult( getSingleImageCaptureIntent(uri, MediaStore.ACTION_VIDEO_CAPTURE), - extras = cacheParam.extras + activityExtras = cacheParam.extras ) { // Wait for the capture button to be displayed composeTestRule.waitForCaptureButton() 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 0fbda940e..bf1a9a2f6 100644 --- a/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt +++ b/app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt @@ -64,16 +64,15 @@ val isEmulatorWithFakeFrontCamera: Boolean (Build.VERSION.SDK_INT == 28 || Build.VERSION.SDK_INT == 34) val compatMainActivityExtras: Bundle? - get() { - val extras = Bundle() - if (isEmulatorWithFakeFrontCamera) { - // The GMD API 28 and 34 emulators' PackageInfo reports it has front and back cameras, but - // GMD is only configured for a back camera. This causes CameraX to take a long time - // to initialize. Set the device to use single lens mode to work around this issue. - extras.putString(MainActivity.KEY_DEBUG_SINGLE_LENS_MODE, "back") + get() = if (isEmulatorWithFakeFrontCamera) { + // The GMD API 28 and 34 emulators' PackageInfo reports it has front and back cameras, but + // GMD is only configured for a back camera. This causes CameraX to take a long time + // to initialize. Set the device to use single lens mode to work around this issue. + Bundle().apply { + putString(MainActivity.KEY_DEBUG_SINGLE_LENS_MODE, "back") } - - return if (extras.size() == 0) null else extras + } else { + null } val debugExtra: Bundle = Bundle().apply { putBoolean("KEY_DEBUG_MODE", true) } @@ -177,20 +176,10 @@ inline fun runMainActivityMediaStoreAutoDeleteScenarioTest( } } -/** - * Runs a test scenario for [MainActivity] with optional extras, properly merging them with - * compatibility extras required for emulators. - * - * @param extras Optional bundle of extras to pass to the activity. - * @param block The test block to execute within the scenario. - */ inline fun runMainActivityScenarioTest( extras: Bundle? = null, crossinline block: ActivityScenario.() -> Unit -) { - val activityExtras = compatMainActivityExtras?.apply { extras?.let { putAll(it) } } ?: extras - runScenarioTest(activityExtras, block) -} +) = runScenarioTest(extras ?: compatMainActivityExtras, block) inline fun runScenarioTest( activityExtras: Bundle? = null, @@ -216,15 +205,6 @@ inline fun runScenarioTest( } } -/** - * Runs a test scenario for [MainActivity] expecting a result, properly merging optional extras - * with compatibility extras required for emulators. - * - * @param intent The intent to launch the activity with. - * @param extras Optional bundle of extras to merge with the intent. - * @param block The test block to execute within the scenario. - * @return The [Instrumentation.ActivityResult] containing result code and data. - */ inline fun runMainActivityScenarioTestForResult( intent: Intent, extras: Bundle? = null, diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index d2d204300..1b6e714f0 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera.ui.uistateadapter.capture.compound import com.google.jetpackcamera.core.camera.CameraSystem -import com.google.jetpackcamera.core.camera.VideoRecordingState import com.google.jetpackcamera.model.ExternalCaptureMode import com.google.jetpackcamera.settings.ConstraintsRepository import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -40,11 +39,12 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.PreviewDisplayUiStat import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState import com.google.jetpackcamera.ui.uistateadapter.capture.from import com.google.jetpackcamera.ui.uistateadapter.capture.updateFrom -import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import com.google.jetpackcamera.core.camera.VideoRecordingState +import java.util.concurrent.TimeUnit /** * Creates a [Flow] of [CaptureUiState] by combining the latest values from various sources. @@ -80,8 +80,7 @@ fun captureUiState( trackedCaptureUiState ) { cameraAppSettings, systemConstraints, cameraState, trackedUiState -> val videoRecordingState = cameraState.videoRecordingState - val roundedVideoRecordingState = - roundVideoRecordingState(videoRecordingState, timePrecision) + val roundedVideoRecordingState = roundVideoRecordingState(videoRecordingState, timePrecision) val roundedCameraState = cameraState.copy(videoRecordingState = roundedVideoRecordingState) val captureModeUiState = CaptureModeUiState.from( @@ -104,15 +103,15 @@ fun captureUiState( it?.updateFrom( cameraAppSettings = cameraAppSettings, systemConstraints = systemConstraints, - cameraState = cameraState + cameraState = roundedCameraState ) ?: FlashModeUiState.from(cameraAppSettings, systemConstraints) } focusMeteringUiState = focusMeteringUiState.let { it?.updateFrom( - cameraState = cameraState + cameraState = roundedCameraState ) - ?: FocusMeteringUiState.from(cameraState) + ?: FocusMeteringUiState.from(roundedCameraState) } CaptureUiState.Ready( externalCaptureMode = externalCaptureMode, @@ -140,13 +139,13 @@ fun captureUiState( sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp, stabilizationUiState = StabilizationUiState.from( cameraAppSettings, - cameraState + roundedCameraState ), flashModeUiState = flashModeUiState, - videoQuality = cameraState.videoQualityInfo.quality, + videoQuality = roundedCameraState.videoQualityInfo.quality, audioUiState = AudioUiState.from( cameraAppSettings, - cameraState + roundedCameraState ), elapsedTimeUiState = ElapsedTimeUiState.from(roundedCameraState), captureButtonUiState = CaptureButtonUiState.from( @@ -157,18 +156,18 @@ fun captureUiState( zoomUiState = ZoomUiState.from( systemConstraints, cameraAppSettings.cameraLensFacing, - cameraState + roundedCameraState ), zoomControlUiState = ZoomControlUiState.from( trackedUiState.zoomAnimationTarget, systemConstraints, cameraAppSettings, - cameraState + roundedCameraState ), captureModeToggleUiState = CaptureModeToggleUiState.from( systemConstraints, cameraAppSettings, - cameraState, + roundedCameraState, externalCaptureMode ), hdrUiState = hdrUiState, @@ -190,17 +189,12 @@ internal fun roundVideoRecordingState( timePrecision: TimeUnit ): VideoRecordingState { if (videoRecordingState !is VideoRecordingState.Active) return videoRecordingState - if (timePrecision == TimeUnit.NANOSECONDS) return videoRecordingState val stepNanos = timePrecision.toNanos(1) val roundedNanos = (videoRecordingState.elapsedTimeNanos / stepNanos) * stepNanos return when (videoRecordingState) { - is VideoRecordingState.Active.Recording -> videoRecordingState.copy( - elapsedTimeNanos = roundedNanos - ) - is VideoRecordingState.Active.Paused -> videoRecordingState.copy( - elapsedTimeNanos = roundedNanos - ) + is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) } } diff --git a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt index df28f5ab2..04a2bcf59 100644 --- a/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt +++ b/ui/uistateadapter/capture/src/test/java/com/google/jetpackcamera/ui/uistateadapter/capture/CaptureUiStateAdapterTest.kt @@ -52,6 +52,5 @@ class CaptureUiStateAdapterTest { val state = VideoRecordingState.Active.Paused(0L, 0.0, 1234567890L) val rounded = roundVideoRecordingState(state, TimeUnit.SECONDS) assertThat((rounded as VideoRecordingState.Active).elapsedTimeNanos).isEqualTo(1000000000L) - assertThat(rounded).isInstanceOf(VideoRecordingState.Active.Paused::class.java) } } From 9f54155ec317063bc47cdebc640a4ab4eea34d4b Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Sat, 30 May 2026 07:13:19 +0000 Subject: [PATCH 15/16] Apply spotless formatting --- .../capture/compound/CaptureUiStateAdapter.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index 1b6e714f0..1bdd81446 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -16,6 +16,7 @@ package com.google.jetpackcamera.ui.uistateadapter.capture.compound import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.core.camera.VideoRecordingState import com.google.jetpackcamera.model.ExternalCaptureMode import com.google.jetpackcamera.settings.ConstraintsRepository import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -39,12 +40,11 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.PreviewDisplayUiStat import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState import com.google.jetpackcamera.ui.uistateadapter.capture.from import com.google.jetpackcamera.ui.uistateadapter.capture.updateFrom +import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull -import com.google.jetpackcamera.core.camera.VideoRecordingState -import java.util.concurrent.TimeUnit /** * Creates a [Flow] of [CaptureUiState] by combining the latest values from various sources. @@ -80,7 +80,8 @@ fun captureUiState( trackedCaptureUiState ) { cameraAppSettings, systemConstraints, cameraState, trackedUiState -> val videoRecordingState = cameraState.videoRecordingState - val roundedVideoRecordingState = roundVideoRecordingState(videoRecordingState, timePrecision) + val roundedVideoRecordingState = + roundVideoRecordingState(videoRecordingState, timePrecision) val roundedCameraState = cameraState.copy(videoRecordingState = roundedVideoRecordingState) val captureModeUiState = CaptureModeUiState.from( @@ -194,7 +195,11 @@ internal fun roundVideoRecordingState( val roundedNanos = (videoRecordingState.elapsedTimeNanos / stepNanos) * stepNanos return when (videoRecordingState) { - is VideoRecordingState.Active.Recording -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) - is VideoRecordingState.Active.Paused -> videoRecordingState.copy(elapsedTimeNanos = roundedNanos) + is VideoRecordingState.Active.Recording -> videoRecordingState.copy( + elapsedTimeNanos = roundedNanos + ) + is VideoRecordingState.Active.Paused -> videoRecordingState.copy( + elapsedTimeNanos = roundedNanos + ) } } From 1b581b783c09fb36431b4cf6170ba82714a29c17 Mon Sep 17 00:00:00 2001 From: Trevor McGuire Date: Sat, 30 May 2026 07:21:35 +0000 Subject: [PATCH 16/16] Use roundedCameraState for sessionFirstFrameTimestamp --- .../ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt index 1bdd81446..116600839 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/compound/CaptureUiStateAdapter.kt @@ -137,7 +137,7 @@ fun captureUiState( trackedUiState.focusedQuickSetting, externalCaptureMode ), - sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp, + sessionFirstFrameTimestamp = roundedCameraState.sessionFirstFrameTimestamp, stabilizationUiState = StabilizationUiState.from( cameraAppSettings, roundedCameraState