Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bdcc2f0
Fix intent extras merging and usage in VideoRecordingDeviceTest
temcguir May 14, 2026
a996449
Round elapsedTimeNanos to nearest second in CaptureUiStateAdapter
temcguir May 14, 2026
385de58
feat: make elapsed time rounding configurable in CaptureUiStateAdapte…
temcguir May 14, 2026
55a1364
test: add unit tests for CaptureUiStateAdapter rounding
temcguir May 14, 2026
d4ae74f
docs: add state conflation guideline to styleguide
temcguir May 15, 2026
25844e9
refactor: apply review comments to simplify rounding and avoid redund…
temcguir May 15, 2026
282d848
refactor: apply remaining review comments
temcguir May 15, 2026
b6fd9fe
refactor: move roundedCameraState definition to match bot suggestion
temcguir May 15, 2026
cf32cfa
docs: add timePrecision parameter to captureUiState KDoc
temcguir May 15, 2026
8bdc11f
docs: add KDocs to modified test helper functions in UiTestUtil
temcguir May 15, 2026
8e5adf6
Merge remote-tracking branch 'origin/main' into temcguir/state_confla…
temcguir May 22, 2026
8135752
Apply spotless formatting
temcguir May 22, 2026
72f6888
Resolve PR 514 comments: skip rounding if NANOSECONDS, add test for p…
temcguir May 22, 2026
69ef5d5
Apply spotless formatting to PR 514
temcguir May 22, 2026
b17cf34
Merge branch 'main' into temcguir/state_conflation
temcguir May 30, 2026
4859361
Refine VideoRecordingState rounding logic and remove intent extras te…
temcguir May 30, 2026
9f54155
Apply spotless formatting
temcguir May 30, 2026
1b581b7
Use roundedCameraState for sessionFirstFrameTimestamp
temcguir May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gemini/styleguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,6 +40,7 @@ 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
Expand All @@ -56,6 +58,7 @@ import kotlinx.coroutines.flow.filterNotNull
* 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.
Expand All @@ -64,7 +67,8 @@ fun captureUiState(
cameraSystem: CameraSystem,
constraintsRepository: ConstraintsRepository,
trackedCaptureUiState: MutableStateFlow<TrackedCaptureUiState>,
externalCaptureMode: ExternalCaptureMode
externalCaptureMode: ExternalCaptureMode,
timePrecision: TimeUnit = TimeUnit.SECONDS
): Flow<CaptureUiState> {
var flashModeUiState: FlashModeUiState? = null
var focusMeteringUiState: FocusMeteringUiState? = null
Expand All @@ -75,6 +79,11 @@ fun captureUiState(
cameraSystem.getCurrentCameraState(),
trackedCaptureUiState
) { cameraAppSettings, systemConstraints, cameraState, trackedUiState ->
val videoRecordingState = cameraState.videoRecordingState
val roundedVideoRecordingState =
roundVideoRecordingState(videoRecordingState, timePrecision)
val roundedCameraState = cameraState.copy(videoRecordingState = roundedVideoRecordingState)

val captureModeUiState = CaptureModeUiState.from(
systemConstraints,
cameraAppSettings,
Expand All @@ -95,20 +104,19 @@ 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,
videoRecordingState = cameraState.videoRecordingState,
videoRecordingState = roundedVideoRecordingState,
flipLensUiState = flipLensUiState,
aspectRatioUiState = aspectRatioUiState,
previewDisplayUiState = PreviewDisplayUiState(
Expand All @@ -129,47 +137,69 @@ fun captureUiState(
trackedUiState.focusedQuickSetting,
externalCaptureMode
),
sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp,
sessionFirstFrameTimestamp = roundedCameraState.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(cameraState),
elapsedTimeUiState = ElapsedTimeUiState.from(roundedCameraState),
captureButtonUiState = CaptureButtonUiState.from(
cameraAppSettings,
cameraState,
roundedCameraState,
trackedUiState.isRecordingLocked
),
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,
focusMeteringUiState = focusMeteringUiState,
imageWellUiState = ImageWellUiState.from(
trackedUiState.recentCapturedMedia,
cameraState.videoRecordingState
roundedVideoRecordingState
),
screenFlashUiState = ScreenFlashUiState.from(trackedUiState)
)
}
}

/**
* Rounds the elapsed time of a [VideoRecordingState] to the given [timePrecision] to reduce UI recomposition frequency.
*/
internal fun roundVideoRecordingState(
videoRecordingState: VideoRecordingState,
timePrecision: TimeUnit
): VideoRecordingState {
if (videoRecordingState !is VideoRecordingState.Active) return videoRecordingState

Comment thread
temcguir marked this conversation as resolved.
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
)
}
}
Comment thread
temcguir marked this conversation as resolved.
Comment thread
temcguir marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.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)
}

@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)
}
}
Loading