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
29b087e
Refactor test fakes into dedicated testing modules
temcguir Apr 20, 2026
184da5e
Correct copyright year in new build files
temcguir Apr 20, 2026
cc85dcf
Correct copyright year to 2026 in new build files
temcguir Apr 20, 2026
3a9fc87
Implement fake controllers with unit tests in dedicated testing module
temcguir Apr 21, 2026
08e9084
Address code review comments: add explicit test runners and simplify …
temcguir Apr 21, 2026
b931f60
Standardize build toolchain to jvmToolchain(17) in postcapture modules
temcguir Apr 21, 2026
eceb1d0
Implement high-fidelity camera startup emulation in FakeCameraSystem
temcguir Apr 21, 2026
2fb597e
Address code review comments for FakeCaptureController and FakeSnackB…
temcguir Apr 22, 2026
7f5a834
Merge branch 'temcguir/fake_controllers' into temcguir/fake_camera_sy…
temcguir Apr 22, 2026
5e99e5b
Fix CameraXCameraSystemTest build error after PreviewSurfaceRequest c…
temcguir Apr 22, 2026
9d43e46
Address code review: Make PreviewSurfaceRequest.CameraX a data class
temcguir Apr 22, 2026
03cccdf
Address code review: Make PreviewSurfaceRequest.Viewfinder a data class
temcguir Apr 22, 2026
70f407d
Address code review: Derive FakeCameraSystem surface resolution from …
temcguir Apr 22, 2026
ab983c7
Address code review: Set isCameraRunning to false while waiting for s…
temcguir Apr 22, 2026
f935fa1
Address code review: Extract preview gestures into a reusable Modifier
temcguir Apr 22, 2026
cf572fa
Merge branch 'main' into temcguir/fake_controllers
temcguir Apr 23, 2026
efa6b4d
Merge branch 'main' into temcguir/fake_controllers
temcguir Apr 29, 2026
b681e01
Merge branch 'temcguir/fake_controllers' into temcguir/fake_camera_sy…
temcguir Apr 29, 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 core/camera/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ dependencies {
// CameraX
implementation(libs.camera.core)
implementation(libs.camera.camera2)
implementation(libs.camera.viewfinder.core)
implementation(libs.camera.lifecycle)
implementation(libs.camera.video)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,9 @@ class CameraXCameraSystemTest {
private fun CameraXCameraSystem.providePreviewSurface() {
cameraSystemScope.launch {
getSurfaceRequest().filterNotNull().collect {
it.provideUpdatingSurface()
if (it is PreviewSurfaceRequest.CameraX) {
it.surfaceRequest.provideUpdatingSurface()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ private fun createPreviewUseCase(
}.build()
.apply {
setSurfaceProvider { surfaceRequest ->
surfaceRequests.update { surfaceRequest }
surfaceRequests.update { PreviewSurfaceRequest.CameraX(surfaceRequest) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package com.google.jetpackcamera.core.camera

import android.content.Context
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.jetpackcamera.core.camera.lowlight.LowLightBoostEffectProvider
import com.google.jetpackcamera.core.common.FilePathGenerator
Expand All @@ -41,7 +40,7 @@ internal data class CameraSessionContext(
val focusMeteringEvents: Channel<CameraEvent.FocusMeteringEvent>,
val videoCaptureControlEvents: Channel<VideoCaptureControlEvent>,
val currentCameraState: MutableStateFlow<CameraState>,
val surfaceRequests: MutableStateFlow<SurfaceRequest?>,
val surfaceRequests: MutableStateFlow<PreviewSurfaceRequest?>,
val transientSettings: StateFlow<TransientSessionSettings?>,
val lowLightBoostEffectProvider: LowLightBoostEffectProvider? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ interface CameraSystem {

fun getCurrentCameraState(): StateFlow<CameraState>

fun getSurfaceRequest(): StateFlow<SurfaceRequest?>
/**
* Returns the current [PreviewSurfaceRequest].
*
* This will be null if no surface is currently requested.
*/
fun getSurfaceRequest(): StateFlow<PreviewSurfaceRequest?>

fun getScreenFlashEvents(): ReceiveChannel<ScreenFlashEvent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import androidx.camera.core.CameraXConfig
import androidx.camera.core.DynamicRange as CXDynamicRange
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OutputFileOptions
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.takePicture
import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration
import androidx.camera.lifecycle.ProcessCameraProvider
Expand Down Expand Up @@ -135,9 +134,10 @@ constructor(
private var currentCameraState = MutableStateFlow(CameraState())
override fun getCurrentCameraState(): StateFlow<CameraState> = currentCameraState.asStateFlow()

private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
private val _surfaceRequest = MutableStateFlow<PreviewSurfaceRequest?>(null)

override fun getSurfaceRequest(): StateFlow<SurfaceRequest?> = _surfaceRequest.asStateFlow()
override fun getSurfaceRequest(): StateFlow<PreviewSurfaceRequest?> =
_surfaceRequest.asStateFlow()

private val lowLightBoostAvailabilityChecker: LowLightBoostAvailabilityChecker?
private val lowLightBoostEffectProvider: LowLightBoostEffectProvider?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ internal suspend fun CameraSessionContext.processFocusMeteringEvents(
cameraInfo: CameraInfo,
cameraControl: CameraControl
) {
surfaceRequests.flatMapLatest { surfaceRequest ->
surfaceRequests.flatMapLatest { previewSurfaceRequest ->
val surfaceRequest =
(previewSurfaceRequest as? PreviewSurfaceRequest.CameraX)?.surfaceRequest
surfaceRequest?.let { request ->
Log.d(
TAG,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.core.camera

import android.view.Surface
import androidx.camera.core.SurfaceRequest
import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest
import kotlinx.coroutines.CompletableDeferred

/**
* A sealed interface representing a request for a preview surface, abstracting the specific
* viewfinder implementation required.
*/
sealed interface PreviewSurfaceRequest {
/**
* Wraps a CameraX [SurfaceRequest] for the production CameraXViewfinder.
*
* @property surfaceRequest The CameraX [SurfaceRequest].
*/
data class CameraX(val surfaceRequest: SurfaceRequest) : PreviewSurfaceRequest

/**
* Wraps a [ViewfinderSurfaceRequest] for the standalone Viewfinder composable.
*
* @property surfaceRequest The standalone [ViewfinderSurfaceRequest].
* @property surfaceDeferred A [CompletableDeferred] that will be completed with the [Surface]
* once provided by the UI.
*/
data class Viewfinder(
val surfaceRequest: ViewfinderSurfaceRequest,
val surfaceDeferred: CompletableDeferred<Surface> = CompletableDeferred()
) : PreviewSurfaceRequest
}
4 changes: 2 additions & 2 deletions core/camera/testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ dependencies {
implementation(project(":core:camera"))
implementation(project(":core:model"))
implementation(project(":data:settings"))

implementation(libs.camera.core)
implementation(libs.camera.viewfinder.core)
implementation(libs.kotlinx.coroutines.core)

// Testing
testImplementation(libs.junit)
testImplementation(libs.truth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ package com.google.jetpackcamera.core.camera.testing

import android.annotation.SuppressLint
import android.content.ContentResolver
import android.view.Surface
import androidx.camera.core.ImageCapture
import androidx.camera.core.SurfaceRequest
import androidx.camera.viewfinder.core.ImplementationMode
import androidx.camera.viewfinder.core.ViewfinderSurfaceRequest
import com.google.jetpackcamera.core.camera.CameraState
import com.google.jetpackcamera.core.camera.CameraSystem
import com.google.jetpackcamera.core.camera.OnVideoRecordEvent
import com.google.jetpackcamera.core.camera.PreviewSurfaceRequest
import com.google.jetpackcamera.model.AspectRatio
import com.google.jetpackcamera.model.CameraZoomRatio
import com.google.jetpackcamera.model.CaptureMode
Expand All @@ -38,6 +41,7 @@ import com.google.jetpackcamera.model.StreamConfig
import com.google.jetpackcamera.model.TestPattern
import com.google.jetpackcamera.model.VideoQuality
import com.google.jetpackcamera.settings.model.CameraAppSettings
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -53,12 +57,19 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti
private var initialized = false
private var useCasesBinded = false

/** Whether the preview has started. */
var previewStarted = false

/** Number of pictures taken. */
var numPicturesTaken = 0

/** Whether a recording is in progress. */
var recordingInProgress = false

/** Whether the current recording is paused. */
var isRecordingPaused = false

/** Whether the lens facing is front. */
var isLensFacingFront = false

private var isScreenFlash = true
Expand All @@ -85,9 +96,11 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti

currentSettings
.onCompletion {
_surfaceRequest.value = null
useCasesBinded = false
previewStarted = false
recordingInProgress = false
_currentCameraState.update { CameraState() }
}.collectLatest {
useCasesBinded = true
previewStarted = true
Expand All @@ -96,6 +109,22 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti
isScreenFlash =
isLensFacingFront &&
(it.flashMode == FlashMode.AUTO || it.flashMode == FlashMode.ON)

val aspectRatio = it.aspectRatio
val request = ViewfinderSurfaceRequest(
1080 * aspectRatio.denominator / aspectRatio.numerator,
1080,
ImplementationMode.EXTERNAL
)
Comment thread
temcguir marked this conversation as resolved.
val deferred = CompletableDeferred<Surface>()
_currentCameraState.update { state ->
state.copy(isCameraRunning = false)
}
_surfaceRequest.value = PreviewSurfaceRequest.Viewfinder(request, deferred)
deferred.await()
_currentCameraState.update { state ->
state.copy(isCameraRunning = true)
}
Comment thread
temcguir marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -163,8 +192,9 @@ class FakeCameraSystem(defaultCameraSettings: CameraAppSettings = CameraAppSetti

override fun getCurrentCameraState(): StateFlow<CameraState> = _currentCameraState.asStateFlow()

private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
override fun getSurfaceRequest(): StateFlow<SurfaceRequest?> = _surfaceRequest.asStateFlow()
private val _surfaceRequest = MutableStateFlow<PreviewSurfaceRequest?>(null)
override fun getSurfaceRequest(): StateFlow<PreviewSurfaceRequest?> =
_surfaceRequest.asStateFlow()

override fun getScreenFlashEvents() = screenFlashEvents
override fun getCurrentSettings(): StateFlow<CameraAppSettings?> = currentSettings.asStateFlow()
Expand Down
Loading
Loading