From 5b223fe08b3f37672634a888b99579df5e950a44 Mon Sep 17 00:00:00 2001 From: David Jia Date: Thu, 19 Feb 2026 17:52:18 -0800 Subject: [PATCH 1/6] init --- .../feature/preview/PreviewScreen.kt | 59 ++-- .../feature/preview/PreviewViewModel.kt | 305 ++++-------------- .../feature/preview/PreviewViewModelTest.kt | 8 +- .../ui/components/capture/CaptureCallbacks.kt | 165 ++++++++++ .../components/capture/SnackBarCallbacks.kt | 117 +++++++ .../capture/debug/DebugCallbacks.kt | 71 ++++ .../quicksettings/QuickSettingsCallbacks.kt | 143 ++++++++ 7 files changed, 599 insertions(+), 269 deletions(-) create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index ef6940968..dfb21e3f1 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -198,7 +198,9 @@ fun PreviewScreen( val context = LocalContext.current LaunchedEffect(Unit) { - debouncedOrientationFlow(context).collect(viewModel::setDisplayRotation) + debouncedOrientationFlow(context).collect( + viewModel.captureCallbacks.setDisplayRotation + ) } val scope = rememberCoroutineScope() val zoomState = remember { @@ -213,8 +215,8 @@ fun PreviewScreen( ) ?.initialZoomRatio ?: 1f, - onAnimateStateChanged = viewModel::setZoomAnimationState, - onChangeZoomLevel = viewModel::changeZoomRatio, + onAnimateStateChanged = viewModel.captureCallbacks.setZoomAnimationState, + onChangeZoomLevel = viewModel.captureCallbacks.changeZoomRatio, zoomRange = (currentUiState.zoomUiState as? ZoomUiState.Enabled) ?.primaryZoomRange ?: Range(1f, 1f) @@ -251,8 +253,8 @@ fun PreviewScreen( val oldZoomRatios = it.zoomRatios val oldAudioEnabled = it.isAudioEnabled Log.d(TAG, "reset pre recording settings") - viewModel.setAudioEnabled(oldAudioEnabled) - viewModel.setLensFacing(oldPrimaryLensFacing) + viewModel.captureCallbacks.setAudioEnabled(oldAudioEnabled) + viewModel.quickSettingsCallbacks.setLensFacing(oldPrimaryLensFacing) zoomState.apply { absoluteZoom( targetZoomLevel = oldZoomRatios[oldPrimaryLensFacing] ?: 1f, @@ -279,11 +281,11 @@ fun PreviewScreen( screenFlashUiState = screenFlashUiState, surfaceRequest = surfaceRequest, onNavigateToSettings = onNavigateToSettings, - onClearUiScreenBrightness = viewModel::setClearUiScreenBrightness, - onSetLensFacing = viewModel::setLensFacing, - onTapToFocus = viewModel::tapToFocus, - onSetTestPattern = viewModel::setTestPattern, - onSetImageWell = viewModel::imageWellToRepository, + onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness, + onSetLensFacing = viewModel.quickSettingsCallbacks.setLensFacing, + onTapToFocus = viewModel.captureCallbacks.tapToFocus, + onSetTestPattern = viewModel.debugCallbacks.setTestPattern, + onSetImageWell = viewModel.captureCallbacks.imageWellToRepository, onAbsoluteZoom = { zoomRatio: Float, lensToZoom: LensToZoom -> scope.launch { zoomState.absoluteZoom( @@ -317,26 +319,29 @@ fun PreviewScreen( } }, - onSetCaptureMode = viewModel::setCaptureMode, - onChangeFlash = viewModel::setFlash, - onChangeAspectRatio = viewModel::setAspectRatio, - onSetStreamConfig = viewModel::setStreamConfig, - onChangeDynamicRange = viewModel::setDynamicRange, - onChangeConcurrentCameraMode = viewModel::setConcurrentCameraMode, - onChangeImageFormat = viewModel::setImageFormat, - onDisabledCaptureMode = viewModel::enqueueDisabledHdrToggleSnackBar, - onToggleQuickSettings = viewModel::toggleQuickSettings, - onSetFocusedSetting = viewModel::setFocusedSetting, - onToggleDebugOverlay = viewModel::toggleDebugOverlay, - onToggleDebugHidingComponents = viewModel::toggleDebugHidingComponents, - onSetPause = viewModel::setPaused, - onSetAudioEnabled = viewModel::setAudioEnabled, + onSetCaptureMode = viewModel.quickSettingsCallbacks.setCaptureMode, + onChangeFlash = viewModel.quickSettingsCallbacks.setFlash, + onChangeAspectRatio = viewModel.quickSettingsCallbacks.setAspectRatio, + onSetStreamConfig = viewModel.quickSettingsCallbacks.setStreamConfig, + onChangeDynamicRange = viewModel.quickSettingsCallbacks.setDynamicRange, + onChangeConcurrentCameraMode = + viewModel.quickSettingsCallbacks.setConcurrentCameraMode, + onChangeImageFormat = viewModel.quickSettingsCallbacks.setImageFormat, + onDisabledCaptureMode = + viewModel.snackBarCallbacks.enqueueDisabledHdrToggleSnackBar, + onToggleQuickSettings = viewModel.quickSettingsCallbacks.toggleQuickSettings, + onSetFocusedSetting = viewModel.quickSettingsCallbacks.setFocusedSetting, + onToggleDebugOverlay = viewModel.debugCallbacks.toggleDebugOverlay, + onToggleDebugHidingComponents = + viewModel.debugCallbacks.toggleDebugHidingComponents, + onSetPause = viewModel.captureCallbacks.setPaused, + onSetAudioEnabled = viewModel.captureCallbacks.setAudioEnabled, onCaptureImage = viewModel::captureImage, onStartVideoRecording = viewModel::startVideoRecording, onStopVideoRecording = viewModel::stopVideoRecording, - onLockVideoRecording = viewModel::setLockedRecording, + onLockVideoRecording = viewModel.captureCallbacks.setLockedRecording, onRequestWindowColorMode = onRequestWindowColorMode, - onSnackBarResult = viewModel::onSnackBarResult, + onSnackBarResult = viewModel.snackBarCallbacks.onSnackBarResult, onNavigatePostCapture = onNavigateToPostCapture, debugUiState = debugUiState, snackBarUiState = snackBarUiState @@ -349,7 +354,7 @@ fun PreviewScreen( if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || readStoragePermission.status.isGranted ) { - viewModel.updateLastCapturedMedia() + viewModel.captureCallbacks.updateLastCapturedMedia() } } } diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index d3c46b453..dc7978ee4 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -36,25 +36,14 @@ import com.google.jetpackcamera.feature.preview.navigation.getCaptureUris import com.google.jetpackcamera.feature.preview.navigation.getDebugSettings import com.google.jetpackcamera.feature.preview.navigation.getExternalCaptureMode import com.google.jetpackcamera.feature.preview.navigation.getRequestedSaveMode -import com.google.jetpackcamera.model.AspectRatio -import com.google.jetpackcamera.model.CameraZoomRatio import com.google.jetpackcamera.model.CaptureEvent -import com.google.jetpackcamera.model.CaptureMode -import com.google.jetpackcamera.model.ConcurrentCameraMode import com.google.jetpackcamera.model.DebugSettings -import com.google.jetpackcamera.model.DeviceRotation -import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.ExternalCaptureMode -import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageCaptureEvent -import com.google.jetpackcamera.model.ImageOutputFormat import com.google.jetpackcamera.model.IntProgress -import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.LowLightBoostState import com.google.jetpackcamera.model.SaveLocation import com.google.jetpackcamera.model.SaveMode -import com.google.jetpackcamera.model.StreamConfig -import com.google.jetpackcamera.model.TestPattern import com.google.jetpackcamera.model.VideoCaptureEvent import com.google.jetpackcamera.settings.ConstraintsRepository import com.google.jetpackcamera.settings.SettingsRepository @@ -69,18 +58,20 @@ import com.google.jetpackcamera.ui.components.capture.ScreenFlash import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.ui.uistate.DisableRationale +import com.google.jetpackcamera.ui.components.capture.addSnackBarData +import com.google.jetpackcamera.ui.components.capture.debug.getDebugCallbacks +import com.google.jetpackcamera.ui.components.capture.getCaptureCallbacks +import com.google.jetpackcamera.ui.components.capture.getSnackBarCallbacks +import com.google.jetpackcamera.ui.components.capture.postCurrentMediaToMediaRepository +import com.google.jetpackcamera.ui.components.capture.quicksettings.getQuickSettingsCallbacks import com.google.jetpackcamera.ui.uistate.capture.DebugUiState -import com.google.jetpackcamera.ui.uistate.capture.ImageWellUiState import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState import com.google.jetpackcamera.ui.uistate.capture.SnackbarData import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting import com.google.jetpackcamera.ui.uistateadapter.capture.compound.captureUiState import com.google.jetpackcamera.ui.uistateadapter.capture.debugUiState import dagger.hilt.android.lifecycle.HiltViewModel -import java.util.LinkedList import javax.inject.Inject import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineStart @@ -180,6 +171,32 @@ class PreviewViewModel @Inject constructor( initialValue = DebugUiState.Disabled ) + val quickSettingsCallbacks = getQuickSettingsCallbacks( + trackedCaptureUiState = trackedCaptureUiState, + viewModelScope = viewModelScope, + cameraSystem = cameraSystem, + externalCaptureMode = externalCaptureMode + ) + + val debugCallbacks = getDebugCallbacks( + trackedCaptureUiState = trackedCaptureUiState, + cameraSystem = cameraSystem + ) + + val snackBarCallbacks = getSnackBarCallbacks( + incrementSnackBarCount = { snackBarCount.incrementAndGet() }, + viewModelScope = viewModelScope, + snackBarUiState = _snackBarUiState + ) + + val captureCallbacks = getCaptureCallbacks( + viewModelScope = viewModelScope, + cameraSystem = cameraSystem, + trackedCaptureUiState = trackedCaptureUiState, + mediaRepository = mediaRepository, + captureUiState = captureUiState + ) + init { viewModelScope.launch { launch { @@ -202,6 +219,8 @@ class PreviewViewModel @Inject constructor( val cookieInt = snackBarCount.incrementAndGet() Log.d(TAG, "LowLightBoostState changed to Error #$cookieInt") addSnackBarData( + viewModelScope, + _snackBarUiState, SnackbarData( cookie = "LowLightBoost-$cookieInt", stringResource = R.string.low_light_boost_error_toast_message, @@ -214,34 +233,6 @@ class PreviewViewModel @Inject constructor( } } } - fun toggleDebugHidingComponents() { - trackedCaptureUiState.update { old -> - old.copy(debugHidingComponents = !old.debugHidingComponents) - } - } - - /** - * Sets the media from the image well to the [MediaRepository]. - */ - fun imageWellToRepository() { - (captureUiState.value as? CaptureUiState.Ready) - ?.let { it.imageWellUiState as? ImageWellUiState.LastCapture } - ?.let { postCurrentMediaToMediaRepository(it.mediaDescriptor) } - } - - private fun postCurrentMediaToMediaRepository(mediaDescriptor: MediaDescriptor) { - viewModelScope.launch { - mediaRepository.setCurrentMedia(mediaDescriptor) - } - } - - fun updateLastCapturedMedia() { - viewModelScope.launch { - trackedCaptureUiState.update { old -> - old.copy(recentCapturedMedia = mediaRepository.getLastCapturedMedia()) - } - } - } fun startCamera() { Log.d(TAG, "startCamera") @@ -280,78 +271,6 @@ class PreviewViewModel @Inject constructor( } } - fun setFlash(flashMode: FlashMode) { - viewModelScope.launch { - // apply to cameraSystem - cameraSystem.setFlashMode(flashMode) - } - } - - fun setAspectRatio(aspectRatio: AspectRatio) { - viewModelScope.launch { - cameraSystem.setAspectRatio(aspectRatio) - } - } - - fun setStreamConfig(streamConfig: StreamConfig) { - viewModelScope.launch { - cameraSystem.setStreamConfig(streamConfig) - } - } - - /** Sets the camera to a designated lens facing */ - fun setLensFacing(newLensFacing: LensFacing) { - viewModelScope.launch { - // apply to cameraSystem - cameraSystem.setLensFacing(newLensFacing) - } - } - - fun setAudioEnabled(shouldEnableAudio: Boolean) { - viewModelScope.launch { - cameraSystem.setAudioEnabled(shouldEnableAudio) - } - - Log.d( - TAG, - "Toggle Audio: $shouldEnableAudio" - ) - } - - fun setPaused(shouldBePaused: Boolean) { - viewModelScope.launch { - if (shouldBePaused) { - cameraSystem.pauseVideoRecording() - } else { - cameraSystem.resumeVideoRecording() - } - } - } - - private fun addSnackBarData(snackBarData: SnackbarData) { - viewModelScope.launch { - _snackBarUiState.update { old -> - val newQueue = LinkedList(old.snackBarQueue) - newQueue.add(snackBarData) - Log.d(TAG, "SnackBar added. Queue size: ${newQueue.size}") - old.copy( - snackBarQueue = newQueue - ) - } - } - } - - private fun enqueueExternalImageCaptureUnsupportedSnackBar() { - addSnackBarData( - SnackbarData( - cookie = "Image-ExternalVideoCaptureMode", - stringResource = R.string.toast_image_capture_external_unsupported, - withDismissAction = true, - testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG - ) - ) - } - private fun nextSaveLocation(saveMode: SaveMode): Pair { return when (externalCaptureMode) { ExternalCaptureMode.ImageCapture, @@ -389,7 +308,16 @@ class PreviewViewModel @Inject constructor( (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == ExternalCaptureMode.VideoCapture ) { - enqueueExternalImageCaptureUnsupportedSnackBar() + addSnackBarData( + viewModelScope, + _snackBarUiState, + SnackbarData( + cookie = "Image-ExternalVideoCaptureMode", + stringResource = R.string.toast_image_capture_external_unsupported, + withDismissAction = true, + testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG + ) + ) return } @@ -398,6 +326,8 @@ class PreviewViewModel @Inject constructor( ExternalCaptureMode.VideoCapture ) { addSnackBarData( + viewModelScope, + _snackBarUiState, SnackbarData( cookie = "Image-ExternalVideoCaptureMode", stringResource = R.string.toast_image_capture_external_unsupported, @@ -430,10 +360,12 @@ class PreviewViewModel @Inject constructor( } } if (saveLocation !is SaveLocation.Cache) { - updateLastCapturedMedia() + captureCallbacks.updateLastCapturedMedia() } else { savedUri?.let { postCurrentMediaToMediaRepository( + viewModelScope, + mediaRepository, MediaDescriptor.Content.Image(it, null, true) ) } @@ -489,20 +421,13 @@ class PreviewViewModel @Inject constructor( testTag = IMAGE_CAPTURE_FAILURE_TAG ) } - snackBarData?.let { addSnackBarData(it) } - } - - fun enqueueDisabledHdrToggleSnackBar(disabledReason: DisableRationale) { - val cookieInt = snackBarCount.incrementAndGet() - val cookie = "DisabledHdrToggle-$cookieInt" - addSnackBarData( - SnackbarData( - cookie = cookie, - stringResource = disabledReason.reasonTextResId, - withDismissAction = true, - testTag = disabledReason.testTag + snackBarData?.let { + addSnackBarData( + viewModelScope, + _snackBarUiState, + it ) - ) + } } fun startVideoRecording() { @@ -512,6 +437,8 @@ class PreviewViewModel @Inject constructor( ) { Log.d(TAG, "externalVideoRecording") addSnackBarData( + viewModelScope, + _snackBarUiState, SnackbarData( cookie = "Video-ExternalImageCaptureMode", stringResource = R.string.toast_video_capture_external_unsupported, @@ -539,9 +466,11 @@ class PreviewViewModel @Inject constructor( } if (saveLocation !is SaveLocation.Cache) { - updateLastCapturedMedia() + captureCallbacks.updateLastCapturedMedia() } else { postCurrentMediaToMediaRepository( + viewModelScope, + mediaRepository, MediaDescriptor.Content.Video(it.savedUri, null, true) ) } @@ -572,7 +501,13 @@ class PreviewViewModel @Inject constructor( } } - snackbarToShow?.let { data -> addSnackBarData(data) } + snackbarToShow?.let { data -> + addSnackBarData( + viewModelScope, + _snackBarUiState, + data + ) + } } Log.d(TAG, "cameraSystem.startRecording success") } catch (exception: IllegalStateException) { @@ -588,110 +523,4 @@ class PreviewViewModel @Inject constructor( recordingJob?.cancel() } } - - /** - "Locks" the video recording such that the user no longer needs to keep their finger pressed on the capture button - */ - fun setLockedRecording(isLocked: Boolean) { - trackedCaptureUiState.update { old -> - old.copy(isRecordingLocked = isLocked) - } - } - - fun setZoomAnimationState(targetValue: Float?) { - trackedCaptureUiState.update { old -> - old.copy(zoomAnimationTarget = targetValue) - } - } - - fun changeZoomRatio(newZoomState: CameraZoomRatio) { - cameraSystem.changeZoomRatio(newZoomState = newZoomState) - } - - fun setTestPattern(newTestPattern: TestPattern) { - cameraSystem.setTestPattern(newTestPattern = newTestPattern) - } - - fun setDynamicRange(dynamicRange: DynamicRange) { - if (externalCaptureMode != ExternalCaptureMode.ImageCapture && - externalCaptureMode != ExternalCaptureMode.MultipleImageCapture - ) { - viewModelScope.launch { - cameraSystem.setDynamicRange(dynamicRange) - } - } - } - - fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) { - viewModelScope.launch { - cameraSystem.setConcurrentCameraMode(concurrentCameraMode) - } - } - - fun setImageFormat(imageFormat: ImageOutputFormat) { - if (externalCaptureMode != ExternalCaptureMode.VideoCapture) { - viewModelScope.launch { - cameraSystem.setImageFormat(imageFormat) - } - } - } - - fun setCaptureMode(captureMode: CaptureMode) { - viewModelScope.launch { - cameraSystem.setCaptureMode(captureMode) - } - } - - fun toggleQuickSettings() { - trackedCaptureUiState.update { old -> - old.copy(isQuickSettingsOpen = !old.isQuickSettingsOpen) - } - } - - fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) { - trackedCaptureUiState.update { old -> - old.copy(focusedQuickSetting = focusedQuickSetting) - } - } - - fun toggleDebugOverlay() { - trackedCaptureUiState.update { old -> - old.copy(isDebugOverlayOpen = !old.isDebugOverlayOpen) - } - } - - fun tapToFocus(x: Float, y: Float) { - Log.d(TAG, "tapToFocus") - viewModelScope.launch { - cameraSystem.tapToFocus(x, y) - } - } - - fun onSnackBarResult(cookie: String) { - viewModelScope.launch { - _snackBarUiState.update { old -> - val newQueue = LinkedList(old.snackBarQueue) - val snackBarData = newQueue.poll() - if (snackBarData != null && snackBarData.cookie == cookie) { - // If the latest snackBar had a result, then clear snackBarToShow - Log.d(TAG, "SnackBar removed. Queue size: ${newQueue.size}") - old.copy( - snackBarQueue = newQueue - ) - } else { - old - } - } - } - } - - fun setClearUiScreenBrightness(brightness: Float) { - screenFlash.setClearUiScreenBrightness(brightness) - } - - fun setDisplayRotation(deviceRotation: DeviceRotation) { - viewModelScope.launch { - cameraSystem.setDeviceRotation(deviceRotation) - } - } } 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 5dd34fc68..22731c70e 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 @@ -113,7 +113,7 @@ class PreviewViewModelTest { @Test fun setFlash() = runTest(StandardTestDispatcher()) { previewViewModel.startCamera() - previewViewModel.setFlash(FlashMode.AUTO) + previewViewModel.quickSettingsCallbacks.setFlash(FlashMode.AUTO) advanceUntilIdle() assertIsReady(previewViewModel.captureUiState.value).also { @@ -136,7 +136,7 @@ class PreviewViewModelTest { .selectedLensFacing ).isEqualTo(LensFacing.BACK) } - previewViewModel.setLensFacing(LensFacing.FRONT) + previewViewModel.quickSettingsCallbacks.setLensFacing(LensFacing.FRONT) advanceUntilIdle() // ui state and camera should both be true now @@ -160,7 +160,7 @@ class PreviewViewModelTest { } // Toggle to open - previewViewModel.toggleQuickSettings() + previewViewModel.quickSettingsCallbacks.toggleQuickSettings() advanceUntilIdle() assertIsReady(previewViewModel.captureUiState.value).also { val quickSettings = it.quickSettingsUiState as QuickSettingsUiState.Available @@ -168,7 +168,7 @@ class PreviewViewModelTest { } // Toggle back to closed - previewViewModel.toggleQuickSettings() + previewViewModel.quickSettingsCallbacks.toggleQuickSettings() advanceUntilIdle() assertIsReady(previewViewModel.captureUiState.value).also { val quickSettings = it.quickSettingsUiState as QuickSettingsUiState.Available diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt new file mode 100644 index 000000000..03fbd525b --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt @@ -0,0 +1,165 @@ +/* + * 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.components.capture + +import android.util.Log +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.data.media.MediaDescriptor +import com.google.jetpackcamera.data.media.MediaRepository +import com.google.jetpackcamera.model.CameraZoomRatio +import com.google.jetpackcamera.model.DeviceRotation +import com.google.jetpackcamera.ui.uistate.capture.ImageWellUiState +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** + * This file contains the data class [CaptureCallbacks] and helper functions to create it. + * [CaptureCallbacks] is used to handle UI events on the capture screen. + */ + +private const val TAG = "CaptureCallbacks" + +/** + * Data class holding callbacks for capture-related UI events. + * + * @param setDisplayRotation Sets the display rotation for the camera. + * @param tapToFocus Initiates a tap-to-focus action at the given coordinates. + * @param changeZoomRatio Changes the camera's zoom ratio. + * @param setZoomAnimationState Sets the target for the zoom animation. + * @param setAudioEnabled Toggles audio recording. + * @param setLockedRecording Locks or unlocks the recording. + * @param updateLastCapturedMedia Updates the UI with the most recently captured media. + * @param imageWellToRepository Posts the media from the image well to the media repository. + * @param setPaused Pauses or resumes video recording. + */ +data class CaptureCallbacks( + val setDisplayRotation: (DeviceRotation) -> Unit, + val tapToFocus: (Float, Float) -> Unit, + val changeZoomRatio: (CameraZoomRatio) -> Unit, + val setZoomAnimationState: (Float?) -> Unit, + val setAudioEnabled: (Boolean) -> Unit, + val setLockedRecording: (Boolean) -> Unit, + val updateLastCapturedMedia: () -> Unit, + val imageWellToRepository: () -> Unit, + val setPaused: (Boolean) -> Unit +) + +/** + * Creates a [CaptureCallbacks] instance with implementations that interact with the camera system + * and update the UI state. + * + * @param viewModelScope The [CoroutineScope] for launching coroutines. + * @param cameraSystem The [CameraSystem] to interact with the camera hardware. + * @param trackedCaptureUiState The mutable state flow for the tracked capture UI state. + * @param mediaRepository The [MediaRepository] for accessing media. + * @param captureUiState The state flow for the overall capture UI state. + * @return An instance of [CaptureCallbacks]. + */ +fun getCaptureCallbacks( + viewModelScope: CoroutineScope, + cameraSystem: CameraSystem, + trackedCaptureUiState: MutableStateFlow, + mediaRepository: MediaRepository, + captureUiState: StateFlow +): CaptureCallbacks { + return CaptureCallbacks( + setDisplayRotation = { deviceRotation -> + viewModelScope.launch { + cameraSystem.setDeviceRotation(deviceRotation) + } + }, + tapToFocus = { x, y -> + Log.d(TAG, "tapToFocus") + viewModelScope.launch { + cameraSystem.tapToFocus(x, y) + } + }, + changeZoomRatio = { newZoomState -> + cameraSystem.changeZoomRatio( + newZoomState = newZoomState + ) + }, + setZoomAnimationState = { targetValue -> + trackedCaptureUiState.update { old -> + old.copy(zoomAnimationTarget = targetValue) + } + }, + setAudioEnabled = { shouldEnableAudio -> + viewModelScope.launch { + cameraSystem.setAudioEnabled(shouldEnableAudio) + } + + Log.d( + TAG, + "Toggle Audio: $shouldEnableAudio" + ) + }, + setLockedRecording = { isLocked -> + trackedCaptureUiState.update { old -> + old.copy(isRecordingLocked = isLocked) + } + }, + updateLastCapturedMedia = { + viewModelScope.launch { + trackedCaptureUiState.update { old -> + old.copy(recentCapturedMedia = mediaRepository.getLastCapturedMedia()) + } + } + }, + imageWellToRepository = { + (captureUiState.value as? CaptureUiState.Ready) + ?.let { it.imageWellUiState as? ImageWellUiState.LastCapture } + ?.let { + postCurrentMediaToMediaRepository( + viewModelScope, + mediaRepository, + it.mediaDescriptor + ) + } + }, + setPaused = { shouldBePaused -> + viewModelScope.launch { + if (shouldBePaused) { + cameraSystem.pauseVideoRecording() + } else { + cameraSystem.resumeVideoRecording() + } + } + } + ) +} + +/** + * Posts the given [MediaDescriptor] to the [MediaRepository] as the current media. + * + * @param viewModelScope The [CoroutineScope] for launching the coroutine. + * @param mediaRepository The repository to update. + * @param mediaDescriptor The media to set as current. + */ +fun postCurrentMediaToMediaRepository( + viewModelScope: CoroutineScope, + mediaRepository: MediaRepository, + mediaDescriptor: MediaDescriptor +) { + viewModelScope.launch { + mediaRepository.setCurrentMedia(mediaDescriptor) + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt new file mode 100644 index 000000000..71510531e --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt @@ -0,0 +1,117 @@ +/* + * 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.components.capture + +import android.util.Log +import com.google.jetpackcamera.ui.uistate.DisableRationale +import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState +import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import java.util.LinkedList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** + * This file contains the data class [SnackBarCallbacks] and helper functions to create it. + * [SnackBarCallbacks] is used to handle UI events related to snack bars. + */ + +private const val TAG = "SnackBarCallbacks" + +/** + * Data class holding callbacks for snack bar UI events. + * + * @param enqueueDisabledHdrToggleSnackBar Enqueues a snack bar to inform the user that HDR is + * disabled. + * @param onSnackBarResult Handles the result of a snack bar action. + */ +data class SnackBarCallbacks( + val enqueueDisabledHdrToggleSnackBar: (DisableRationale) -> Unit = {}, + val onSnackBarResult: (String) -> Unit = {} +) + +/** + * Creates a [SnackBarCallbacks] instance. + * + * @param incrementSnackBarCount A function to increment the snack bar count. + * @param viewModelScope The [CoroutineScope] for launching coroutines. + * @param snackBarUiState The mutable state flow for the snack bar UI state. + * @return An instance of [SnackBarCallbacks]. + */ +fun getSnackBarCallbacks( + incrementSnackBarCount: () -> Int = { 0 }, + viewModelScope: CoroutineScope, + snackBarUiState: MutableStateFlow +): SnackBarCallbacks { + return SnackBarCallbacks( + enqueueDisabledHdrToggleSnackBar = { disabledReason -> + val cookieInt = incrementSnackBarCount() + val cookie = "DisabledHdrToggle-$cookieInt" + addSnackBarData( + viewModelScope, + snackBarUiState, + SnackbarData( + cookie = cookie, + stringResource = disabledReason.reasonTextResId, + withDismissAction = true, + testTag = disabledReason.testTag + ) + ) + }, + onSnackBarResult = { cookie -> + viewModelScope.launch { + snackBarUiState.update { old -> + val newQueue = LinkedList(old.snackBarQueue) + val snackBarData = newQueue.poll() + if (snackBarData != null && snackBarData.cookie == cookie) { + // If the latest snackBar had a result, then clear snackBarToShow + Log.d(TAG, "SnackBar removed. Queue size: ${newQueue.size}") + old.copy( + snackBarQueue = newQueue + ) + } else { + old + } + } + } + } + ) +} + +/** + * Adds a [SnackbarData] to the snack bar queue. + * + * @param viewModelScope The [CoroutineScope] for launching coroutines. + * @param snackBarUiState The mutable state flow for the snack bar UI state. + * @param snackBarData The data for the snack bar to be added. + */ +fun addSnackBarData( + viewModelScope: CoroutineScope, + snackBarUiState: MutableStateFlow, + snackBarData: SnackbarData +) { + viewModelScope.launch { + snackBarUiState.update { old -> + val newQueue = LinkedList(old.snackBarQueue) + newQueue.add(snackBarData) + Log.d(TAG, "SnackBar added. Queue size: ${newQueue.size}") + old.copy( + snackBarQueue = newQueue + ) + } + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt new file mode 100644 index 000000000..cc6376005 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt @@ -0,0 +1,71 @@ +/* + * 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.components.capture.debug + +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.model.TestPattern +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +/** + * This file contains the data class [DebugCallbacks] and a helper function to create it. + * [DebugCallbacks] is used to handle debug-related UI events on the capture screen. + */ + +/** + * Data class holding callbacks for debug-related UI events. + * + * @param toggleDebugHidingComponents Toggles the visibility of components for debugging purposes. + * @param toggleDebugOverlay Toggles the visibility of the debug overlay. + * @param setTestPattern Sets a test pattern on the camera. + */ +data class DebugCallbacks( + val toggleDebugHidingComponents: () -> Unit, + val toggleDebugOverlay: () -> Unit, + val setTestPattern: (TestPattern) -> Unit +) + +/** + * Creates a [DebugCallbacks] instance with implementations that interact with the camera system + * and update the UI state. + * + * @param trackedCaptureUiState The mutable state flow for the tracked capture UI state. + * @param cameraSystem The [CameraSystem] to interact with the camera hardware. + * @return An instance of [DebugCallbacks]. + */ +fun getDebugCallbacks( + trackedCaptureUiState: MutableStateFlow, + cameraSystem: CameraSystem +): DebugCallbacks { + return DebugCallbacks( + toggleDebugHidingComponents = { + trackedCaptureUiState.update { old -> + old.copy(debugHidingComponents = !old.debugHidingComponents) + } + }, + toggleDebugOverlay = { + trackedCaptureUiState.update { old -> + old.copy(isDebugOverlayOpen = !old.isDebugOverlayOpen) + } + }, + setTestPattern = { newTestPattern -> + cameraSystem.setTestPattern( + newTestPattern = newTestPattern + ) + } + ) +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt new file mode 100644 index 000000000..cc0afff52 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt @@ -0,0 +1,143 @@ +/* + * 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.components.capture.quicksettings + +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.ExternalCaptureMode +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** + * This file contains the data class [QuickSettingsCallbacks] and a helper function to create it. + * [QuickSettingsCallbacks] is used to handle UI events on the quick settings screen. + */ + +/** + * Data class holding callbacks for quick settings UI events. + * + * @param toggleQuickSettings Toggles the quick settings panel. + * @param setFocusedSetting Sets the currently focused quick setting. + * @param setLensFacing Toggles the lens facing (front or back). + * @param setFlash Toggles the flash mode. + * @param setAspectRatio Sets the aspect ratio. + * @param setStreamConfig Sets the stream configuration. + * @param setDynamicRange Sets the dynamic range. + * @param setImageFormat Sets the image format. + * @param setConcurrentCameraMode Sets the concurrent camera mode. + * @param setCaptureMode Sets the capture mode. + */ +data class QuickSettingsCallbacks( + val toggleQuickSettings: () -> Unit, + val setFocusedSetting: (FocusedQuickSetting) -> Unit, + val setLensFacing: (lensFace: LensFacing) -> Unit, + val setFlash: (flashMode: FlashMode) -> Unit, + val setAspectRatio: (aspectRation: AspectRatio) -> Unit, + val setStreamConfig: (streamConfig: StreamConfig) -> Unit, + val setDynamicRange: (dynamicRange: DynamicRange) -> Unit, + val setImageFormat: (imageOutputFormat: ImageOutputFormat) -> Unit, + val setConcurrentCameraMode: (concurrentCameraMode: ConcurrentCameraMode) -> Unit, + val setCaptureMode: (CaptureMode) -> Unit +) + +/** + * Creates a [QuickSettingsCallbacks] instance with implementations that interact with the camera + * system and update the UI state. + * + * @param trackedCaptureUiState The mutable state flow for the tracked capture UI state. + * @param viewModelScope The [CoroutineScope] for launching coroutines. + * @param cameraSystem The [CameraSystem] to interact with the camera hardware. + * @param externalCaptureMode The external capture mode. + * @return An instance of [QuickSettingsCallbacks]. + */ +fun getQuickSettingsCallbacks( + trackedCaptureUiState: MutableStateFlow, + viewModelScope: CoroutineScope, + cameraSystem: CameraSystem, + externalCaptureMode: ExternalCaptureMode +): QuickSettingsCallbacks { + return QuickSettingsCallbacks( + toggleQuickSettings = { + trackedCaptureUiState.update { old -> + old.copy(isQuickSettingsOpen = !old.isQuickSettingsOpen) + } + }, + setFocusedSetting = { focusedQuickSetting -> + trackedCaptureUiState.update { old -> + old.copy(focusedQuickSetting = focusedQuickSetting) + } + }, + setLensFacing = { newLensFacing -> + viewModelScope.launch { + // apply to cameraSystem + cameraSystem.setLensFacing(newLensFacing) + } + }, + setFlash = { flashMode -> + viewModelScope.launch { + // apply to cameraSystem + cameraSystem.setFlashMode(flashMode) + } + }, + setAspectRatio = { aspectRatio -> + viewModelScope.launch { + cameraSystem.setAspectRatio(aspectRatio) + } + }, + setStreamConfig = { streamConfig -> + viewModelScope.launch { + cameraSystem.setStreamConfig(streamConfig) + } + }, + setDynamicRange = { dynamicRange -> + if (externalCaptureMode != ExternalCaptureMode.ImageCapture && + externalCaptureMode != ExternalCaptureMode.MultipleImageCapture + ) { + viewModelScope.launch { + cameraSystem.setDynamicRange(dynamicRange) + } + } + }, + setImageFormat = { imageFormat -> + if (externalCaptureMode != ExternalCaptureMode.VideoCapture) { + viewModelScope.launch { + cameraSystem.setImageFormat(imageFormat) + } + } + }, + setConcurrentCameraMode = { concurrentCameraMode -> + viewModelScope.launch { + cameraSystem.setConcurrentCameraMode(concurrentCameraMode) + } + }, + setCaptureMode = { captureMode -> + viewModelScope.launch { + cameraSystem.setCaptureMode(captureMode) + } + } + ) +} From 65ea97a368dc921f851d988f0fe11b64f59a26f4 Mon Sep 17 00:00:00 2001 From: David Jia Date: Tue, 24 Feb 2026 13:43:02 -0800 Subject: [PATCH 2/6] controllers --- .../feature/postcapture/PostCaptureScreen.kt | 23 +-- .../postcapture/PostCaptureViewModel.kt | 53 ++---- .../postcapture/PostCaptureViewModelTest.kt | 6 +- .../feature/preview/PreviewScreen.kt | 152 +++++++----------- .../feature/preview/PreviewViewModel.kt | 56 +++---- .../feature/preview/PreviewViewModelTest.kt | 8 +- ui/components/capture/build.gradle.kts | 2 + .../capture/CaptureScreenComponents.kt | 22 +-- .../capture/MultipleEventsCutter.kt | 1 + .../components/capture/SnackBarCallbacks.kt | 1 + .../components/capture/SnackBarController.kt | 27 ++++ .../capture/SnackBarControllerImpl.kt | 84 ++++++++++ .../capture/debug/DebugCallbacks.kt | 71 -------- .../capture/debug/DebugOverlayComponents.kt | 16 +- .../debug/controller/DebugController.kt | 41 +++++ .../debug/controller/DebugControllerImpl.kt | 46 ++++++ .../quicksettings/QuickSettingsCallbacks.kt | 143 ---------------- .../quicksettings/QuickSettingsScreen.kt | 86 +++++----- .../controller/QuickSettingsController.kt | 41 +++++ .../controller/QuickSettingsControllerImpl.kt | 109 +++++++++++++ .../ui/QuickSettingsComponents.kt | 7 +- .../capture/SnackBarUiStateAdapter.kt | 4 +- 22 files changed, 534 insertions(+), 465 deletions(-) create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt delete mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt delete mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt diff --git a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt index 0499d07de..f6aa0c41f 100644 --- a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt +++ b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureScreen.kt @@ -34,6 +34,7 @@ import com.google.jetpackcamera.feature.postcapture.ui.PostCaptureLayout import com.google.jetpackcamera.feature.postcapture.ui.SaveCurrentMediaButton import com.google.jetpackcamera.feature.postcapture.ui.ShareCurrentMediaButton import com.google.jetpackcamera.feature.postcapture.utils.MediaSharing +import com.google.jetpackcamera.ui.components.capture.SnackBarController import com.google.jetpackcamera.ui.components.capture.TestableSnackbar import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState import com.google.jetpackcamera.ui.uistate.postcapture.DeleteButtonUiState @@ -73,8 +74,8 @@ fun PostCaptureScreen( onSaveMedia = viewModel::saveCurrentMedia, onShareCurrentMedia = viewModel::onShareCurrentMedia, onLoadVideo = viewModel::loadCurrentVideo, - onSnackBarResult = viewModel::onSnackBarResult, - snackBarUiState = snackBarUiState + snackBarUiState = snackBarUiState, + snackBarController = viewModel.snackBarController ) } @@ -87,8 +88,8 @@ fun PostCaptureComponent( onShareCurrentMedia: () -> Unit, onDeleteMedia: (onSuccessCallback: () -> Unit) -> Unit, onLoadVideo: () -> Unit, - onSnackBarResult: (String) -> Unit, - snackBarUiState: SnackBarUiState = SnackBarUiState.Disabled + snackBarUiState: SnackBarUiState = SnackBarUiState.Disabled, + snackBarController: SnackBarController? = null ) { when (uiState) { PostCaptureUiState.Loading -> { @@ -139,12 +140,14 @@ fun PostCaptureComponent( if (snackBarUiState is SnackBarUiState.Enabled) { val snackBarData = snackBarUiState.snackBarQueue.peek() if (snackBarData != null) { - TestableSnackbar( - modifier = modifier.testTag(snackBarData.testTag), - snackbarToShow = snackBarData, - snackbarHostState = snackbarHostState, - onSnackbarResult = onSnackBarResult - ) + snackBarController?.let { + TestableSnackbar( + modifier = modifier.testTag(snackBarData.testTag), + snackbarToShow = snackBarData, + snackbarHostState = snackbarHostState, + snackBarController = snackBarController + ) + } } } } diff --git a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt index ad3db301c..9f819da7e 100644 --- a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt +++ b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt @@ -36,6 +36,8 @@ import com.google.jetpackcamera.feature.postcapture.ui.SNACKBAR_POST_CAPTURE_IMA import com.google.jetpackcamera.feature.postcapture.ui.SNACKBAR_POST_CAPTURE_VIDEO_DELETE_FAILURE import com.google.jetpackcamera.feature.postcapture.ui.SNACKBAR_POST_CAPTURE_VIDEO_SAVE_FAILURE import com.google.jetpackcamera.feature.postcapture.ui.SNACKBAR_POST_CAPTURE_VIDEO_SAVE_SUCCESS +import com.google.jetpackcamera.ui.components.capture.SnackBarController +import com.google.jetpackcamera.ui.components.capture.SnackBarControllerImpl import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState import com.google.jetpackcamera.ui.uistate.capture.SnackbarData import com.google.jetpackcamera.ui.uistate.postcapture.DeleteButtonUiState @@ -110,8 +112,10 @@ class PostCaptureViewModel @Inject constructor( MutableStateFlow(SnackBarUiState.Enabled()) val snackBarUiState: StateFlow = _snackBarUiState.asStateFlow() - private val snackBarCount = atomic(0) - + val snackBarController: SnackBarController = SnackBarControllerImpl( + viewModelScope = viewModelScope, + snackBarUiState = _snackBarUiState + ) private var player: ExoPlayer? = null private val playerState = MutableStateFlow(PlayerState.Unavailable) @@ -301,7 +305,7 @@ class PostCaptureViewModel @Inject constructor( */ private suspend fun saveMedia(mediaDescriptor: MediaDescriptor.Content) { - val cookieInt = snackBarCount.incrementAndGet() + val cookieInt = snackBarController.incrementAndGetSnackBarCount() val cookie = "MediaSave-$cookieInt" val result: Uri? try { @@ -320,7 +324,7 @@ class PostCaptureViewModel @Inject constructor( SNACKBAR_POST_CAPTURE_VIDEO_SAVE_SUCCESS } - addSnackBarData( + snackBarController.addSnackBarData( SnackbarData( cookie = cookie, stringResource = stringResource, @@ -342,7 +346,7 @@ class PostCaptureViewModel @Inject constructor( SNACKBAR_POST_CAPTURE_VIDEO_SAVE_FAILURE } - addSnackBarData( + snackBarController.addSnackBarData( SnackbarData( cookie = cookie, stringResource = stringResource, @@ -363,7 +367,7 @@ class PostCaptureViewModel @Inject constructor( SNACKBAR_POST_CAPTURE_VIDEO_SAVE_FAILURE } - addSnackBarData( + snackBarController.addSnackBarData( SnackbarData( cookie = cookie, stringResource = stringResource, @@ -387,9 +391,9 @@ class PostCaptureViewModel @Inject constructor( false } if (!result) { - val cookieInt = snackBarCount.incrementAndGet() + val cookieInt = snackBarController.incrementAndGetSnackBarCount() val cookie = "MediaDelete-$cookieInt" - addSnackBarData( + snackBarController.addSnackBarData( SnackbarData( cookie = cookie, stringResource = when (mediaDescriptor) { @@ -425,39 +429,6 @@ class PostCaptureViewModel @Inject constructor( } } } - - // snackbar interaction - private fun addSnackBarData(snackBarData: SnackbarData) { - viewModelScope.launch { - _snackBarUiState.update { old -> - val newQueue = LinkedList(old.snackBarQueue) - - newQueue.add(snackBarData) - Log.d(TAG, "SnackBar added. Queue size: ${newQueue.size}") - old.copy( - snackBarQueue = newQueue - ) - } - } - } - - fun onSnackBarResult(cookie: String) { - viewModelScope.launch { - _snackBarUiState.update { state -> - val newQueue = LinkedList(state.snackBarQueue) - val snackBarData = newQueue.poll() - if (snackBarData != null && snackBarData.cookie == cookie) { - // If the latest snackBar had a result, then clear snackBarToShow - Log.d(TAG, "SnackBar removed. Queue size: ${newQueue.size}") - state.copy( - snackBarQueue = newQueue - ) - } else { - state - } - } - } - } } sealed interface PlayerState { diff --git a/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt b/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt index 66b94d606..0a85bbb40 100644 --- a/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt +++ b/feature/postcapture/src/test/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModelTest.kt @@ -349,7 +349,7 @@ internal class PostCaptureViewModelTest { val cookie = snackBarUiState.snackBarQueue.first().cookie // When - viewModel.onSnackBarResult(cookie) + viewModel.snackBarController.onSnackBarResult(cookie) advanceUntilIdle() // Then @@ -367,7 +367,7 @@ internal class PostCaptureViewModelTest { advanceUntilIdle() // When - viewModel.onSnackBarResult("incorrect_cookie") + viewModel.snackBarController.onSnackBarResult("incorrect_cookie") advanceUntilIdle() // Then @@ -382,7 +382,7 @@ internal class PostCaptureViewModelTest { assertThat(snackBarUiState.snackBarQueue).isEmpty() // When - viewModel.onSnackBarResult("any_cookie") + viewModel.snackBarController.onSnackBarResult("any_cookie") advanceUntilIdle() // Then diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index dfb21e3f1..614047d2e 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -65,19 +65,11 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.jetpackcamera.core.camera.InitialRecordingSettings import com.google.jetpackcamera.core.camera.VideoRecordingState -import com.google.jetpackcamera.model.AspectRatio import com.google.jetpackcamera.model.CaptureEvent import com.google.jetpackcamera.model.CaptureMode -import com.google.jetpackcamera.model.ConcurrentCameraMode -import com.google.jetpackcamera.model.DynamicRange import com.google.jetpackcamera.model.ExternalCaptureMode -import com.google.jetpackcamera.model.FlashMode import com.google.jetpackcamera.model.ImageCaptureEvent -import com.google.jetpackcamera.model.ImageOutputFormat -import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.LensToZoom -import com.google.jetpackcamera.model.StreamConfig -import com.google.jetpackcamera.model.TestPattern import com.google.jetpackcamera.model.VideoCaptureEvent import com.google.jetpackcamera.ui.components.capture.AmplitudeToggleButton import com.google.jetpackcamera.ui.components.capture.CAPTURE_MODE_TOGGLE_BUTTON @@ -93,6 +85,7 @@ import com.google.jetpackcamera.ui.components.capture.PreviewDisplay import com.google.jetpackcamera.ui.components.capture.PreviewLayout import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.ScreenFlashScreen +import com.google.jetpackcamera.ui.components.capture.SnackBarController import com.google.jetpackcamera.ui.components.capture.StabilizationIcon import com.google.jetpackcamera.ui.components.capture.TestableSnackbar import com.google.jetpackcamera.ui.components.capture.VIDEO_QUALITY_TAG @@ -101,7 +94,9 @@ import com.google.jetpackcamera.ui.components.capture.ZoomButtonRow import com.google.jetpackcamera.ui.components.capture.ZoomState import com.google.jetpackcamera.ui.components.capture.debouncedOrientationFlow import com.google.jetpackcamera.ui.components.capture.debug.DebugOverlay +import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugController import com.google.jetpackcamera.ui.components.capture.quicksettings.QuickSettingsBottomSheet +import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsController import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashModeIndicator import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.HdrIndicator import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.ToggleQuickSettingsButton @@ -117,7 +112,6 @@ import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState import com.google.jetpackcamera.ui.uistate.capture.ZoomControlUiState import com.google.jetpackcamera.ui.uistate.capture.ZoomUiState import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.launch @@ -254,7 +248,7 @@ fun PreviewScreen( val oldAudioEnabled = it.isAudioEnabled Log.d(TAG, "reset pre recording settings") viewModel.captureCallbacks.setAudioEnabled(oldAudioEnabled) - viewModel.quickSettingsCallbacks.setLensFacing(oldPrimaryLensFacing) + viewModel.quickSettingsController.setLensFacing(oldPrimaryLensFacing) zoomState.apply { absoluteZoom( targetZoomLevel = oldZoomRatios[oldPrimaryLensFacing] ?: 1f, @@ -282,9 +276,7 @@ fun PreviewScreen( surfaceRequest = surfaceRequest, onNavigateToSettings = onNavigateToSettings, onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness, - onSetLensFacing = viewModel.quickSettingsCallbacks.setLensFacing, onTapToFocus = viewModel.captureCallbacks.tapToFocus, - onSetTestPattern = viewModel.debugCallbacks.setTestPattern, onSetImageWell = viewModel.captureCallbacks.imageWellToRepository, onAbsoluteZoom = { zoomRatio: Float, lensToZoom: LensToZoom -> scope.launch { @@ -318,22 +310,6 @@ fun PreviewScreen( ) } }, - - onSetCaptureMode = viewModel.quickSettingsCallbacks.setCaptureMode, - onChangeFlash = viewModel.quickSettingsCallbacks.setFlash, - onChangeAspectRatio = viewModel.quickSettingsCallbacks.setAspectRatio, - onSetStreamConfig = viewModel.quickSettingsCallbacks.setStreamConfig, - onChangeDynamicRange = viewModel.quickSettingsCallbacks.setDynamicRange, - onChangeConcurrentCameraMode = - viewModel.quickSettingsCallbacks.setConcurrentCameraMode, - onChangeImageFormat = viewModel.quickSettingsCallbacks.setImageFormat, - onDisabledCaptureMode = - viewModel.snackBarCallbacks.enqueueDisabledHdrToggleSnackBar, - onToggleQuickSettings = viewModel.quickSettingsCallbacks.toggleQuickSettings, - onSetFocusedSetting = viewModel.quickSettingsCallbacks.setFocusedSetting, - onToggleDebugOverlay = viewModel.debugCallbacks.toggleDebugOverlay, - onToggleDebugHidingComponents = - viewModel.debugCallbacks.toggleDebugHidingComponents, onSetPause = viewModel.captureCallbacks.setPaused, onSetAudioEnabled = viewModel.captureCallbacks.setAudioEnabled, onCaptureImage = viewModel::captureImage, @@ -341,10 +317,11 @@ fun PreviewScreen( onStopVideoRecording = viewModel::stopVideoRecording, onLockVideoRecording = viewModel.captureCallbacks.setLockedRecording, onRequestWindowColorMode = onRequestWindowColorMode, - onSnackBarResult = viewModel.snackBarCallbacks.onSnackBarResult, onNavigatePostCapture = onNavigateToPostCapture, debugUiState = debugUiState, - snackBarUiState = snackBarUiState + snackBarUiState = snackBarUiState, + debugController = viewModel.debugController, + snackBarController = viewModel.snackBarController ) val readStoragePermission: PermissionState = rememberPermissionState( Manifest.permission.READ_EXTERNAL_STORAGE @@ -370,26 +347,12 @@ private fun ContentScreen( modifier: Modifier = Modifier, onNavigateToSettings: () -> Unit = {}, onClearUiScreenBrightness: (Float) -> Unit = {}, - onSetCaptureMode: (CaptureMode) -> Unit = {}, - onSetLensFacing: (newLensFacing: LensFacing) -> Unit = {}, onTapToFocus: (x: Float, y: Float) -> Unit = { _, _ -> }, - onSetTestPattern: (TestPattern) -> Unit = {}, onSetImageWell: () -> Unit = {}, onAbsoluteZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, onScaleZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, onIncrementZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, onAnimateZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, - onChangeFlash: (FlashMode) -> Unit = {}, - onChangeAspectRatio: (AspectRatio) -> Unit = {}, - onSetStreamConfig: (StreamConfig) -> Unit = {}, - onChangeDynamicRange: (DynamicRange) -> Unit = {}, - onChangeConcurrentCameraMode: (ConcurrentCameraMode) -> Unit = {}, - onChangeImageFormat: (ImageOutputFormat) -> Unit = {}, - onDisabledCaptureMode: (DisableRationale) -> Unit = {}, - onToggleQuickSettings: () -> Unit = {}, - onSetFocusedSetting: (FocusedQuickSetting) -> Unit = {}, - onToggleDebugOverlay: () -> Unit = {}, - onToggleDebugHidingComponents: () -> Unit = {}, onSetPause: (Boolean) -> Unit = {}, onSetAudioEnabled: (Boolean) -> Unit = {}, onCaptureImage: (ContentResolver) -> Unit = {}, @@ -397,17 +360,19 @@ private fun ContentScreen( onStopVideoRecording: () -> Unit = {}, onLockVideoRecording: (Boolean) -> Unit = {}, onRequestWindowColorMode: (Int) -> Unit = {}, - onSnackBarResult: (String) -> Unit = {}, onNavigatePostCapture: () -> Unit = {}, debugUiState: DebugUiState = DebugUiState.Disabled, - snackBarUiState: SnackBarUiState = SnackBarUiState.Disabled + snackBarUiState: SnackBarUiState = SnackBarUiState.Disabled, + debugController: DebugController? = null, + quickSettingsController: QuickSettingsController? = null, + snackBarController: SnackBarController? = null ) { val onFlipCamera = { if (captureUiState.flipLensUiState is FlipLensUiState.Available) { - onSetLensFacing( + quickSettingsController?.setLensFacing( ( - captureUiState.flipLensUiState as FlipLensUiState.Available - ) + captureUiState.flipLensUiState as FlipLensUiState.Available + ) .selectedLensFacing.flip() ) } @@ -460,7 +425,7 @@ private fun ContentScreen( if ((captureUiState.quickSettingsUiState as? QuickSettingsUiState.Available) ?.quickSettingsIsOpen == true ) { - onToggleQuickSettings() + quickSettingsController?.toggleQuickSettings() } action() } @@ -478,7 +443,6 @@ private fun ContentScreen( onIncrementZoom = { targetZoom -> onIncrementZoom(targetZoom, LensToZoom.PRIMARY) }, - onToggleQuickSettings = onToggleQuickSettings, onStartVideoRecording = { runCaptureAction { onStartVideoRecording() @@ -529,14 +493,17 @@ private fun ContentScreen( enter = fadeIn(), exit = fadeOut(animationSpec = tween(delayMillis = 1_500)) ) { - ToggleQuickSettingsButton( - modifier = it, - toggleBottomSheet = onToggleQuickSettings, - isOpen = ( - captureUiState.quickSettingsUiState - as? QuickSettingsUiState.Available - )?.quickSettingsIsOpen == true - ) + quickSettingsController?.let { quickSettingsController -> + ToggleQuickSettingsButton( + modifier = it, + isOpen = ( + captureUiState.quickSettingsUiState + as? QuickSettingsUiState.Available + )?.quickSettingsIsOpen == true, + quickSettingsController = quickSettingsController + + ) + } } }, audioToggleButton = { @@ -552,43 +519,36 @@ private fun ContentScreen( uiState = captureUiState.captureModeToggleUiState as CaptureModeToggleUiState.Available, - onChangeCaptureMode = onSetCaptureMode, - onToggleWhenDisabled = onDisabledCaptureMode, + quickSettingsController = quickSettingsController, + snackBarController = snackBarController, modifier = it.testTag(CAPTURE_MODE_TOGGLE_BUTTON) ) } }, quickSettingsOverlay = { - QuickSettingsBottomSheet( - modifier = it, - quickSettingsUiState = captureUiState.quickSettingsUiState, - toggleQuickSettings = onToggleQuickSettings, - onSetFocusedSetting = onSetFocusedSetting, - onLensFaceClick = onSetLensFacing, - onFlashModeClick = onChangeFlash, - onAspectRatioClick = onChangeAspectRatio, - onStreamConfigClick = onSetStreamConfig, - onDynamicRangeClick = onChangeDynamicRange, - onImageOutputFormatClick = onChangeImageFormat, - onConcurrentCameraModeClick = onChangeConcurrentCameraMode, - onCaptureModeClick = onSetCaptureMode, - onNavigateToSettings = { - onToggleQuickSettings() - onNavigateToSettings() - } - ) + quickSettingsController?.let { quickSettingsController -> + QuickSettingsBottomSheet( + modifier = it, + quickSettingsUiState = captureUiState.quickSettingsUiState, + onNavigateToSettings = { + quickSettingsController.toggleQuickSettings() + onNavigateToSettings() + }, + quickSettingsController = quickSettingsController + ) + } }, debugOverlay = { modifier, extraControls -> - (debugUiState as? DebugUiState.Enabled)?.let { - DebugOverlay( - modifier = modifier, - toggleIsOpen = onToggleDebugOverlay, - debugUiState = it, - onSetTestPattern = onSetTestPattern, - onChangeZoomRatio = { f: Float -> onAbsoluteZoom(f, LensToZoom.PRIMARY) }, - extraControls = extraControls.orEmpty(), - onToggleHidingComponents = onToggleDebugHidingComponents - ) + (debugUiState as? DebugUiState.Enabled)?.let { debugUiState -> + debugController?.let { debugController -> + DebugOverlay( + modifier = modifier, + debugUiState = debugUiState, + onChangeZoomRatio = { f: Float -> onAbsoluteZoom(f, LensToZoom.PRIMARY) }, + extraControls = extraControls.orEmpty(), + debugController = debugController + ) + } } }, debugVisibilityWrapper = { content -> @@ -612,12 +572,14 @@ private fun ContentScreen( if (snackBarUiState is SnackBarUiState.Enabled) { val snackBarData = snackBarUiState.snackBarQueue.peek() if (snackBarData != null) { - TestableSnackbar( - modifier = modifier.testTag(snackBarData.testTag), - snackbarToShow = snackBarData, - snackbarHostState = snackbarHostState, - onSnackbarResult = onSnackBarResult - ) + snackBarController?.let { snackBarController -> + TestableSnackbar( + modifier = modifier.testTag(snackBarData.testTag), + snackbarToShow = snackBarData, + snackbarHostState = snackbarHostState, + snackBarController = snackBarController + ) + } } } }, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index dc7978ee4..9c7570298 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -55,15 +55,18 @@ import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.ui.components.capture.LOW_LIGHT_BOOST_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.ScreenFlash +import com.google.jetpackcamera.ui.components.capture.SnackBarController +import com.google.jetpackcamera.ui.components.capture.SnackBarControllerImpl import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG -import com.google.jetpackcamera.ui.components.capture.addSnackBarData -import com.google.jetpackcamera.ui.components.capture.debug.getDebugCallbacks +import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugController +import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugControllerImpl import com.google.jetpackcamera.ui.components.capture.getCaptureCallbacks import com.google.jetpackcamera.ui.components.capture.getSnackBarCallbacks import com.google.jetpackcamera.ui.components.capture.postCurrentMediaToMediaRepository -import com.google.jetpackcamera.ui.components.capture.quicksettings.getQuickSettingsCallbacks +import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsController +import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsControllerImpl import com.google.jetpackcamera.ui.uistate.capture.DebugUiState import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState import com.google.jetpackcamera.ui.uistate.capture.SnackbarData @@ -72,8 +75,8 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState import com.google.jetpackcamera.ui.uistateadapter.capture.compound.captureUiState import com.google.jetpackcamera.ui.uistateadapter.capture.debugUiState import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.atomicfu.atomic +import javax.inject.Inject import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job @@ -134,7 +137,6 @@ class PreviewViewModel @Inject constructor( val screenFlash = ScreenFlash(cameraSystem, viewModelScope) - private val snackBarCount = atomic(0) private val videoCaptureStartedCount = atomic(0) // Eagerly initialize the CameraSystem and encapsulate in a Deferred that can be @@ -171,20 +173,24 @@ class PreviewViewModel @Inject constructor( initialValue = DebugUiState.Disabled ) - val quickSettingsCallbacks = getQuickSettingsCallbacks( + val quickSettingsController: QuickSettingsController = QuickSettingsControllerImpl( trackedCaptureUiState = trackedCaptureUiState, viewModelScope = viewModelScope, cameraSystem = cameraSystem, externalCaptureMode = externalCaptureMode ) - val debugCallbacks = getDebugCallbacks( - trackedCaptureUiState = trackedCaptureUiState, - cameraSystem = cameraSystem + val debugController: DebugController = DebugControllerImpl( + cameraSystem = cameraSystem, + trackedCaptureUiState = trackedCaptureUiState ) val snackBarCallbacks = getSnackBarCallbacks( - incrementSnackBarCount = { snackBarCount.incrementAndGet() }, + viewModelScope = viewModelScope, + snackBarUiState = _snackBarUiState + ) + + val snackBarController: SnackBarController = SnackBarControllerImpl( viewModelScope = viewModelScope, snackBarUiState = _snackBarUiState ) @@ -216,11 +222,9 @@ class PreviewViewModel @Inject constructor( .distinctUntilChanged() .collect { state -> if (state is LowLightBoostState.Error) { - val cookieInt = snackBarCount.incrementAndGet() + val cookieInt = snackBarController.incrementAndGetSnackBarCount() Log.d(TAG, "LowLightBoostState changed to Error #$cookieInt") - addSnackBarData( - viewModelScope, - _snackBarUiState, + snackBarController.addSnackBarData( SnackbarData( cookie = "LowLightBoost-$cookieInt", stringResource = R.string.low_light_boost_error_toast_message, @@ -308,9 +312,7 @@ class PreviewViewModel @Inject constructor( (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == ExternalCaptureMode.VideoCapture ) { - addSnackBarData( - viewModelScope, - _snackBarUiState, + snackBarController.addSnackBarData( SnackbarData( cookie = "Image-ExternalVideoCaptureMode", stringResource = R.string.toast_image_capture_external_unsupported, @@ -325,9 +327,7 @@ class PreviewViewModel @Inject constructor( (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == ExternalCaptureMode.VideoCapture ) { - addSnackBarData( - viewModelScope, - _snackBarUiState, + snackBarController.addSnackBarData( SnackbarData( cookie = "Image-ExternalVideoCaptureMode", stringResource = R.string.toast_image_capture_external_unsupported, @@ -391,7 +391,7 @@ class PreviewViewModel @Inject constructor( onSuccess: (T) -> Unit = {}, onFailure: (exception: Exception) -> Unit = {} ) { - val cookieInt = snackBarCount.incrementAndGet() + val cookieInt = snackBarController.incrementAndGetSnackBarCount() val cookie = "Image-$cookieInt" val snackBarData = try { traceAsync(IMAGE_CAPTURE_TRACE, cookieInt) { @@ -422,9 +422,7 @@ class PreviewViewModel @Inject constructor( ) } snackBarData?.let { - addSnackBarData( - viewModelScope, - _snackBarUiState, + snackBarController.addSnackBarData( it ) } @@ -436,9 +434,7 @@ class PreviewViewModel @Inject constructor( ExternalCaptureMode.ImageCapture ) { Log.d(TAG, "externalVideoRecording") - addSnackBarData( - viewModelScope, - _snackBarUiState, + snackBarController.addSnackBarData( SnackbarData( cookie = "Video-ExternalImageCaptureMode", stringResource = R.string.toast_video_capture_external_unsupported, @@ -502,11 +498,7 @@ class PreviewViewModel @Inject constructor( } snackbarToShow?.let { data -> - addSnackBarData( - viewModelScope, - _snackBarUiState, - data - ) + snackBarController.addSnackBarData(data) } } Log.d(TAG, "cameraSystem.startRecording success") 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 22731c70e..f90737ee6 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 @@ -113,7 +113,7 @@ class PreviewViewModelTest { @Test fun setFlash() = runTest(StandardTestDispatcher()) { previewViewModel.startCamera() - previewViewModel.quickSettingsCallbacks.setFlash(FlashMode.AUTO) + previewViewModel.quickSettingsController.setFlash(FlashMode.AUTO) advanceUntilIdle() assertIsReady(previewViewModel.captureUiState.value).also { @@ -136,7 +136,7 @@ class PreviewViewModelTest { .selectedLensFacing ).isEqualTo(LensFacing.BACK) } - previewViewModel.quickSettingsCallbacks.setLensFacing(LensFacing.FRONT) + previewViewModel.quickSettingsController.setLensFacing(LensFacing.FRONT) advanceUntilIdle() // ui state and camera should both be true now @@ -160,7 +160,7 @@ class PreviewViewModelTest { } // Toggle to open - previewViewModel.quickSettingsCallbacks.toggleQuickSettings() + previewViewModel.quickSettingsController.toggleQuickSettings() advanceUntilIdle() assertIsReady(previewViewModel.captureUiState.value).also { val quickSettings = it.quickSettingsUiState as QuickSettingsUiState.Available @@ -168,7 +168,7 @@ class PreviewViewModelTest { } // Toggle back to closed - previewViewModel.quickSettingsCallbacks.toggleQuickSettings() + previewViewModel.quickSettingsController.toggleQuickSettings() advanceUntilIdle() assertIsReady(previewViewModel.captureUiState.value).also { val quickSettings = it.quickSettingsUiState as QuickSettingsUiState.Available diff --git a/ui/components/capture/build.gradle.kts b/ui/components/capture/build.gradle.kts index 90cc1e81c..ece39b1a8 100644 --- a/ui/components/capture/build.gradle.kts +++ b/ui/components/capture/build.gradle.kts @@ -100,6 +100,8 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + implementation(libs.kotlinx.atomicfu) + implementation(project(":ui:uistate")) implementation(project(":ui:uistate:capture")) implementation(project(":core:camera")) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index ef96e1174..3fc343e27 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -108,6 +108,7 @@ import com.google.jetpackcamera.core.camera.VideoRecordingState import com.google.jetpackcamera.model.CaptureMode import com.google.jetpackcamera.model.StabilizationMode import com.google.jetpackcamera.model.VideoQuality +import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsController import com.google.jetpackcamera.ui.uistate.DisableRationale import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState @@ -238,8 +239,8 @@ fun AmplitudeToggleButton( @Composable fun CaptureModeToggleButton( uiState: CaptureModeToggleUiState.Available, - onChangeCaptureMode: (CaptureMode) -> Unit, - onToggleWhenDisabled: (DisableRationale) -> Unit, + quickSettingsController: QuickSettingsController?, + snackBarController: SnackBarController?, modifier: Modifier = Modifier ) { // Captures image (left), else captures video (right). @@ -261,7 +262,7 @@ fun CaptureModeToggleButton( checked = toggleState, onCheckedChange = { isChecked -> val newCaptureMode = if (isChecked) CaptureMode.VIDEO_ONLY else CaptureMode.IMAGE_ONLY - onChangeCaptureMode(newCaptureMode) + quickSettingsController?.setCaptureMode(newCaptureMode) }, onToggleWhenDisabled = { val disabledReason: DisableRationale? = @@ -274,7 +275,7 @@ fun CaptureModeToggleButton( as? SingleSelectableUiState.Disabled ) ?.disabledReason - disabledReason?.let { onToggleWhenDisabled(it) } + disabledReason?.let { snackBarController?.enqueueDisabledHdrToggleSnackBar(it) } }, enabled = enabled, leftIcon = if (uiState.selectedCaptureMode == @@ -309,7 +310,7 @@ fun TestableSnackbar( modifier: Modifier = Modifier, snackbarToShow: SnackbarData, snackbarHostState: SnackbarHostState, - onSnackbarResult: (String) -> Unit + snackBarController: SnackBarController ) { Box( // box seems to need to have some size to be detected by UiAutomator @@ -335,11 +336,12 @@ fun TestableSnackbar( ) when (result) { SnackbarResult.ActionPerformed, - SnackbarResult.Dismissed -> onSnackbarResult(snackbarToShow.cookie) + SnackbarResult.Dismissed -> snackBarController.onSnackBarResult( + snackbarToShow.cookie) } } catch (e: Exception) { // This is equivalent to dismissing the snackbar - onSnackbarResult(snackbarToShow.cookie) + snackBarController.onSnackBarResult(snackbarToShow.cookie) } } } @@ -515,12 +517,12 @@ fun CaptureButton( modifier: Modifier = Modifier, captureButtonUiState: CaptureButtonUiState, isQuickSettingsOpen: Boolean, - onToggleQuickSettings: () -> Unit = {}, onIncrementZoom: (Float) -> Unit = {}, onCaptureImage: (ContentResolver) -> Unit = {}, onStartVideoRecording: () -> Unit = {}, onStopVideoRecording: () -> Unit = {}, - onLockVideoRecording: (Boolean) -> Unit = {} + onLockVideoRecording: (Boolean) -> Unit = {}, + quickSettingsController: QuickSettingsController? = null ) { val context = LocalContext.current @@ -532,7 +534,7 @@ fun CaptureButton( onCaptureImage(context.contentResolver) } if (isQuickSettingsOpen) { - onToggleQuickSettings() + quickSettingsController?.toggleQuickSettings() } }, onStartRecording = { diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt index 93fea248b..c8728e548 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt @@ -35,3 +35,4 @@ internal class MultipleEventsCutter { private const val DURATION_BETWEEN_CLICKS_MS = 300L } } + diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt index 71510531e..2bf3106e7 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt @@ -115,3 +115,4 @@ fun addSnackBarData( } } } + diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt new file mode 100644 index 000000000..ed2acca86 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt @@ -0,0 +1,27 @@ +/* + * 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.components.capture + +import com.google.jetpackcamera.ui.uistate.DisableRationale +import com.google.jetpackcamera.ui.uistate.capture.SnackbarData + +interface SnackBarController { + fun enqueueDisabledHdrToggleSnackBar(disabledReason: DisableRationale) + fun onSnackBarResult(cookie: String) + fun incrementAndGetSnackBarCount(): Int + fun addSnackBarData(snackBarData: SnackbarData) +} \ No newline at end of file diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt new file mode 100644 index 000000000..bc373c04c --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt @@ -0,0 +1,84 @@ +/* + * 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.components.capture + +import android.util.Log +import com.google.jetpackcamera.ui.uistate.DisableRationale +import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState +import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.util.LinkedList + +private const val TAG = "SnackBarControllerImpl" + +class SnackBarControllerImpl( + private val viewModelScope: CoroutineScope, + private val snackBarUiState: MutableStateFlow +): SnackBarController { + val snackBarCount = atomic(0) + override fun enqueueDisabledHdrToggleSnackBar(disabledReason: DisableRationale) { + val cookieInt = incrementAndGetSnackBarCount() + val cookie = "DisabledHdrToggle-$cookieInt" + addSnackBarData( + SnackbarData( + cookie = cookie, + stringResource = disabledReason.reasonTextResId, + withDismissAction = true, + testTag = disabledReason.testTag + ) + ) + } + + override fun onSnackBarResult(cookie: String) { + viewModelScope.launch { + snackBarUiState.update { old -> + val newQueue = LinkedList(old.snackBarQueue) + val snackBarData = newQueue.poll() + if (snackBarData != null && snackBarData.cookie == cookie) { + // If the latest snackBar had a result, then clear snackBarToShow + Log.d(TAG, "SnackBar removed. Queue size: ${newQueue.size}") + old.copy( + snackBarQueue = newQueue + ) + } else { + old + } + } + } + } + + override fun incrementAndGetSnackBarCount(): Int { + return snackBarCount.incrementAndGet() + } + + override fun addSnackBarData(snackBarData: SnackbarData) { + viewModelScope.launch { + snackBarUiState.update { old -> + val newQueue = LinkedList(old.snackBarQueue) + newQueue.add(snackBarData) + Log.d(TAG, "SnackBar added. Queue size: ${newQueue.size}") + old.copy( + snackBarQueue = newQueue + ) + } + } + } +} \ No newline at end of file diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt deleted file mode 100644 index cc6376005..000000000 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugCallbacks.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.components.capture.debug - -import com.google.jetpackcamera.core.camera.CameraSystem -import com.google.jetpackcamera.model.TestPattern -import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update - -/** - * This file contains the data class [DebugCallbacks] and a helper function to create it. - * [DebugCallbacks] is used to handle debug-related UI events on the capture screen. - */ - -/** - * Data class holding callbacks for debug-related UI events. - * - * @param toggleDebugHidingComponents Toggles the visibility of components for debugging purposes. - * @param toggleDebugOverlay Toggles the visibility of the debug overlay. - * @param setTestPattern Sets a test pattern on the camera. - */ -data class DebugCallbacks( - val toggleDebugHidingComponents: () -> Unit, - val toggleDebugOverlay: () -> Unit, - val setTestPattern: (TestPattern) -> Unit -) - -/** - * Creates a [DebugCallbacks] instance with implementations that interact with the camera system - * and update the UI state. - * - * @param trackedCaptureUiState The mutable state flow for the tracked capture UI state. - * @param cameraSystem The [CameraSystem] to interact with the camera hardware. - * @return An instance of [DebugCallbacks]. - */ -fun getDebugCallbacks( - trackedCaptureUiState: MutableStateFlow, - cameraSystem: CameraSystem -): DebugCallbacks { - return DebugCallbacks( - toggleDebugHidingComponents = { - trackedCaptureUiState.update { old -> - old.copy(debugHidingComponents = !old.debugHidingComponents) - } - }, - toggleDebugOverlay = { - trackedCaptureUiState.update { old -> - old.copy(isDebugOverlayOpen = !old.isDebugOverlayOpen) - } - }, - setTestPattern = { newTestPattern -> - cameraSystem.setTestPattern( - newTestPattern = newTestPattern - ) - } - ) -} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt index f4dc7abb3..256d8b9f6 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt @@ -77,6 +77,8 @@ import com.google.jetpackcamera.ui.components.capture.LOGICAL_CAMERA_ID_TAG import com.google.jetpackcamera.ui.components.capture.PHYSICAL_CAMERA_ID_TAG import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.ZOOM_RATIO_TAG +import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugController +import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugControllerImpl import com.google.jetpackcamera.ui.uistate.capture.DebugUiState import kotlin.math.abs @@ -168,22 +170,20 @@ private fun ToggleVisibilityButton( fun DebugOverlay( modifier: Modifier = Modifier, onChangeZoomRatio: (Float) -> Unit, - onSetTestPattern: (TestPattern) -> Unit, - toggleIsOpen: () -> Unit, - onToggleHidingComponents: () -> Unit, debugUiState: DebugUiState.Enabled, - vararg extraControls: @Composable () -> Unit + vararg extraControls: @Composable () -> Unit, + debugController: DebugController ) { Box(modifier = modifier.fillMaxSize()) { Column(modifier = Modifier.padding(top = 100.dp)) { ToggleVisibilityButton( - onToggleHidingComponents = onToggleHidingComponents, + onToggleHidingComponents = debugController::toggleDebugHidingComponents, isHidingComponents = debugUiState.debugHidingComponents ) if (!debugUiState.debugHidingComponents) { DebugConsole( debugUiState = debugUiState, - onToggleDebugOverlay = toggleIsOpen, + onToggleDebugOverlay = debugController::toggleDebugOverlay, extraControls = extraControls ) } @@ -193,8 +193,8 @@ fun DebugOverlay( DebugDialogContainer( modifier = Modifier, onChangeZoomRatio = onChangeZoomRatio, - onSetTestPattern = onSetTestPattern, - toggleIsOpen = toggleIsOpen, + onSetTestPattern = debugController::setTestPattern, + toggleIsOpen = debugController::toggleDebugOverlay, debugUiState = it ) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt new file mode 100644 index 000000000..6c595793d --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt @@ -0,0 +1,41 @@ +/* + * 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.components.capture.debug.controller + +import com.google.jetpackcamera.model.TestPattern + +/** + * Interface for controlling debug features. + */ +interface DebugController { + /** + * Toggles the visibility of debug UI components. + */ + fun toggleDebugHidingComponents() + + /** + * Toggles the debug overlay. + */ + fun toggleDebugOverlay() + + /** + * Sets the test pattern for the camera. + * + * @param testPattern The test pattern to set. + */ + fun setTestPattern(testPattern: TestPattern) +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt new file mode 100644 index 000000000..fdd6bc975 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt @@ -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.ui.components.capture.debug.controller + +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.model.TestPattern +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +class DebugControllerImpl( + private val cameraSystem: CameraSystem, + private val trackedCaptureUiState: MutableStateFlow +): DebugController { + override fun toggleDebugHidingComponents() { + trackedCaptureUiState.update { old -> + old.copy(debugHidingComponents = !old.debugHidingComponents) + } + } + + override fun toggleDebugOverlay() { + trackedCaptureUiState.update { old -> + old.copy(isDebugOverlayOpen = !old.isDebugOverlayOpen) + } + } + + override fun setTestPattern(testPattern: TestPattern) { + cameraSystem.setTestPattern( + newTestPattern = testPattern + ) + } +} \ No newline at end of file diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt deleted file mode 100644 index cc0afff52..000000000 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsCallbacks.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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.components.capture.quicksettings - -import com.google.jetpackcamera.core.camera.CameraSystem -import com.google.jetpackcamera.model.AspectRatio -import com.google.jetpackcamera.model.CaptureMode -import com.google.jetpackcamera.model.ConcurrentCameraMode -import com.google.jetpackcamera.model.DynamicRange -import com.google.jetpackcamera.model.ExternalCaptureMode -import com.google.jetpackcamera.model.FlashMode -import com.google.jetpackcamera.model.ImageOutputFormat -import com.google.jetpackcamera.model.LensFacing -import com.google.jetpackcamera.model.StreamConfig -import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -/** - * This file contains the data class [QuickSettingsCallbacks] and a helper function to create it. - * [QuickSettingsCallbacks] is used to handle UI events on the quick settings screen. - */ - -/** - * Data class holding callbacks for quick settings UI events. - * - * @param toggleQuickSettings Toggles the quick settings panel. - * @param setFocusedSetting Sets the currently focused quick setting. - * @param setLensFacing Toggles the lens facing (front or back). - * @param setFlash Toggles the flash mode. - * @param setAspectRatio Sets the aspect ratio. - * @param setStreamConfig Sets the stream configuration. - * @param setDynamicRange Sets the dynamic range. - * @param setImageFormat Sets the image format. - * @param setConcurrentCameraMode Sets the concurrent camera mode. - * @param setCaptureMode Sets the capture mode. - */ -data class QuickSettingsCallbacks( - val toggleQuickSettings: () -> Unit, - val setFocusedSetting: (FocusedQuickSetting) -> Unit, - val setLensFacing: (lensFace: LensFacing) -> Unit, - val setFlash: (flashMode: FlashMode) -> Unit, - val setAspectRatio: (aspectRation: AspectRatio) -> Unit, - val setStreamConfig: (streamConfig: StreamConfig) -> Unit, - val setDynamicRange: (dynamicRange: DynamicRange) -> Unit, - val setImageFormat: (imageOutputFormat: ImageOutputFormat) -> Unit, - val setConcurrentCameraMode: (concurrentCameraMode: ConcurrentCameraMode) -> Unit, - val setCaptureMode: (CaptureMode) -> Unit -) - -/** - * Creates a [QuickSettingsCallbacks] instance with implementations that interact with the camera - * system and update the UI state. - * - * @param trackedCaptureUiState The mutable state flow for the tracked capture UI state. - * @param viewModelScope The [CoroutineScope] for launching coroutines. - * @param cameraSystem The [CameraSystem] to interact with the camera hardware. - * @param externalCaptureMode The external capture mode. - * @return An instance of [QuickSettingsCallbacks]. - */ -fun getQuickSettingsCallbacks( - trackedCaptureUiState: MutableStateFlow, - viewModelScope: CoroutineScope, - cameraSystem: CameraSystem, - externalCaptureMode: ExternalCaptureMode -): QuickSettingsCallbacks { - return QuickSettingsCallbacks( - toggleQuickSettings = { - trackedCaptureUiState.update { old -> - old.copy(isQuickSettingsOpen = !old.isQuickSettingsOpen) - } - }, - setFocusedSetting = { focusedQuickSetting -> - trackedCaptureUiState.update { old -> - old.copy(focusedQuickSetting = focusedQuickSetting) - } - }, - setLensFacing = { newLensFacing -> - viewModelScope.launch { - // apply to cameraSystem - cameraSystem.setLensFacing(newLensFacing) - } - }, - setFlash = { flashMode -> - viewModelScope.launch { - // apply to cameraSystem - cameraSystem.setFlashMode(flashMode) - } - }, - setAspectRatio = { aspectRatio -> - viewModelScope.launch { - cameraSystem.setAspectRatio(aspectRatio) - } - }, - setStreamConfig = { streamConfig -> - viewModelScope.launch { - cameraSystem.setStreamConfig(streamConfig) - } - }, - setDynamicRange = { dynamicRange -> - if (externalCaptureMode != ExternalCaptureMode.ImageCapture && - externalCaptureMode != ExternalCaptureMode.MultipleImageCapture - ) { - viewModelScope.launch { - cameraSystem.setDynamicRange(dynamicRange) - } - } - }, - setImageFormat = { imageFormat -> - if (externalCaptureMode != ExternalCaptureMode.VideoCapture) { - viewModelScope.launch { - cameraSystem.setImageFormat(imageFormat) - } - } - }, - setConcurrentCameraMode = { concurrentCameraMode -> - viewModelScope.launch { - cameraSystem.setConcurrentCameraMode(concurrentCameraMode) - } - }, - setCaptureMode = { captureMode -> - viewModelScope.launch { - cameraSystem.setCaptureMode(captureMode) - } - } - ) -} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt index c182c4e5e..e4e4adf13 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt @@ -42,6 +42,7 @@ import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_RATIO_BUTTO import com.google.jetpackcamera.ui.components.capture.QUICK_SETTINGS_STREAM_CONFIG_BUTTON import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.SETTINGS_BUTTON +import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsController import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickFlipCamera import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickNavSettings import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.QuickSetConcurrentCamera @@ -72,32 +73,23 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.QuickSettingsUiState fun QuickSettingsBottomSheet( modifier: Modifier = Modifier, quickSettingsUiState: QuickSettingsUiState, - toggleQuickSettings: () -> Unit, - onSetFocusedSetting: (FocusedQuickSetting) -> Unit, onNavigateToSettings: () -> Unit, - onLensFaceClick: (lensFace: LensFacing) -> Unit, - onFlashModeClick: (flashMode: FlashMode) -> Unit, - onAspectRatioClick: (aspectRation: AspectRatio) -> Unit, - onStreamConfigClick: (streamConfig: StreamConfig) -> Unit, - onDynamicRangeClick: (dynamicRange: DynamicRange) -> Unit, - onImageOutputFormatClick: (imageOutputFormat: ImageOutputFormat) -> Unit, - onConcurrentCameraModeClick: (concurrentCameraMode: ConcurrentCameraMode) -> Unit, - onCaptureModeClick: (CaptureMode) -> Unit + quickSettingsController: QuickSettingsController ) { if (quickSettingsUiState is QuickSettingsUiState.Available) { - val onUnFocus = { onSetFocusedSetting(FocusedQuickSetting.NONE) } + val onUnFocus = { quickSettingsController.setFocusedSetting(FocusedQuickSetting.NONE) } val displayedQuickSettings: List<@Composable () -> Unit> = when (quickSettingsUiState.focusedQuickSetting) { FocusedQuickSetting.ASPECT_RATIO -> focusedRatioButtons( onUnFocus = onUnFocus, - onSetAspectRatio = onAspectRatioClick, + onSetAspectRatio = quickSettingsController::setAspectRatio, aspectRatioUiState = quickSettingsUiState.aspectRatioUiState ) FocusedQuickSetting.CAPTURE_MODE -> focusedCaptureModeButtons( onUnFocus = onUnFocus, - onSetCaptureMode = onCaptureModeClick, + onSetCaptureMode = quickSettingsController::setCaptureMode, captureModeUiState = quickSettingsUiState.captureModeUiState ) @@ -107,7 +99,7 @@ fun QuickSettingsBottomSheet( add { QuickSetFlash( modifier = Modifier.testTag(QUICK_SETTINGS_FLASH_BUTTON), - onClick = { f: FlashMode -> onFlashModeClick(f) }, + onClick = { f: FlashMode -> quickSettingsController.setFlash(f) }, flashModeUiState = quickSettingsUiState.flashModeUiState ) } @@ -123,7 +115,9 @@ fun QuickSettingsBottomSheet( .testTag(BTN_QUICK_SETTINGS_FOCUS_CAPTURE_MODE) .semantics { description?.let { stateDescription = it } }, setCaptureMode = { - onSetFocusedSetting(FocusedQuickSetting.CAPTURE_MODE) + quickSettingsController.setFocusedSetting( + FocusedQuickSetting.CAPTURE_MODE + ) }, captureModeUiState = quickSettingsUiState.captureModeUiState ) @@ -132,7 +126,8 @@ fun QuickSettingsBottomSheet( add { QuickFlipCamera( modifier = Modifier.testTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON), - setLensFacing = { l: LensFacing -> onLensFaceClick(l) }, + setLensFacing = { l: LensFacing -> + quickSettingsController.setLensFacing(l) }, flipLensUiState = quickSettingsUiState.flipLensUiState ) } @@ -141,7 +136,7 @@ fun QuickSettingsBottomSheet( ToggleFocusedQuickSetRatio( modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_BUTTON), setRatio = { - onSetFocusedSetting(FocusedQuickSetting.ASPECT_RATIO) + quickSettingsController.setFocusedSetting(FocusedQuickSetting.ASPECT_RATIO) }, isHighlightEnabled = false, aspectRatioUiState = quickSettingsUiState.aspectRatioUiState @@ -153,7 +148,7 @@ fun QuickSettingsBottomSheet( modifier = Modifier.testTag( QUICK_SETTINGS_STREAM_CONFIG_BUTTON ), - setStreamConfig = { c: StreamConfig -> onStreamConfigClick(c) }, + setStreamConfig = { c: StreamConfig -> quickSettingsController.setStreamConfig(c) }, streamConfigUiState = quickSettingsUiState.streamConfigUiState ) } @@ -162,8 +157,8 @@ fun QuickSettingsBottomSheet( QuickSetHdr( modifier = Modifier.testTag(QUICK_SETTINGS_HDR_BUTTON), onClick = { d: DynamicRange, i: ImageOutputFormat -> - onDynamicRangeClick(d) - onImageOutputFormatClick(i) + quickSettingsController.setDynamicRange(d) + quickSettingsController.setImageFormat(i) }, hdrUiState = quickSettingsUiState.hdrUiState ) @@ -174,7 +169,7 @@ fun QuickSettingsBottomSheet( modifier = Modifier.testTag(QUICK_SETTINGS_CONCURRENT_CAMERA_MODE_BUTTON), setConcurrentCameraMode = { c: ConcurrentCameraMode -> - onConcurrentCameraModeClick(c) + quickSettingsController.setConcurrentCameraMode(c) }, concurrentCameraUiState = quickSettingsUiState .concurrentCameraUiState @@ -198,7 +193,7 @@ fun QuickSettingsBottomSheet( modifier = modifier, onDismiss = { onUnFocus() - toggleQuickSettings() + quickSettingsController.toggleQuickSettings() }, sheetState = sheetState, *displayedQuickSettings.toTypedArray() @@ -268,17 +263,8 @@ fun ExpandedQuickSettingsUiPreview() { ), quickSettingsIsOpen = true ), - onLensFaceClick = { }, - onFlashModeClick = { }, - onAspectRatioClick = { }, - onStreamConfigClick = { }, - onDynamicRangeClick = { }, - onImageOutputFormatClick = { }, - onConcurrentCameraModeClick = { }, - toggleQuickSettings = { }, - onNavigateToSettings = { }, - onCaptureModeClick = { }, - onSetFocusedSetting = {} + onNavigateToSettings = {}, + quickSettingsController = MockQuickSettingsController() ) } } @@ -339,17 +325,31 @@ fun ExpandedQuickSettingsUiPreview_WithHdr() { ), quickSettingsIsOpen = true ), - onLensFaceClick = { }, - onFlashModeClick = { }, - onAspectRatioClick = { }, - onStreamConfigClick = { }, - onDynamicRangeClick = { }, - onImageOutputFormatClick = { }, - onConcurrentCameraModeClick = { }, - toggleQuickSettings = { }, onNavigateToSettings = { }, - onCaptureModeClick = { }, - onSetFocusedSetting = {} + quickSettingsController = MockQuickSettingsController() ) } } + +class MockQuickSettingsController : QuickSettingsController { + override fun toggleQuickSettings() {} + + override fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) {} + + override fun setLensFacing(lensFace: LensFacing) {} + + override fun setFlash(flashMode: FlashMode) {} + + override fun setAspectRatio(aspectRatio: AspectRatio) {} + + override fun setStreamConfig(streamConfig: StreamConfig) {} + + override fun setDynamicRange(dynamicRange: DynamicRange) {} + + override fun setImageFormat(imageOutputFormat: ImageOutputFormat) {} + + override fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) {} + + override fun setCaptureMode(captureMode: CaptureMode) {} + +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt new file mode 100644 index 000000000..06ddbd416 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt @@ -0,0 +1,41 @@ +/* + * 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.components.capture.quicksettings.controller + +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting + +interface QuickSettingsController { + fun toggleQuickSettings() + fun setFocusedSetting (focusedQuickSetting: FocusedQuickSetting) + fun setLensFacing(lensFace: LensFacing) + fun setFlash(flashMode: FlashMode) + fun setAspectRatio(aspectRatio: AspectRatio) + fun setStreamConfig(streamConfig: StreamConfig) + fun setDynamicRange(dynamicRange: DynamicRange) + fun setImageFormat(imageOutputFormat: ImageOutputFormat) + fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) + fun setCaptureMode(captureMode: CaptureMode) + +} \ No newline at end of file diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt new file mode 100644 index 000000000..b18a6d000 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt @@ -0,0 +1,109 @@ +/* + * 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.components.capture.quicksettings.controller + +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.model.AspectRatio +import com.google.jetpackcamera.model.CaptureMode +import com.google.jetpackcamera.model.ConcurrentCameraMode +import com.google.jetpackcamera.model.DynamicRange +import com.google.jetpackcamera.model.ExternalCaptureMode +import com.google.jetpackcamera.model.FlashMode +import com.google.jetpackcamera.model.ImageOutputFormat +import com.google.jetpackcamera.model.LensFacing +import com.google.jetpackcamera.model.StreamConfig +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class QuickSettingsControllerImpl( + private val trackedCaptureUiState: MutableStateFlow, + private val viewModelScope: CoroutineScope, + private val cameraSystem: CameraSystem, + private val externalCaptureMode: ExternalCaptureMode +) : QuickSettingsController { + override fun toggleQuickSettings() { + trackedCaptureUiState.update { old -> + old.copy(isQuickSettingsOpen = !old.isQuickSettingsOpen) + } + } + + override fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) { + trackedCaptureUiState.update { old -> + old.copy(focusedQuickSetting = focusedQuickSetting) + } + } + + override fun setLensFacing(lensFace: LensFacing) { + viewModelScope.launch { + // apply to cameraSystem + cameraSystem.setLensFacing(lensFace) + } + } + + override fun setFlash(flashMode: FlashMode) { + viewModelScope.launch { + // apply to cameraSystem + cameraSystem.setFlashMode(flashMode) + } + } + + override fun setAspectRatio(aspectRatio: AspectRatio) { + viewModelScope.launch { + cameraSystem.setAspectRatio(aspectRatio) + } + } + + override fun setStreamConfig(streamConfig: StreamConfig) { + viewModelScope.launch { + cameraSystem.setStreamConfig(streamConfig) + } + } + + override fun setDynamicRange(dynamicRange: DynamicRange) { + if (externalCaptureMode != ExternalCaptureMode.ImageCapture && + externalCaptureMode != ExternalCaptureMode.MultipleImageCapture + ) { + viewModelScope.launch { + cameraSystem.setDynamicRange(dynamicRange) + } + } + } + + override fun setImageFormat(imageOutputFormat: ImageOutputFormat) { + if (externalCaptureMode != ExternalCaptureMode.VideoCapture) { + viewModelScope.launch { + cameraSystem.setImageFormat(imageOutputFormat) + } + } + } + + override fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) { + viewModelScope.launch { + cameraSystem.setConcurrentCameraMode(concurrentCameraMode) + } + } + + override fun setCaptureMode(captureMode: CaptureMode) { + viewModelScope.launch { + cameraSystem.setCaptureMode(captureMode) + } + } +} \ No newline at end of file diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt index aa03ac926..538daeb16 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/ui/QuickSettingsComponents.kt @@ -100,6 +100,7 @@ import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraFlashM import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraLensFace import com.google.jetpackcamera.ui.components.capture.quicksettings.CameraStreamConfig import com.google.jetpackcamera.ui.components.capture.quicksettings.QuickSettingsEnum +import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsController import com.google.jetpackcamera.ui.uistate.SingleSelectableUiState import com.google.jetpackcamera.ui.uistate.capture.AspectRatioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeUiState @@ -469,9 +470,9 @@ fun QuickSetConcurrentCamera( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ToggleQuickSettingsButton( - toggleBottomSheet: () -> Unit, isOpen: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + quickSettingsController: QuickSettingsController ) { val buttonSize = IconButtonDefaults.mediumContainerSize( IconButtonDefaults.IconButtonWidthOption.Narrow @@ -490,7 +491,7 @@ fun ToggleQuickSettingsButton( closedDescription } }, - onClick = toggleBottomSheet, + onClick = quickSettingsController::toggleQuickSettings, colors = IconButtonDefaults.iconButtonColors( // Set the background color of the button containerColor = Color.White.copy(alpha = 0.08f), diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt index 206a3f593..ee7535947 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt @@ -15,8 +15,8 @@ */ package com.google.jetpackcamera.ui.uistateadapter.capture -import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState -import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import com.google.jetpackcamera.ui.uistate.SnackBarUiState +import com.google.jetpackcamera.ui.uistate.SnackbarData import java.util.Queue fun SnackBarUiState.Companion.from(snackBarQueue: Queue): SnackBarUiState { From 07f7ba37f7c3e7c0d946aec371a51f8a2ffef911 Mon Sep 17 00:00:00 2001 From: David Jia Date: Tue, 24 Feb 2026 16:41:40 -0800 Subject: [PATCH 3/6] camera, capturescreen, capture controllers --- .../postcapture/PostCaptureViewModel.kt | 2 - .../feature/preview/PreviewScreen.kt | 89 ++--- .../feature/preview/PreviewViewModel.kt | 373 +++--------------- .../feature/preview/PreviewViewModelTest.kt | 12 +- .../ui/components/capture/CaptureCallbacks.kt | 165 -------- .../capture/CaptureScreenComponents.kt | 5 +- .../capture/MultipleEventsCutter.kt | 1 - .../components/capture/SnackBarCallbacks.kt | 1 - .../components/capture/SnackBarController.kt | 3 +- .../capture/SnackBarControllerImpl.kt | 7 +- .../ui/components/capture/ZoomState.kt | 14 +- .../capture/controller/CameraController.kt | 21 + .../controller/CameraControllerImpl.kt | 78 ++++ .../capture/controller/CaptureController.kt | 25 ++ .../controller/CaptureControllerImpl.kt | 298 ++++++++++++++ .../controller/CaptureScreenController.kt | 27 ++ .../controller/CaptureScreenControllerImpl.kt | 94 +++++ .../ui/components/capture/controller/Utils.kt | 68 ++++ .../capture/controller/ZoomController.kt | 23 ++ .../capture/controller/ZoomControllerImpl.kt | 39 ++ .../capture/debug/DebugOverlayComponents.kt | 1 - .../debug/controller/DebugController.kt | 1 - .../debug/controller/DebugControllerImpl.kt | 5 +- .../quicksettings/QuickSettingsScreen.kt | 12 +- .../controller/QuickSettingsController.kt | 6 +- .../controller/QuickSettingsControllerImpl.kt | 3 +- .../capture/SnackBarUiStateAdapter.kt | 4 +- 27 files changed, 807 insertions(+), 570 deletions(-) delete mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt create mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt diff --git a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt index 9f819da7e..338424159 100644 --- a/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt +++ b/feature/postcapture/src/main/java/com/google/jetpackcamera/feature/postcapture/PostCaptureViewModel.kt @@ -47,9 +47,7 @@ import com.google.jetpackcamera.ui.uistate.postcapture.ShareButtonUiState import com.google.jetpackcamera.ui.uistateadapter.postcapture.from import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import java.util.LinkedList import javax.inject.Inject -import kotlinx.atomicfu.atomic import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index 614047d2e..25184bb10 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -16,7 +16,6 @@ package com.google.jetpackcamera.feature.preview import android.Manifest -import android.content.ContentResolver import android.os.Build import android.util.Log import android.util.Range @@ -92,6 +91,8 @@ import com.google.jetpackcamera.ui.components.capture.VIDEO_QUALITY_TAG import com.google.jetpackcamera.ui.components.capture.VideoQualityIcon import com.google.jetpackcamera.ui.components.capture.ZoomButtonRow import com.google.jetpackcamera.ui.components.capture.ZoomState +import com.google.jetpackcamera.ui.components.capture.controller.CaptureController +import com.google.jetpackcamera.ui.components.capture.controller.CaptureScreenController import com.google.jetpackcamera.ui.components.capture.debouncedOrientationFlow import com.google.jetpackcamera.ui.components.capture.debug.DebugOverlay import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugController @@ -100,7 +101,6 @@ import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.Q import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.FlashModeIndicator import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.HdrIndicator import com.google.jetpackcamera.ui.components.capture.quicksettings.ui.ToggleQuickSettingsButton -import com.google.jetpackcamera.ui.uistate.DisableRationale import com.google.jetpackcamera.ui.uistate.capture.AudioUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureButtonUiState import com.google.jetpackcamera.ui.uistate.capture.CaptureModeToggleUiState @@ -145,9 +145,9 @@ fun PreviewScreen( by viewModel.surfaceRequest.collectAsState() LifecycleStartEffect(Unit) { - viewModel.startCamera() + viewModel.cameraController.startCamera() onStopOrDispose { - viewModel.stopCamera() + viewModel.cameraController.stopCamera() } } @@ -193,7 +193,7 @@ fun PreviewScreen( val context = LocalContext.current LaunchedEffect(Unit) { debouncedOrientationFlow(context).collect( - viewModel.captureCallbacks.setDisplayRotation + viewModel.captureScreenController::setDisplayRotation ) } val scope = rememberCoroutineScope() @@ -209,11 +209,10 @@ fun PreviewScreen( ) ?.initialZoomRatio ?: 1f, - onAnimateStateChanged = viewModel.captureCallbacks.setZoomAnimationState, - onChangeZoomLevel = viewModel.captureCallbacks.changeZoomRatio, zoomRange = (currentUiState.zoomUiState as? ZoomUiState.Enabled) ?.primaryZoomRange - ?: Range(1f, 1f) + ?: Range(1f, 1f), + zoomController = viewModel.zoomController ) } @@ -247,8 +246,10 @@ fun PreviewScreen( val oldZoomRatios = it.zoomRatios val oldAudioEnabled = it.isAudioEnabled Log.d(TAG, "reset pre recording settings") - viewModel.captureCallbacks.setAudioEnabled(oldAudioEnabled) - viewModel.quickSettingsController.setLensFacing(oldPrimaryLensFacing) + viewModel.captureScreenController.setAudioEnabled(oldAudioEnabled) + viewModel.quickSettingsController.setLensFacing( + oldPrimaryLensFacing + ) zoomState.apply { absoluteZoom( targetZoomLevel = oldZoomRatios[oldPrimaryLensFacing] ?: 1f, @@ -276,8 +277,6 @@ fun PreviewScreen( surfaceRequest = surfaceRequest, onNavigateToSettings = onNavigateToSettings, onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness, - onTapToFocus = viewModel.captureCallbacks.tapToFocus, - onSetImageWell = viewModel.captureCallbacks.imageWellToRepository, onAbsoluteZoom = { zoomRatio: Float, lensToZoom: LensToZoom -> scope.launch { zoomState.absoluteZoom( @@ -310,18 +309,15 @@ fun PreviewScreen( ) } }, - onSetPause = viewModel.captureCallbacks.setPaused, - onSetAudioEnabled = viewModel.captureCallbacks.setAudioEnabled, - onCaptureImage = viewModel::captureImage, - onStartVideoRecording = viewModel::startVideoRecording, - onStopVideoRecording = viewModel::stopVideoRecording, - onLockVideoRecording = viewModel.captureCallbacks.setLockedRecording, onRequestWindowColorMode = onRequestWindowColorMode, onNavigatePostCapture = onNavigateToPostCapture, debugUiState = debugUiState, snackBarUiState = snackBarUiState, debugController = viewModel.debugController, - snackBarController = viewModel.snackBarController + snackBarController = viewModel.snackBarController, + quickSettingsController = viewModel.quickSettingsController, + captureScreenController = viewModel.captureScreenController, + captureController = viewModel.captureController ) val readStoragePermission: PermissionState = rememberPermissionState( Manifest.permission.READ_EXTERNAL_STORAGE @@ -331,7 +327,7 @@ fun PreviewScreen( if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || readStoragePermission.status.isGranted ) { - viewModel.captureCallbacks.updateLastCapturedMedia() + viewModel.captureScreenController.updateLastCapturedMedia() } } } @@ -347,32 +343,26 @@ private fun ContentScreen( modifier: Modifier = Modifier, onNavigateToSettings: () -> Unit = {}, onClearUiScreenBrightness: (Float) -> Unit = {}, - onTapToFocus: (x: Float, y: Float) -> Unit = { _, _ -> }, - onSetImageWell: () -> Unit = {}, onAbsoluteZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, onScaleZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, onIncrementZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, onAnimateZoom: (Float, LensToZoom) -> Unit = { _, _ -> }, - onSetPause: (Boolean) -> Unit = {}, - onSetAudioEnabled: (Boolean) -> Unit = {}, - onCaptureImage: (ContentResolver) -> Unit = {}, - onStartVideoRecording: () -> Unit = {}, - onStopVideoRecording: () -> Unit = {}, - onLockVideoRecording: (Boolean) -> Unit = {}, onRequestWindowColorMode: (Int) -> Unit = {}, onNavigatePostCapture: () -> Unit = {}, debugUiState: DebugUiState = DebugUiState.Disabled, snackBarUiState: SnackBarUiState = SnackBarUiState.Disabled, debugController: DebugController? = null, quickSettingsController: QuickSettingsController? = null, - snackBarController: SnackBarController? = null + snackBarController: SnackBarController? = null, + captureScreenController: CaptureScreenController? = null, + captureController: CaptureController? = null ) { val onFlipCamera = { if (captureUiState.flipLensUiState is FlipLensUiState.Available) { quickSettingsController?.setLensFacing( ( - captureUiState.flipLensUiState as FlipLensUiState.Available - ) + captureUiState.flipLensUiState as FlipLensUiState.Available + ) .selectedLensFacing.flip() ) } @@ -381,9 +371,9 @@ private fun ContentScreen( val isAudioEnabled = remember(captureUiState) { captureUiState.audioUiState is AudioUiState.Enabled.On } - val onToggleAudio = remember(isAudioEnabled) { + val onToggleAudio: () -> Unit = remember(isAudioEnabled) { { - onSetAudioEnabled(!isAudioEnabled) + captureScreenController?.setAudioEnabled(!isAudioEnabled) } } @@ -413,7 +403,11 @@ private fun ContentScreen( PreviewDisplay( previewDisplayUiState = captureUiState.previewDisplayUiState, onFlipCamera = onFlipCamera, - onTapToFocus = onTapToFocus, + onTapToFocus = if (captureScreenController != null) { + captureScreenController::tapToFocus + } else { + { _, _ -> } + }, onScaleZoom = { onScaleZoom(it, LensToZoom.PRIMARY) }, surfaceRequest = surfaceRequest, onRequestWindowColorMode = onRequestWindowColorMode, @@ -437,7 +431,7 @@ private fun ContentScreen( )?.quickSettingsIsOpen ?: false, onCaptureImage = { runCaptureAction { - onCaptureImage(it) + captureController?.captureImage(it) } }, onIncrementZoom = { targetZoom -> @@ -445,12 +439,15 @@ private fun ContentScreen( }, onStartVideoRecording = { runCaptureAction { - onStartVideoRecording() + captureController?.startVideoRecording() } }, - onStopVideoRecording = - onStopVideoRecording, - onLockVideoRecording = onLockVideoRecording + onStopVideoRecording = { captureController?.stopVideoRecording() }, + onLockVideoRecording = { isLocked -> + captureController?.setLockedRecording( + isLocked + ) + } ) }, flipCameraButton = { @@ -497,9 +494,9 @@ private fun ContentScreen( ToggleQuickSettingsButton( modifier = it, isOpen = ( - captureUiState.quickSettingsUiState - as? QuickSettingsUiState.Available - )?.quickSettingsIsOpen == true, + captureUiState.quickSettingsUiState + as? QuickSettingsUiState.Available + )?.quickSettingsIsOpen == true, quickSettingsController = quickSettingsController ) @@ -585,7 +582,11 @@ private fun ContentScreen( }, pauseToggleButton = { PauseResumeToggleButton( - onSetPause = onSetPause, + onSetPause = if (captureScreenController != null) { + captureScreenController::setPaused + } else { + { _ -> } + }, currentRecordingState = captureUiState.videoRecordingState ) }, @@ -596,7 +597,7 @@ private fun ContentScreen( modifier = modifier, imageWellUiState = it, onClick = { - onSetImageWell() + captureScreenController?.imageWellToRepository() onNavigatePostCapture() } ) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index 9c7570298..bf0b37ecc 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -15,22 +15,15 @@ */ package com.google.jetpackcamera.feature.preview -import android.content.ContentResolver import android.net.Uri -import android.os.SystemClock import android.util.Log import androidx.camera.core.SurfaceRequest import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.tracing.Trace -import androidx.tracing.traceAsync import com.google.jetpackcamera.core.camera.CameraSystem import com.google.jetpackcamera.core.camera.CameraSystem.Companion.applyDiffs -import com.google.jetpackcamera.core.camera.OnVideoRecordEvent import com.google.jetpackcamera.core.common.DefaultSaveMode -import com.google.jetpackcamera.core.common.traceFirstFramePreview -import com.google.jetpackcamera.data.media.MediaDescriptor import com.google.jetpackcamera.data.media.MediaRepository import com.google.jetpackcamera.feature.preview.navigation.getCaptureUris import com.google.jetpackcamera.feature.preview.navigation.getDebugSettings @@ -39,32 +32,29 @@ import com.google.jetpackcamera.feature.preview.navigation.getRequestedSaveMode import com.google.jetpackcamera.model.CaptureEvent import com.google.jetpackcamera.model.DebugSettings import com.google.jetpackcamera.model.ExternalCaptureMode -import com.google.jetpackcamera.model.ImageCaptureEvent import com.google.jetpackcamera.model.IntProgress import com.google.jetpackcamera.model.LowLightBoostState import com.google.jetpackcamera.model.SaveLocation import com.google.jetpackcamera.model.SaveMode -import com.google.jetpackcamera.model.VideoCaptureEvent import com.google.jetpackcamera.settings.ConstraintsRepository import com.google.jetpackcamera.settings.SettingsRepository import com.google.jetpackcamera.settings.model.CameraAppSettings import com.google.jetpackcamera.settings.model.applyExternalCaptureMode -import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG -import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_FAILURE_TAG -import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.ui.components.capture.LOW_LIGHT_BOOST_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.ScreenFlash import com.google.jetpackcamera.ui.components.capture.SnackBarController import com.google.jetpackcamera.ui.components.capture.SnackBarControllerImpl -import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG -import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG -import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG +import com.google.jetpackcamera.ui.components.capture.controller.CameraController +import com.google.jetpackcamera.ui.components.capture.controller.CameraControllerImpl +import com.google.jetpackcamera.ui.components.capture.controller.CaptureController +import com.google.jetpackcamera.ui.components.capture.controller.CaptureControllerImpl +import com.google.jetpackcamera.ui.components.capture.controller.CaptureScreenController +import com.google.jetpackcamera.ui.components.capture.controller.CaptureScreenControllerImpl +import com.google.jetpackcamera.ui.components.capture.controller.ZoomController +import com.google.jetpackcamera.ui.components.capture.controller.ZoomControllerImpl import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugController import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugControllerImpl -import com.google.jetpackcamera.ui.components.capture.getCaptureCallbacks -import com.google.jetpackcamera.ui.components.capture.getSnackBarCallbacks -import com.google.jetpackcamera.ui.components.capture.postCurrentMediaToMediaRepository import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsController import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsControllerImpl import com.google.jetpackcamera.ui.uistate.capture.DebugUiState @@ -75,11 +65,8 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState import com.google.jetpackcamera.ui.uistateadapter.capture.compound.captureUiState import com.google.jetpackcamera.ui.uistateadapter.capture.debugUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.atomicfu.atomic import javax.inject.Inject -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel @@ -91,12 +78,9 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformWhile -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch private const val TAG = "PreviewViewModel" -private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture" /** * [ViewModel] for [PreviewScreen]. @@ -111,7 +95,7 @@ class PreviewViewModel @Inject constructor( private val mediaRepository: MediaRepository ) : ViewModel() { private val saveMode: SaveMode = savedStateHandle.getRequestedSaveMode() ?: defaultSaveMode - private val trackedCaptureUiState: MutableStateFlow = + private val _trackedCaptureUiState: MutableStateFlow = MutableStateFlow(TrackedCaptureUiState()) private val _snackBarUiState: MutableStateFlow = MutableStateFlow(SnackBarUiState.Enabled()) @@ -123,10 +107,6 @@ class PreviewViewModel @Inject constructor( private val _captureEvents = Channel() val captureEvents: ReceiveChannel = _captureEvents - private var runningCameraJob: Job? = null - - private var recordingJob: Job? = null - private val externalCaptureMode: ExternalCaptureMode = savedStateHandle.getExternalCaptureMode() private val externalUris: List = savedStateHandle.getCaptureUris() private lateinit var externalUriProgress: IntProgress @@ -137,8 +117,6 @@ class PreviewViewModel @Inject constructor( val screenFlash = ScreenFlash(cameraSystem, viewModelScope) - private val videoCaptureStartedCount = atomic(0) - // Eagerly initialize the CameraSystem and encapsulate in a Deferred that can be // used to ensure we don't start the camera before initialization is complete. private var initializationDeferred: Deferred = viewModelScope.async { @@ -152,7 +130,7 @@ class PreviewViewModel @Inject constructor( val captureUiState: StateFlow = captureUiState( cameraSystem, constraintsRepository, - trackedCaptureUiState, + _trackedCaptureUiState, externalCaptureMode ) .stateIn( @@ -165,7 +143,7 @@ class PreviewViewModel @Inject constructor( constraintsRepository, debugSettings, cameraPropertiesJSON, - trackedCaptureUiState + _trackedCaptureUiState ) .stateIn( scope = viewModelScope, @@ -174,7 +152,7 @@ class PreviewViewModel @Inject constructor( ) val quickSettingsController: QuickSettingsController = QuickSettingsControllerImpl( - trackedCaptureUiState = trackedCaptureUiState, + trackedCaptureUiState = _trackedCaptureUiState, viewModelScope = viewModelScope, cameraSystem = cameraSystem, externalCaptureMode = externalCaptureMode @@ -182,25 +160,60 @@ class PreviewViewModel @Inject constructor( val debugController: DebugController = DebugControllerImpl( cameraSystem = cameraSystem, - trackedCaptureUiState = trackedCaptureUiState + trackedCaptureUiState = _trackedCaptureUiState ) - val snackBarCallbacks = getSnackBarCallbacks( + val snackBarController: SnackBarController = SnackBarControllerImpl( viewModelScope = viewModelScope, snackBarUiState = _snackBarUiState ) - val snackBarController: SnackBarController = SnackBarControllerImpl( + val captureScreenController: CaptureScreenController = CaptureScreenControllerImpl( viewModelScope = viewModelScope, - snackBarUiState = _snackBarUiState + cameraSystem = cameraSystem, + trackedCaptureUiState = _trackedCaptureUiState, + mediaRepository = mediaRepository, + captureUiState = captureUiState + ) + + val zoomController: ZoomController = ZoomControllerImpl( + cameraSystem = cameraSystem, + trackedCaptureUiState = _trackedCaptureUiState ) - val captureCallbacks = getCaptureCallbacks( + val cameraController: CameraController = CameraControllerImpl( + initializationDeferred = initializationDeferred, + captureUiState = captureUiState, + viewModelScope = viewModelScope, + cameraSystem = cameraSystem + ) + + val captureController: CaptureController = CaptureControllerImpl( + trackedCaptureUiState = _trackedCaptureUiState, + captureUiState = captureUiState, viewModelScope = viewModelScope, cameraSystem = cameraSystem, - trackedCaptureUiState = trackedCaptureUiState, mediaRepository = mediaRepository, - captureUiState = captureUiState + saveMode = saveMode, + externalCaptureMode = externalCaptureMode, + externalCapturesCallback = { + if (externalUris.isNotEmpty()) { + if (!this::externalUriProgress.isInitialized) { + externalUriProgress = IntProgress(1, 1..externalUris.size) + } + val progress = externalUriProgress + if (progress.currentValue < progress.range.endInclusive) externalUriProgress++ + Pair( + SaveLocation.Explicit(externalUris[progress.currentValue - 1]), + progress + ) + } else { + Pair(SaveLocation.Default, null) + } + }, + captureEvents = _captureEvents, + captureScreenController = captureScreenController, + snackBarController = snackBarController ) init { @@ -237,282 +250,4 @@ class PreviewViewModel @Inject constructor( } } } - - fun startCamera() { - Log.d(TAG, "startCamera") - stopCamera() - runningCameraJob = viewModelScope.launch { - if (Trace.isEnabled()) { - launch(start = CoroutineStart.UNDISPATCHED) { - val startTraceTimestamp: Long = SystemClock.elapsedRealtimeNanos() - traceFirstFramePreview(cookie = 1) { - captureUiState.transformWhile { - var continueCollecting = true - (it as? CaptureUiState.Ready)?.let { uiState -> - if (uiState.sessionFirstFrameTimestamp > startTraceTimestamp) { - emit(Unit) - continueCollecting = false - } - } - continueCollecting - }.collect {} - } - } - } - // Ensure CameraSystem is initialized before starting camera - initializationDeferred.await() - // TODO(yasith): Handle Exceptions from binding use cases - cameraSystem.runCamera() - } - } - - fun stopCamera() { - Log.d(TAG, "stopCamera") - runningCameraJob?.apply { - if (isActive) { - cancel() - } - } - } - - private fun nextSaveLocation(saveMode: SaveMode): Pair { - return when (externalCaptureMode) { - ExternalCaptureMode.ImageCapture, - ExternalCaptureMode.MultipleImageCapture, - ExternalCaptureMode.VideoCapture -> { - if (externalUris.isNotEmpty()) { - if (!this::externalUriProgress.isInitialized) { - externalUriProgress = IntProgress(1, 1..externalUris.size) - } - val progress = externalUriProgress - if (progress.currentValue < progress.range.endInclusive) externalUriProgress++ - Pair( - SaveLocation.Explicit(externalUris[progress.currentValue - 1]), - progress - ) - } else { - Pair(SaveLocation.Default, null) - } - } - - ExternalCaptureMode.Standard -> { - val defaultSaveLocation = - if (saveMode is SaveMode.CacheAndReview) { - SaveLocation.Cache(saveMode.cacheDir) - } else { - SaveLocation.Default - } - Pair(defaultSaveLocation, null) - } - } - } - - fun captureImage(contentResolver: ContentResolver) { - if (captureUiState.value is CaptureUiState.Ready && - (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == - ExternalCaptureMode.VideoCapture - ) { - snackBarController.addSnackBarData( - SnackbarData( - cookie = "Image-ExternalVideoCaptureMode", - stringResource = R.string.toast_image_capture_external_unsupported, - withDismissAction = true, - testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG - ) - ) - return - } - - if (captureUiState.value is CaptureUiState.Ready && - (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == - ExternalCaptureMode.VideoCapture - ) { - snackBarController.addSnackBarData( - SnackbarData( - cookie = "Image-ExternalVideoCaptureMode", - stringResource = R.string.toast_image_capture_external_unsupported, - withDismissAction = true, - testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG - ) - ) - return - } - Log.d(TAG, "captureImage") - viewModelScope.launch { - val (saveLocation, progress) = nextSaveLocation(saveMode) - captureImageInternal( - saveLocation = saveLocation, - doTakePicture = { - cameraSystem.takePicture(contentResolver, saveLocation) { - trackedCaptureUiState.update { old -> - old.copy(lastBlinkTimeStamp = System.currentTimeMillis()) - } - }.savedUri - }, - onSuccess = { savedUri -> - val event = if (progress != null) { - ImageCaptureEvent.SequentialImageSaved(savedUri, progress) - } else { - if (saveLocation is SaveLocation.Cache) { - ImageCaptureEvent.SingleImageCached(savedUri) - } else { - ImageCaptureEvent.SingleImageSaved(savedUri) - } - } - if (saveLocation !is SaveLocation.Cache) { - captureCallbacks.updateLastCapturedMedia() - } else { - savedUri?.let { - postCurrentMediaToMediaRepository( - viewModelScope, - mediaRepository, - MediaDescriptor.Content.Image(it, null, true) - ) - } - } - _captureEvents.trySend(event) - }, - onFailure = { exception -> - val event = if (progress != null) { - ImageCaptureEvent.SequentialImageCaptureError(exception, progress) - } else { - ImageCaptureEvent.SingleImageCaptureError(exception) - } - - _captureEvents.trySend(event) - } - ) - } - } - - private suspend fun captureImageInternal( - saveLocation: SaveLocation, - doTakePicture: suspend () -> T, - onSuccess: (T) -> Unit = {}, - onFailure: (exception: Exception) -> Unit = {} - ) { - val cookieInt = snackBarController.incrementAndGetSnackBarCount() - val cookie = "Image-$cookieInt" - val snackBarData = try { - traceAsync(IMAGE_CAPTURE_TRACE, cookieInt) { - doTakePicture() - }.also { result -> - onSuccess(result) - } - Log.d(TAG, "cameraSystem.takePicture success") - // don't display snackbar for successful capture - if (saveLocation is SaveLocation.Cache) { - null - } else { - SnackbarData( - cookie = cookie, - stringResource = R.string.toast_image_capture_success, - withDismissAction = true, - testTag = IMAGE_CAPTURE_SUCCESS_TAG - ) - } - } catch (exception: Exception) { - onFailure(exception) - Log.d(TAG, "cameraSystem.takePicture error", exception) - SnackbarData( - cookie = cookie, - stringResource = R.string.toast_capture_failure, - withDismissAction = true, - testTag = IMAGE_CAPTURE_FAILURE_TAG - ) - } - snackBarData?.let { - snackBarController.addSnackBarData( - it - ) - } - } - - fun startVideoRecording() { - if (captureUiState.value is CaptureUiState.Ready && - (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == - ExternalCaptureMode.ImageCapture - ) { - Log.d(TAG, "externalVideoRecording") - snackBarController.addSnackBarData( - SnackbarData( - cookie = "Video-ExternalImageCaptureMode", - stringResource = R.string.toast_video_capture_external_unsupported, - withDismissAction = true, - testTag = VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG - ) - ) - return - } - Log.d(TAG, "startVideoRecording") - recordingJob = viewModelScope.launch { - val cookie = "Video-${videoCaptureStartedCount.incrementAndGet()}" - val saveMode = saveMode - val (saveLocation, _) = nextSaveLocation(saveMode) - try { - cameraSystem.startVideoRecording(saveLocation) { - var snackbarToShow: SnackbarData? - when (it) { - is OnVideoRecordEvent.OnVideoRecorded -> { - Log.d(TAG, "cameraSystem.startRecording OnVideoRecorded") - val event = if (saveLocation is SaveLocation.Cache) { - VideoCaptureEvent.VideoCached(it.savedUri) - } else { - VideoCaptureEvent.VideoSaved(it.savedUri) - } - - if (saveLocation !is SaveLocation.Cache) { - captureCallbacks.updateLastCapturedMedia() - } else { - postCurrentMediaToMediaRepository( - viewModelScope, - mediaRepository, - MediaDescriptor.Content.Video(it.savedUri, null, true) - ) - } - - _captureEvents.trySend(event) - // don't display snackbar for successful capture - snackbarToShow = if (saveLocation is SaveLocation.Cache) { - null - } else { - SnackbarData( - cookie = cookie, - stringResource = R.string.toast_video_capture_success, - withDismissAction = true, - testTag = VIDEO_CAPTURE_SUCCESS_TAG - ) - } - } - - is OnVideoRecordEvent.OnVideoRecordError -> { - Log.d(TAG, "cameraSystem.startRecording OnVideoRecordError") - _captureEvents.trySend(VideoCaptureEvent.VideoCaptureError(it.error)) - snackbarToShow = SnackbarData( - cookie = cookie, - stringResource = R.string.toast_video_capture_failure, - withDismissAction = true, - testTag = VIDEO_CAPTURE_FAILURE_TAG - ) - } - } - - snackbarToShow?.let { data -> - snackBarController.addSnackBarData(data) - } - } - Log.d(TAG, "cameraSystem.startRecording success") - } catch (exception: IllegalStateException) { - Log.d(TAG, "cameraSystem.startVideoRecording error", exception) - } - } - } - - fun stopVideoRecording() { - Log.d(TAG, "stopVideoRecording") - viewModelScope.launch { - cameraSystem.stopVideoRecording() - recordingJob?.cancel() - } - } } 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 f90737ee6..6742a8d28 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 @@ -87,7 +87,7 @@ class PreviewViewModelTest { val contentResolver: ContentResolver = ApplicationProvider.getApplicationContext().contentResolver startCameraUntilRunning() - previewViewModel.captureImage(contentResolver) + previewViewModel.captureController.captureImage(contentResolver) advanceUntilIdle() assertThat(cameraSystem.numPicturesTaken).isEqualTo(1) } @@ -95,7 +95,7 @@ class PreviewViewModelTest { @Test fun startVideoRecording() = runTest(StandardTestDispatcher()) { startCameraUntilRunning() - previewViewModel.startVideoRecording() + previewViewModel.captureController.startVideoRecording() advanceUntilIdle() assertThat(cameraSystem.recordingInProgress).isTrue() } @@ -103,16 +103,16 @@ class PreviewViewModelTest { @Test fun stopVideoRecording() = runTest(StandardTestDispatcher()) { startCameraUntilRunning() - previewViewModel.startVideoRecording() + previewViewModel.captureController.startVideoRecording() advanceUntilIdle() - previewViewModel.stopVideoRecording() + previewViewModel.captureController.stopVideoRecording() advanceUntilIdle() assertThat(cameraSystem.recordingInProgress).isFalse() } @Test fun setFlash() = runTest(StandardTestDispatcher()) { - previewViewModel.startCamera() + previewViewModel.cameraController.startCamera() previewViewModel.quickSettingsController.setFlash(FlashMode.AUTO) advanceUntilIdle() @@ -177,7 +177,7 @@ class PreviewViewModelTest { } private fun TestScope.startCameraUntilRunning() { - previewViewModel.startCamera() + previewViewModel.cameraController.startCamera() advanceUntilIdle() } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt deleted file mode 100644 index 03fbd525b..000000000 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureCallbacks.kt +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.components.capture - -import android.util.Log -import com.google.jetpackcamera.core.camera.CameraSystem -import com.google.jetpackcamera.data.media.MediaDescriptor -import com.google.jetpackcamera.data.media.MediaRepository -import com.google.jetpackcamera.model.CameraZoomRatio -import com.google.jetpackcamera.model.DeviceRotation -import com.google.jetpackcamera.ui.uistate.capture.ImageWellUiState -import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -/** - * This file contains the data class [CaptureCallbacks] and helper functions to create it. - * [CaptureCallbacks] is used to handle UI events on the capture screen. - */ - -private const val TAG = "CaptureCallbacks" - -/** - * Data class holding callbacks for capture-related UI events. - * - * @param setDisplayRotation Sets the display rotation for the camera. - * @param tapToFocus Initiates a tap-to-focus action at the given coordinates. - * @param changeZoomRatio Changes the camera's zoom ratio. - * @param setZoomAnimationState Sets the target for the zoom animation. - * @param setAudioEnabled Toggles audio recording. - * @param setLockedRecording Locks or unlocks the recording. - * @param updateLastCapturedMedia Updates the UI with the most recently captured media. - * @param imageWellToRepository Posts the media from the image well to the media repository. - * @param setPaused Pauses or resumes video recording. - */ -data class CaptureCallbacks( - val setDisplayRotation: (DeviceRotation) -> Unit, - val tapToFocus: (Float, Float) -> Unit, - val changeZoomRatio: (CameraZoomRatio) -> Unit, - val setZoomAnimationState: (Float?) -> Unit, - val setAudioEnabled: (Boolean) -> Unit, - val setLockedRecording: (Boolean) -> Unit, - val updateLastCapturedMedia: () -> Unit, - val imageWellToRepository: () -> Unit, - val setPaused: (Boolean) -> Unit -) - -/** - * Creates a [CaptureCallbacks] instance with implementations that interact with the camera system - * and update the UI state. - * - * @param viewModelScope The [CoroutineScope] for launching coroutines. - * @param cameraSystem The [CameraSystem] to interact with the camera hardware. - * @param trackedCaptureUiState The mutable state flow for the tracked capture UI state. - * @param mediaRepository The [MediaRepository] for accessing media. - * @param captureUiState The state flow for the overall capture UI state. - * @return An instance of [CaptureCallbacks]. - */ -fun getCaptureCallbacks( - viewModelScope: CoroutineScope, - cameraSystem: CameraSystem, - trackedCaptureUiState: MutableStateFlow, - mediaRepository: MediaRepository, - captureUiState: StateFlow -): CaptureCallbacks { - return CaptureCallbacks( - setDisplayRotation = { deviceRotation -> - viewModelScope.launch { - cameraSystem.setDeviceRotation(deviceRotation) - } - }, - tapToFocus = { x, y -> - Log.d(TAG, "tapToFocus") - viewModelScope.launch { - cameraSystem.tapToFocus(x, y) - } - }, - changeZoomRatio = { newZoomState -> - cameraSystem.changeZoomRatio( - newZoomState = newZoomState - ) - }, - setZoomAnimationState = { targetValue -> - trackedCaptureUiState.update { old -> - old.copy(zoomAnimationTarget = targetValue) - } - }, - setAudioEnabled = { shouldEnableAudio -> - viewModelScope.launch { - cameraSystem.setAudioEnabled(shouldEnableAudio) - } - - Log.d( - TAG, - "Toggle Audio: $shouldEnableAudio" - ) - }, - setLockedRecording = { isLocked -> - trackedCaptureUiState.update { old -> - old.copy(isRecordingLocked = isLocked) - } - }, - updateLastCapturedMedia = { - viewModelScope.launch { - trackedCaptureUiState.update { old -> - old.copy(recentCapturedMedia = mediaRepository.getLastCapturedMedia()) - } - } - }, - imageWellToRepository = { - (captureUiState.value as? CaptureUiState.Ready) - ?.let { it.imageWellUiState as? ImageWellUiState.LastCapture } - ?.let { - postCurrentMediaToMediaRepository( - viewModelScope, - mediaRepository, - it.mediaDescriptor - ) - } - }, - setPaused = { shouldBePaused -> - viewModelScope.launch { - if (shouldBePaused) { - cameraSystem.pauseVideoRecording() - } else { - cameraSystem.resumeVideoRecording() - } - } - } - ) -} - -/** - * Posts the given [MediaDescriptor] to the [MediaRepository] as the current media. - * - * @param viewModelScope The [CoroutineScope] for launching the coroutine. - * @param mediaRepository The repository to update. - * @param mediaDescriptor The media to set as current. - */ -fun postCurrentMediaToMediaRepository( - viewModelScope: CoroutineScope, - mediaRepository: MediaRepository, - mediaDescriptor: MediaDescriptor -) { - viewModelScope.launch { - mediaRepository.setCurrentMedia(mediaDescriptor) - } -} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt index 3fc343e27..e6ac53ae3 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/CaptureScreenComponents.kt @@ -337,11 +337,12 @@ fun TestableSnackbar( when (result) { SnackbarResult.ActionPerformed, SnackbarResult.Dismissed -> snackBarController.onSnackBarResult( - snackbarToShow.cookie) + snackbarToShow.cookie + ) } } catch (e: Exception) { // This is equivalent to dismissing the snackbar - snackBarController.onSnackBarResult(snackbarToShow.cookie) + snackBarController.onSnackBarResult(snackbarToShow.cookie) } } } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt index c8728e548..93fea248b 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/MultipleEventsCutter.kt @@ -35,4 +35,3 @@ internal class MultipleEventsCutter { private const val DURATION_BETWEEN_CLICKS_MS = 300L } } - diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt index 2bf3106e7..71510531e 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt @@ -115,4 +115,3 @@ fun addSnackBarData( } } } - diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt index ed2acca86..6b2c4a788 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.jetpackcamera.ui.components.capture import com.google.jetpackcamera.ui.uistate.DisableRationale @@ -24,4 +23,4 @@ interface SnackBarController { fun onSnackBarResult(cookie: String) fun incrementAndGetSnackBarCount(): Int fun addSnackBarData(snackBarData: SnackbarData) -} \ No newline at end of file +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt index bc373c04c..40dba78b9 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt @@ -13,26 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.jetpackcamera.ui.components.capture import android.util.Log import com.google.jetpackcamera.ui.uistate.DisableRationale import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import java.util.LinkedList import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.util.LinkedList private const val TAG = "SnackBarControllerImpl" class SnackBarControllerImpl( private val viewModelScope: CoroutineScope, private val snackBarUiState: MutableStateFlow -): SnackBarController { +) : SnackBarController { val snackBarCount = atomic(0) override fun enqueueDisabledHdrToggleSnackBar(disabledReason: DisableRationale) { val cookieInt = incrementAndGetSnackBarCount() @@ -81,4 +80,4 @@ class SnackBarControllerImpl( } } } -} \ No newline at end of file +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/ZoomState.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/ZoomState.kt index 38cb3380a..ee6220850 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/ZoomState.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/ZoomState.kt @@ -25,15 +25,15 @@ import androidx.core.util.toClosedRange import com.google.jetpackcamera.model.CameraZoomRatio import com.google.jetpackcamera.model.LensToZoom import com.google.jetpackcamera.model.ZoomStrategy +import com.google.jetpackcamera.ui.components.capture.controller.ZoomController class ZoomState( initialZoomLevel: Float, zoomRange: Range, - val onChangeZoomLevel: (CameraZoomRatio) -> Unit, - val onAnimateStateChanged: (Float?) -> Unit + private val zoomController: ZoomController ) { init { - onAnimateStateChanged(null) + zoomController.setZoomAnimationState(null) } private var functionalZoom = initialZoomLevel @@ -44,7 +44,7 @@ class ZoomState( private suspend fun mutateZoom(block: suspend () -> Unit) { mutatorMutex.mutate { - onAnimateStateChanged(null) + zoomController.setZoomAnimationState(null) block() } } @@ -57,7 +57,7 @@ class ZoomState( if (lensToZoom == LensToZoom.PRIMARY) { functionalZoom = targetZoomLevel.coerceIn(functionalZoomRange.toClosedRange()) } - onChangeZoomLevel( + zoomController.setZoomRatio( CameraZoomRatio( ZoomStrategy.Absolute( targetZoomLevel.coerceIn(functionalZoomRange.toClosedRange()), @@ -93,7 +93,7 @@ class ZoomState( lensToZoom: LensToZoom ) { mutatorMutex.mutate { - onAnimateStateChanged(targetZoomLevel) + zoomController.setZoomAnimationState(targetZoomLevel) Animatable(initialValue = functionalZoom).animateTo( targetValue = targetZoomLevel, @@ -101,7 +101,7 @@ class ZoomState( ) { // this is called every animation frame functionalZoom = value.coerceIn(functionalZoomRange.toClosedRange()) - onChangeZoomLevel( + zoomController.setZoomRatio( CameraZoomRatio( ZoomStrategy.Absolute( functionalZoom, diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt new file mode 100644 index 000000000..b4aba7559 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt @@ -0,0 +1,21 @@ +/* + * 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.components.capture.controller + +interface CameraController { + fun startCamera() + fun stopCamera() +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt new file mode 100644 index 000000000..d8f397293 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt @@ -0,0 +1,78 @@ +/* + * 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.components.capture.controller + +import android.os.SystemClock +import android.util.Log +import androidx.lifecycle.viewModelScope +import androidx.tracing.Trace +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.core.common.traceFirstFramePreview +import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.launch + +private const val TAG = "CameraControllerImpl" + +class CameraControllerImpl( + private val initializationDeferred: Deferred, + private val captureUiState: StateFlow, + private val viewModelScope: CoroutineScope, + private val cameraSystem: CameraSystem +) : CameraController { + private var runningCameraJob: Job? = null + override fun startCamera() { + Log.d(TAG, "startCamera") + stopCamera() + runningCameraJob = viewModelScope.launch { + if (Trace.isEnabled()) { + launch(start = CoroutineStart.UNDISPATCHED) { + val startTraceTimestamp: Long = SystemClock.elapsedRealtimeNanos() + traceFirstFramePreview(cookie = 1) { + captureUiState.transformWhile { + var continueCollecting = true + (it as? CaptureUiState.Ready)?.let { uiState -> + if (uiState.sessionFirstFrameTimestamp > startTraceTimestamp) { + emit(Unit) + continueCollecting = false + } + } + continueCollecting + }.collect {} + } + } + } + // Ensure CameraSystem is initialized before starting camera + initializationDeferred.await() + // TODO(yasith): Handle Exceptions from binding use cases + cameraSystem.runCamera() + } + } + + override fun stopCamera() { + Log.d(TAG, "stopCamera") + runningCameraJob?.apply { + if (isActive) { + cancel() + } + } + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt new file mode 100644 index 000000000..32928e787 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt @@ -0,0 +1,25 @@ +/* + * 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.components.capture.controller + +import android.content.ContentResolver + +interface CaptureController { + fun captureImage(contentResolver: ContentResolver) + fun startVideoRecording() + fun stopVideoRecording() + fun setLockedRecording(isLocked: Boolean) +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt new file mode 100644 index 000000000..2789c5700 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt @@ -0,0 +1,298 @@ +/* + * 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.components.capture.controller + +import android.content.ContentResolver +import android.util.Log +import androidx.tracing.traceAsync +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.core.camera.OnVideoRecordEvent +import com.google.jetpackcamera.data.media.MediaDescriptor +import com.google.jetpackcamera.data.media.MediaRepository +import com.google.jetpackcamera.model.CaptureEvent +import com.google.jetpackcamera.model.ExternalCaptureMode +import com.google.jetpackcamera.model.ImageCaptureEvent +import com.google.jetpackcamera.model.IntProgress +import com.google.jetpackcamera.model.SaveLocation +import com.google.jetpackcamera.model.SaveMode +import com.google.jetpackcamera.model.VideoCaptureEvent +import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG +import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_FAILURE_TAG +import com.google.jetpackcamera.ui.components.capture.IMAGE_CAPTURE_SUCCESS_TAG +import com.google.jetpackcamera.ui.components.capture.R +import com.google.jetpackcamera.ui.components.capture.SnackBarController +import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG +import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG +import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG +import com.google.jetpackcamera.ui.components.capture.controller.Utils.nextSaveLocation +import com.google.jetpackcamera.ui.components.capture.controller.Utils.postCurrentMediaToMediaRepository +import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +private const val TAG = "CaptureButtonControllerImpl" + +private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture" + +class CaptureControllerImpl( + private val trackedCaptureUiState: MutableStateFlow, + private val captureUiState: StateFlow, + private val viewModelScope: CoroutineScope, + private val cameraSystem: CameraSystem, + private val mediaRepository: MediaRepository, + private val saveMode: SaveMode, + private val externalCaptureMode: ExternalCaptureMode, + private val externalCapturesCallback: () -> Pair, + private val captureEvents: Channel, + private val captureScreenController: CaptureScreenController, + private val snackBarController: SnackBarController? +) : CaptureController { + + private val traceCookie = atomic(0) + private val videoCaptureStartedCount = atomic(0) + private var recordingJob: Job? = null + + override fun captureImage(contentResolver: ContentResolver) { + if (captureUiState.value is CaptureUiState.Ready && + (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == + ExternalCaptureMode.VideoCapture + ) { + snackBarController?.addSnackBarData( + SnackbarData( + cookie = "Image-ExternalVideoCaptureMode", + stringResource = R.string.toast_image_capture_external_unsupported, + withDismissAction = true, + testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG + ) + ) + return + } + + if (captureUiState.value is CaptureUiState.Ready && + (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == + ExternalCaptureMode.VideoCapture + ) { + snackBarController?.addSnackBarData( + SnackbarData( + cookie = "Image-ExternalVideoCaptureMode", + stringResource = R.string.toast_image_capture_external_unsupported, + withDismissAction = true, + testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG + ) + ) + return + } + Log.d(TAG, "captureImage") + viewModelScope.launch { + val (saveLocation, progress) = nextSaveLocation( + saveMode, + externalCaptureMode, + externalCapturesCallback + ) + captureImageInternal( + saveLocation = saveLocation, + doTakePicture = { + cameraSystem.takePicture(contentResolver, saveLocation) { + trackedCaptureUiState.update { old -> + old.copy(lastBlinkTimeStamp = System.currentTimeMillis()) + } + }.savedUri + }, + onSuccess = { savedUri -> + val event = if (progress != null) { + ImageCaptureEvent.SequentialImageSaved(savedUri, progress) + } else { + if (saveLocation is SaveLocation.Cache) { + ImageCaptureEvent.SingleImageCached(savedUri) + } else { + ImageCaptureEvent.SingleImageSaved(savedUri) + } + } + if (saveLocation !is SaveLocation.Cache) { + captureScreenController.updateLastCapturedMedia() + } else { + savedUri?.let { + postCurrentMediaToMediaRepository( + viewModelScope, + mediaRepository, + MediaDescriptor.Content.Image(it, null, true) + ) + } + } + captureEvents.trySend(event) + }, + onFailure = { exception -> + val event = if (progress != null) { + ImageCaptureEvent.SequentialImageCaptureError(exception, progress) + } else { + ImageCaptureEvent.SingleImageCaptureError(exception) + } + + captureEvents.trySend(event) + } + ) + } + } + + override fun startVideoRecording() { + if (captureUiState.value is CaptureUiState.Ready && + (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == + ExternalCaptureMode.ImageCapture + ) { + Log.d(TAG, "externalVideoRecording") + snackBarController?.addSnackBarData( + SnackbarData( + cookie = "Video-ExternalImageCaptureMode", + stringResource = R.string.toast_video_capture_external_unsupported, + withDismissAction = true, + testTag = VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG + ) + ) + return + } + Log.d(TAG, "startVideoRecording") + recordingJob = viewModelScope.launch { + val cookie = "Video-${videoCaptureStartedCount.incrementAndGet()}" + val (saveLocation, _) = nextSaveLocation( + saveMode, + externalCaptureMode, + externalCapturesCallback + ) + try { + cameraSystem.startVideoRecording(saveLocation) { + var snackbarToShow: SnackbarData? + when (it) { + is OnVideoRecordEvent.OnVideoRecorded -> { + Log.d(TAG, "cameraSystem.startRecording OnVideoRecorded") + val event = if (saveLocation is SaveLocation.Cache) { + VideoCaptureEvent.VideoCached(it.savedUri) + } else { + VideoCaptureEvent.VideoSaved(it.savedUri) + } + + if (saveLocation !is SaveLocation.Cache) { + captureScreenController.updateLastCapturedMedia() + } else { + postCurrentMediaToMediaRepository( + viewModelScope, + mediaRepository, + MediaDescriptor.Content.Video(it.savedUri, null, true) + ) + } + + captureEvents.trySend(event) + // don't display snackbar for successful capture + snackbarToShow = if (saveLocation is SaveLocation.Cache) { + null + } else { + SnackbarData( + cookie = cookie, + stringResource = R.string.toast_video_capture_success, + withDismissAction = true, + testTag = VIDEO_CAPTURE_SUCCESS_TAG + ) + } + } + + is OnVideoRecordEvent.OnVideoRecordError -> { + Log.d(TAG, "cameraSystem.startRecording OnVideoRecordError") + captureEvents.trySend(VideoCaptureEvent.VideoCaptureError(it.error)) + snackbarToShow = SnackbarData( + cookie = cookie, + stringResource = R.string.toast_video_capture_failure, + withDismissAction = true, + testTag = VIDEO_CAPTURE_FAILURE_TAG + ) + } + } + + snackbarToShow?.let { data -> + snackBarController?.addSnackBarData(data) + } + } + Log.d(TAG, "cameraSystem.startRecording success") + } catch (exception: IllegalStateException) { + Log.d(TAG, "cameraSystem.startVideoRecording error", exception) + } + } + } + + override fun stopVideoRecording() { + Log.d(TAG, "stopVideoRecording") + viewModelScope.launch { + cameraSystem.stopVideoRecording() + recordingJob?.cancel() + } + } + + private suspend fun captureImageInternal( + saveLocation: SaveLocation, + doTakePicture: suspend () -> T, + onSuccess: (T) -> Unit = {}, + onFailure: (exception: Exception) -> Unit = {} + ) { + val cookieInt = snackBarController?.incrementAndGetSnackBarCount() + ?: traceCookie.incrementAndGet() + val cookie = "Image-$cookieInt" + val snackBarData = try { + traceAsync(IMAGE_CAPTURE_TRACE, cookieInt) { + doTakePicture() + }.also { result -> + onSuccess(result) + } + Log.d(TAG, "cameraSystem.takePicture success") + // don't display snackbar for successful capture + if (saveLocation is SaveLocation.Cache) { + null + } else { + SnackbarData( + cookie = cookie, + stringResource = R.string.toast_image_capture_success, + withDismissAction = true, + testTag = IMAGE_CAPTURE_SUCCESS_TAG + ) + } + } catch (exception: Exception) { + onFailure(exception) + Log.d(TAG, "cameraSystem.takePicture error", exception) + SnackbarData( + cookie = cookie, + stringResource = R.string.toast_capture_failure, + withDismissAction = true, + testTag = IMAGE_CAPTURE_FAILURE_TAG + ) + } + snackBarData?.let { + snackBarController?.addSnackBarData( + it + ) + } + } + + override fun setLockedRecording(isLocked: Boolean) { + trackedCaptureUiState.update { old -> + old.copy(isRecordingLocked = isLocked) + } + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt new file mode 100644 index 000000000..8720c54b1 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt @@ -0,0 +1,27 @@ +/* + * 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.components.capture.controller + +import com.google.jetpackcamera.model.DeviceRotation + +interface CaptureScreenController { + fun setDisplayRotation(deviceRotation: DeviceRotation) + fun tapToFocus(x: Float, y: Float) + fun setAudioEnabled(shouldEnableAudio: Boolean) + fun updateLastCapturedMedia() + fun imageWellToRepository() + fun setPaused(shouldBePaused: Boolean) +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt new file mode 100644 index 000000000..7d2865dc1 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt @@ -0,0 +1,94 @@ +/* + * 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.components.capture.controller + +import android.util.Log +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.data.media.MediaRepository +import com.google.jetpackcamera.model.DeviceRotation +import com.google.jetpackcamera.ui.components.capture.controller.Utils.postCurrentMediaToMediaRepository +import com.google.jetpackcamera.ui.uistate.capture.ImageWellUiState +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +private const val TAG = "CaptureScreenControllerImpl" + +class CaptureScreenControllerImpl( + private val viewModelScope: CoroutineScope, + private val cameraSystem: CameraSystem, + private val trackedCaptureUiState: MutableStateFlow, + private val mediaRepository: MediaRepository, + private val captureUiState: StateFlow +) : CaptureScreenController { + override fun setDisplayRotation(deviceRotation: DeviceRotation) { + viewModelScope.launch { + cameraSystem.setDeviceRotation(deviceRotation) + } + } + + override fun tapToFocus(x: Float, y: Float) { + Log.d(TAG, "tapToFocus") + viewModelScope.launch { + cameraSystem.tapToFocus(x, y) + } + } + + override fun setAudioEnabled(shouldEnableAudio: Boolean) { + viewModelScope.launch { + cameraSystem.setAudioEnabled(shouldEnableAudio) + } + + Log.d( + TAG, + "Toggle Audio: $shouldEnableAudio" + ) + } + + override fun updateLastCapturedMedia() { + viewModelScope.launch { + trackedCaptureUiState.update { old -> + old.copy(recentCapturedMedia = mediaRepository.getLastCapturedMedia()) + } + } + } + + override fun imageWellToRepository() { + (captureUiState.value as? CaptureUiState.Ready) + ?.let { it.imageWellUiState as? ImageWellUiState.LastCapture } + ?.let { + postCurrentMediaToMediaRepository( + viewModelScope, + mediaRepository, + it.mediaDescriptor + ) + } + } + + override fun setPaused(shouldBePaused: Boolean) { + viewModelScope.launch { + if (shouldBePaused) { + cameraSystem.pauseVideoRecording() + } else { + cameraSystem.resumeVideoRecording() + } + } + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt new file mode 100644 index 000000000..a70be0c07 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt @@ -0,0 +1,68 @@ +/* + * 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.components.capture.controller + +import com.google.jetpackcamera.data.media.MediaDescriptor +import com.google.jetpackcamera.data.media.MediaRepository +import com.google.jetpackcamera.model.ExternalCaptureMode +import com.google.jetpackcamera.model.IntProgress +import com.google.jetpackcamera.model.SaveLocation +import com.google.jetpackcamera.model.SaveMode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +object Utils { + /** + * Posts the given [MediaDescriptor] to the [MediaRepository] as the current media. + * + * @param viewModelScope The [CoroutineScope] for launching the coroutine. + * @param mediaRepository The repository to update. + * @param mediaDescriptor The media to set as current. + */ + fun postCurrentMediaToMediaRepository( + viewModelScope: CoroutineScope, + mediaRepository: MediaRepository, + mediaDescriptor: MediaDescriptor + ) { + viewModelScope.launch { + mediaRepository.setCurrentMedia(mediaDescriptor) + } + } + + fun nextSaveLocation( + saveMode: SaveMode, + externalCaptureMode: ExternalCaptureMode, + externalCapturesCallback: () -> Pair + ): Pair { + return when (externalCaptureMode) { + ExternalCaptureMode.ImageCapture, + ExternalCaptureMode.MultipleImageCapture, + ExternalCaptureMode.VideoCapture -> { + externalCapturesCallback() + } + + ExternalCaptureMode.Standard -> { + val defaultSaveLocation = + if (saveMode is SaveMode.CacheAndReview) { + SaveLocation.Cache(saveMode.cacheDir) + } else { + SaveLocation.Default + } + Pair(defaultSaveLocation, null) + } + } + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt new file mode 100644 index 000000000..2bb8242d3 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt @@ -0,0 +1,23 @@ +/* + * 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.components.capture.controller + +import com.google.jetpackcamera.model.CameraZoomRatio + +interface ZoomController { + fun setZoomRatio(zoomRatio: CameraZoomRatio) + fun setZoomAnimationState(targetValue: Float?) +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt new file mode 100644 index 000000000..f0ed8c541 --- /dev/null +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt @@ -0,0 +1,39 @@ +/* + * 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.components.capture.controller + +import com.google.jetpackcamera.core.camera.CameraSystem +import com.google.jetpackcamera.model.CameraZoomRatio +import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +class ZoomControllerImpl( + private val cameraSystem: CameraSystem, + private val trackedCaptureUiState: MutableStateFlow +) : ZoomController { + override fun setZoomRatio(zoomRatio: CameraZoomRatio) { + cameraSystem.changeZoomRatio( + newZoomState = zoomRatio + ) + } + + override fun setZoomAnimationState(targetValue: Float?) { + trackedCaptureUiState.update { old -> + old.copy(zoomAnimationTarget = targetValue) + } + } +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt index 256d8b9f6..516298cf0 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/DebugOverlayComponents.kt @@ -78,7 +78,6 @@ import com.google.jetpackcamera.ui.components.capture.PHYSICAL_CAMERA_ID_TAG import com.google.jetpackcamera.ui.components.capture.R import com.google.jetpackcamera.ui.components.capture.ZOOM_RATIO_TAG import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugController -import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugControllerImpl import com.google.jetpackcamera.ui.uistate.capture.DebugUiState import kotlin.math.abs diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt index 6c595793d..fa1a2924d 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.jetpackcamera.ui.components.capture.debug.controller import com.google.jetpackcamera.model.TestPattern diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt index fdd6bc975..93902c33f 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.jetpackcamera.ui.components.capture.debug.controller import com.google.jetpackcamera.core.camera.CameraSystem @@ -25,7 +24,7 @@ import kotlinx.coroutines.flow.update class DebugControllerImpl( private val cameraSystem: CameraSystem, private val trackedCaptureUiState: MutableStateFlow -): DebugController { +) : DebugController { override fun toggleDebugHidingComponents() { trackedCaptureUiState.update { old -> old.copy(debugHidingComponents = !old.debugHidingComponents) @@ -43,4 +42,4 @@ class DebugControllerImpl( newTestPattern = testPattern ) } -} \ No newline at end of file +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt index e4e4adf13..733f137d5 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/QuickSettingsScreen.kt @@ -127,7 +127,8 @@ fun QuickSettingsBottomSheet( QuickFlipCamera( modifier = Modifier.testTag(QUICK_SETTINGS_FLIP_CAMERA_BUTTON), setLensFacing = { l: LensFacing -> - quickSettingsController.setLensFacing(l) }, + quickSettingsController.setLensFacing(l) + }, flipLensUiState = quickSettingsUiState.flipLensUiState ) } @@ -136,7 +137,9 @@ fun QuickSettingsBottomSheet( ToggleFocusedQuickSetRatio( modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_BUTTON), setRatio = { - quickSettingsController.setFocusedSetting(FocusedQuickSetting.ASPECT_RATIO) + quickSettingsController.setFocusedSetting( + FocusedQuickSetting.ASPECT_RATIO + ) }, isHighlightEnabled = false, aspectRatioUiState = quickSettingsUiState.aspectRatioUiState @@ -148,7 +151,9 @@ fun QuickSettingsBottomSheet( modifier = Modifier.testTag( QUICK_SETTINGS_STREAM_CONFIG_BUTTON ), - setStreamConfig = { c: StreamConfig -> quickSettingsController.setStreamConfig(c) }, + setStreamConfig = { c: StreamConfig -> + quickSettingsController.setStreamConfig(c) + }, streamConfigUiState = quickSettingsUiState.streamConfigUiState ) } @@ -351,5 +356,4 @@ class MockQuickSettingsController : QuickSettingsController { override fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) {} override fun setCaptureMode(captureMode: CaptureMode) {} - } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt index 06ddbd416..30e761e69 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.jetpackcamera.ui.components.capture.quicksettings.controller import com.google.jetpackcamera.model.AspectRatio @@ -28,7 +27,7 @@ import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting interface QuickSettingsController { fun toggleQuickSettings() - fun setFocusedSetting (focusedQuickSetting: FocusedQuickSetting) + fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) fun setLensFacing(lensFace: LensFacing) fun setFlash(flashMode: FlashMode) fun setAspectRatio(aspectRatio: AspectRatio) @@ -37,5 +36,4 @@ interface QuickSettingsController { fun setImageFormat(imageOutputFormat: ImageOutputFormat) fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) fun setCaptureMode(captureMode: CaptureMode) - -} \ No newline at end of file +} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt index b18a6d000..2943013b2 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.jetpackcamera.ui.components.capture.quicksettings.controller import com.google.jetpackcamera.core.camera.CameraSystem @@ -106,4 +105,4 @@ class QuickSettingsControllerImpl( cameraSystem.setCaptureMode(captureMode) } } -} \ No newline at end of file +} diff --git a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt index ee7535947..206a3f593 100644 --- a/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt +++ b/ui/uistateadapter/capture/src/main/java/com/google/jetpackcamera/ui/uistateadapter/capture/SnackBarUiStateAdapter.kt @@ -15,8 +15,8 @@ */ package com.google.jetpackcamera.ui.uistateadapter.capture -import com.google.jetpackcamera.ui.uistate.SnackBarUiState -import com.google.jetpackcamera.ui.uistate.SnackbarData +import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState +import com.google.jetpackcamera.ui.uistate.capture.SnackbarData import java.util.Queue fun SnackBarUiState.Companion.from(snackBarQueue: Queue): SnackBarUiState { From 998582aaa99b64e6d6cc9139942d118d1c82c1da Mon Sep 17 00:00:00 2001 From: David Jia Date: Tue, 24 Feb 2026 16:52:31 -0800 Subject: [PATCH 4/6] spotless --- .../feature/preview/PreviewViewModel.kt | 18 +-- .../components/capture/SnackBarCallbacks.kt | 117 ------------------ .../components/capture/SnackBarController.kt | 2 +- .../capture/SnackBarControllerImpl.kt | 4 +- .../controller/CaptureControllerImpl.kt | 2 +- 5 files changed, 13 insertions(+), 130 deletions(-) delete mode 100644 ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index 7acba48d1..f3105ed6b 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -57,9 +57,9 @@ import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugCont import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugControllerImpl import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsController import com.google.jetpackcamera.ui.components.capture.quicksettings.controller.QuickSettingsControllerImpl -import com.google.jetpackcamera.ui.uistate.capture.DebugUiState import com.google.jetpackcamera.ui.uistate.SnackBarUiState import com.google.jetpackcamera.ui.uistate.SnackbarData +import com.google.jetpackcamera.ui.uistate.capture.DebugUiState import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState import com.google.jetpackcamera.ui.uistateadapter.capture.compound.captureUiState @@ -95,7 +95,7 @@ class PreviewViewModel @Inject constructor( private val mediaRepository: MediaRepository ) : ViewModel() { private val saveMode: SaveMode = savedStateHandle.getRequestedSaveMode() ?: defaultSaveMode - private val _trackedCaptureUiState: MutableStateFlow = + private val trackedCaptureUiState: MutableStateFlow = MutableStateFlow(TrackedCaptureUiState()) private val _snackBarUiState: MutableStateFlow = MutableStateFlow(SnackBarUiState.Enabled()) @@ -130,7 +130,7 @@ class PreviewViewModel @Inject constructor( val captureUiState: StateFlow = captureUiState( cameraSystem, constraintsRepository, - _trackedCaptureUiState, + trackedCaptureUiState, externalCaptureMode ) .stateIn( @@ -143,7 +143,7 @@ class PreviewViewModel @Inject constructor( constraintsRepository, debugSettings, cameraPropertiesJSON, - _trackedCaptureUiState + trackedCaptureUiState ) .stateIn( scope = viewModelScope, @@ -152,7 +152,7 @@ class PreviewViewModel @Inject constructor( ) val quickSettingsController: QuickSettingsController = QuickSettingsControllerImpl( - trackedCaptureUiState = _trackedCaptureUiState, + trackedCaptureUiState = trackedCaptureUiState, viewModelScope = viewModelScope, cameraSystem = cameraSystem, externalCaptureMode = externalCaptureMode @@ -160,7 +160,7 @@ class PreviewViewModel @Inject constructor( val debugController: DebugController = DebugControllerImpl( cameraSystem = cameraSystem, - trackedCaptureUiState = _trackedCaptureUiState + trackedCaptureUiState = trackedCaptureUiState ) val snackBarController: SnackBarController = SnackBarControllerImpl( @@ -171,14 +171,14 @@ class PreviewViewModel @Inject constructor( val captureScreenController: CaptureScreenController = CaptureScreenControllerImpl( viewModelScope = viewModelScope, cameraSystem = cameraSystem, - trackedCaptureUiState = _trackedCaptureUiState, + trackedCaptureUiState = trackedCaptureUiState, mediaRepository = mediaRepository, captureUiState = captureUiState ) val zoomController: ZoomController = ZoomControllerImpl( cameraSystem = cameraSystem, - trackedCaptureUiState = _trackedCaptureUiState + trackedCaptureUiState = trackedCaptureUiState ) val cameraController: CameraController = CameraControllerImpl( @@ -189,7 +189,7 @@ class PreviewViewModel @Inject constructor( ) val captureController: CaptureController = CaptureControllerImpl( - trackedCaptureUiState = _trackedCaptureUiState, + trackedCaptureUiState = trackedCaptureUiState, captureUiState = captureUiState, viewModelScope = viewModelScope, cameraSystem = cameraSystem, diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt deleted file mode 100644 index 71510531e..000000000 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarCallbacks.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.components.capture - -import android.util.Log -import com.google.jetpackcamera.ui.uistate.DisableRationale -import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState -import com.google.jetpackcamera.ui.uistate.capture.SnackbarData -import java.util.LinkedList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -/** - * This file contains the data class [SnackBarCallbacks] and helper functions to create it. - * [SnackBarCallbacks] is used to handle UI events related to snack bars. - */ - -private const val TAG = "SnackBarCallbacks" - -/** - * Data class holding callbacks for snack bar UI events. - * - * @param enqueueDisabledHdrToggleSnackBar Enqueues a snack bar to inform the user that HDR is - * disabled. - * @param onSnackBarResult Handles the result of a snack bar action. - */ -data class SnackBarCallbacks( - val enqueueDisabledHdrToggleSnackBar: (DisableRationale) -> Unit = {}, - val onSnackBarResult: (String) -> Unit = {} -) - -/** - * Creates a [SnackBarCallbacks] instance. - * - * @param incrementSnackBarCount A function to increment the snack bar count. - * @param viewModelScope The [CoroutineScope] for launching coroutines. - * @param snackBarUiState The mutable state flow for the snack bar UI state. - * @return An instance of [SnackBarCallbacks]. - */ -fun getSnackBarCallbacks( - incrementSnackBarCount: () -> Int = { 0 }, - viewModelScope: CoroutineScope, - snackBarUiState: MutableStateFlow -): SnackBarCallbacks { - return SnackBarCallbacks( - enqueueDisabledHdrToggleSnackBar = { disabledReason -> - val cookieInt = incrementSnackBarCount() - val cookie = "DisabledHdrToggle-$cookieInt" - addSnackBarData( - viewModelScope, - snackBarUiState, - SnackbarData( - cookie = cookie, - stringResource = disabledReason.reasonTextResId, - withDismissAction = true, - testTag = disabledReason.testTag - ) - ) - }, - onSnackBarResult = { cookie -> - viewModelScope.launch { - snackBarUiState.update { old -> - val newQueue = LinkedList(old.snackBarQueue) - val snackBarData = newQueue.poll() - if (snackBarData != null && snackBarData.cookie == cookie) { - // If the latest snackBar had a result, then clear snackBarToShow - Log.d(TAG, "SnackBar removed. Queue size: ${newQueue.size}") - old.copy( - snackBarQueue = newQueue - ) - } else { - old - } - } - } - } - ) -} - -/** - * Adds a [SnackbarData] to the snack bar queue. - * - * @param viewModelScope The [CoroutineScope] for launching coroutines. - * @param snackBarUiState The mutable state flow for the snack bar UI state. - * @param snackBarData The data for the snack bar to be added. - */ -fun addSnackBarData( - viewModelScope: CoroutineScope, - snackBarUiState: MutableStateFlow, - snackBarData: SnackbarData -) { - viewModelScope.launch { - snackBarUiState.update { old -> - val newQueue = LinkedList(old.snackBarQueue) - newQueue.add(snackBarData) - Log.d(TAG, "SnackBar added. Queue size: ${newQueue.size}") - old.copy( - snackBarQueue = newQueue - ) - } - } -} diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt index 6b2c4a788..a1cbd1f24 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarController.kt @@ -16,7 +16,7 @@ package com.google.jetpackcamera.ui.components.capture import com.google.jetpackcamera.ui.uistate.DisableRationale -import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import com.google.jetpackcamera.ui.uistate.SnackbarData interface SnackBarController { fun enqueueDisabledHdrToggleSnackBar(disabledReason: DisableRationale) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt index 40dba78b9..0af482e9d 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/SnackBarControllerImpl.kt @@ -17,8 +17,8 @@ package com.google.jetpackcamera.ui.components.capture import android.util.Log import com.google.jetpackcamera.ui.uistate.DisableRationale -import com.google.jetpackcamera.ui.uistate.capture.SnackBarUiState -import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import com.google.jetpackcamera.ui.uistate.SnackBarUiState +import com.google.jetpackcamera.ui.uistate.SnackbarData import java.util.LinkedList import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt index 2789c5700..5d93bd8c9 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt @@ -39,7 +39,7 @@ import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_FAILURE_TAG import com.google.jetpackcamera.ui.components.capture.VIDEO_CAPTURE_SUCCESS_TAG import com.google.jetpackcamera.ui.components.capture.controller.Utils.nextSaveLocation import com.google.jetpackcamera.ui.components.capture.controller.Utils.postCurrentMediaToMediaRepository -import com.google.jetpackcamera.ui.uistate.capture.SnackbarData +import com.google.jetpackcamera.ui.uistate.SnackbarData import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState import kotlinx.atomicfu.atomic From 3913eb10abf4be87e8da9af847bf72275a20c32d Mon Sep 17 00:00:00 2001 From: David Jia Date: Wed, 25 Feb 2026 14:25:39 -0800 Subject: [PATCH 5/6] add kdocs --- .../feature/preview/PreviewScreen.kt | 12 +--- .../feature/preview/PreviewViewModel.kt | 1 - .../capture/controller/CameraController.kt | 10 +++ .../controller/CameraControllerImpl.kt | 9 +++ .../capture/controller/CaptureController.kt | 24 ++++++- .../controller/CaptureControllerImpl.kt | 44 +++++-------- .../controller/CaptureScreenController.kt | 35 ++++++++++ .../controller/CaptureScreenControllerImpl.kt | 10 +++ .../ui/components/capture/controller/Utils.kt | 9 +++ .../capture/controller/ZoomController.kt | 16 ++++- .../capture/controller/ZoomControllerImpl.kt | 7 ++ .../debug/controller/DebugController.kt | 4 ++ .../debug/controller/DebugControllerImpl.kt | 7 ++ .../controller/QuickSettingsController.kt | 64 +++++++++++++++++++ .../controller/QuickSettingsControllerImpl.kt | 9 +++ 15 files changed, 221 insertions(+), 40 deletions(-) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index 7e36f529a..9ae5bf3eb 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -403,11 +403,7 @@ private fun ContentScreen( PreviewDisplay( previewDisplayUiState = captureUiState.previewDisplayUiState, onFlipCamera = onFlipCamera, - onTapToFocus = if (captureScreenController != null) { - captureScreenController::tapToFocus - } else { - { _, _ -> } - }, + onTapToFocus = captureScreenController?.let { it::tapToFocus } ?: { _, _ -> }, onScaleZoom = { onScaleZoom(it, LensToZoom.PRIMARY) }, surfaceRequest = surfaceRequest, onRequestWindowColorMode = onRequestWindowColorMode, @@ -582,11 +578,7 @@ private fun ContentScreen( }, pauseToggleButton = { PauseResumeToggleButton( - onSetPause = if (captureScreenController != null) { - captureScreenController::setPaused - } else { - { _ -> } - }, + onSetPause = captureScreenController?.let { it::setPaused } ?: { _ -> }, currentRecordingState = captureUiState.videoRecordingState ) }, diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt index f3105ed6b..cff2e7184 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewViewModel.kt @@ -190,7 +190,6 @@ class PreviewViewModel @Inject constructor( val captureController: CaptureController = CaptureControllerImpl( trackedCaptureUiState = trackedCaptureUiState, - captureUiState = captureUiState, viewModelScope = viewModelScope, cameraSystem = cameraSystem, mediaRepository = mediaRepository, diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt index b4aba7559..b427d74b9 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraController.kt @@ -15,7 +15,17 @@ */ package com.google.jetpackcamera.ui.components.capture.controller +/** + * Interface for controlling camera lifecycle and basic operations. + */ interface CameraController { + /** + * Starts the camera. + */ fun startCamera() + + /** + * Stops the camera. + */ fun stopCamera() } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt index d8f397293..a59c37d41 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CameraControllerImpl.kt @@ -32,6 +32,14 @@ import kotlinx.coroutines.launch private const val TAG = "CameraControllerImpl" +/** + * Implementation of [CameraController] that manages the camera lifecycle. + * + * @param initializationDeferred A [Deferred] that completes when the camera system is initialized. + * @param captureUiState The [StateFlow] of the capture UI state. + * @param viewModelScope The [CoroutineScope] for launching coroutines. + * @param cameraSystem The [CameraSystem] to interact with. + */ class CameraControllerImpl( private val initializationDeferred: Deferred, private val captureUiState: StateFlow, @@ -39,6 +47,7 @@ class CameraControllerImpl( private val cameraSystem: CameraSystem ) : CameraController { private var runningCameraJob: Job? = null + override fun startCamera() { Log.d(TAG, "startCamera") stopCamera() diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt index 32928e787..53ead8098 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureController.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2026 The Android Open Source Project + * Copyright (C) 2024 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. @@ -17,9 +17,31 @@ package com.google.jetpackcamera.ui.components.capture.controller import android.content.ContentResolver +/** + * Interface for controlling capture operations like taking photos and recording videos. + */ interface CaptureController { + /** + * Captures a single image. + * + * @param contentResolver The [ContentResolver] to use for saving the image. + */ fun captureImage(contentResolver: ContentResolver) + + /** + * Starts video recording. + */ fun startVideoRecording() + + /** + * Stops the current video recording. + */ fun stopVideoRecording() + + /** + * Sets whether the recording is locked. + * + * @param isLocked True if the recording should be locked, false otherwise. + */ fun setLockedRecording(isLocked: Boolean) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt index 5d93bd8c9..f8fc3e122 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureControllerImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2026 The Android Open Source Project + * Copyright (C) 2024 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. @@ -41,13 +41,11 @@ import com.google.jetpackcamera.ui.components.capture.controller.Utils.nextSaveL import com.google.jetpackcamera.ui.components.capture.controller.Utils.postCurrentMediaToMediaRepository import com.google.jetpackcamera.ui.uistate.SnackbarData import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState -import com.google.jetpackcamera.ui.uistate.capture.compound.CaptureUiState import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -55,9 +53,22 @@ private const val TAG = "CaptureButtonControllerImpl" private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture" +/** + * Implementation of [CaptureController] that interacts with [CameraSystem] and [MediaRepository]. + * + * @property trackedCaptureUiState State for tracking UI changes during capture. + * @property viewModelScope Scope for launching coroutines. + * @property cameraSystem The camera system to perform capture operations. + * @property mediaRepository Repository for managing captured media. + * @property saveMode Mode for saving captured media. + * @property externalCaptureMode Mode for external capture requests. + * @property externalCapturesCallback Callback for getting external capture information. + * @property captureEvents Channel for sending capture-related events. + * @property captureScreenController Controller for UI-related capture screen actions. + * @property snackBarController Controller for showing snackbars. + */ class CaptureControllerImpl( private val trackedCaptureUiState: MutableStateFlow, - private val captureUiState: StateFlow, private val viewModelScope: CoroutineScope, private val cameraSystem: CameraSystem, private val mediaRepository: MediaRepository, @@ -74,25 +85,7 @@ class CaptureControllerImpl( private var recordingJob: Job? = null override fun captureImage(contentResolver: ContentResolver) { - if (captureUiState.value is CaptureUiState.Ready && - (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == - ExternalCaptureMode.VideoCapture - ) { - snackBarController?.addSnackBarData( - SnackbarData( - cookie = "Image-ExternalVideoCaptureMode", - stringResource = R.string.toast_image_capture_external_unsupported, - withDismissAction = true, - testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG - ) - ) - return - } - - if (captureUiState.value is CaptureUiState.Ready && - (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == - ExternalCaptureMode.VideoCapture - ) { + if (externalCaptureMode == ExternalCaptureMode.VideoCapture) { snackBarController?.addSnackBarData( SnackbarData( cookie = "Image-ExternalVideoCaptureMode", @@ -156,10 +149,7 @@ class CaptureControllerImpl( } override fun startVideoRecording() { - if (captureUiState.value is CaptureUiState.Ready && - (captureUiState.value as CaptureUiState.Ready).externalCaptureMode == - ExternalCaptureMode.ImageCapture - ) { + if (externalCaptureMode == ExternalCaptureMode.ImageCapture) { Log.d(TAG, "externalVideoRecording") snackBarController?.addSnackBarData( SnackbarData( diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt index 8720c54b1..b316d6737 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenController.kt @@ -17,11 +17,46 @@ package com.google.jetpackcamera.ui.components.capture.controller import com.google.jetpackcamera.model.DeviceRotation +/** + * Interface for controlling actions on the capture screen. + */ interface CaptureScreenController { + /** + * Sets the display rotation. + * + * @param deviceRotation The device rotation to set. + */ fun setDisplayRotation(deviceRotation: DeviceRotation) + + /** + * Initiates a tap-to-focus action at the given coordinates. + * + * @param x The x-coordinate of the tap. + * @param y The y-coordinate of the tap. + */ fun tapToFocus(x: Float, y: Float) + + /** + * Enables or disables audio recording. + * + * @param shouldEnableAudio Whether audio should be enabled. + */ fun setAudioEnabled(shouldEnableAudio: Boolean) + + /** + * Updates the UI with the last captured media. + */ fun updateLastCapturedMedia() + + /** + * Transfers the media from the image well to the repository. + */ fun imageWellToRepository() + + /** + * Pauses or resumes video recording. + * + * @param shouldBePaused Whether the recording should be paused. + */ fun setPaused(shouldBePaused: Boolean) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt index 7d2865dc1..9fc5684cf 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/CaptureScreenControllerImpl.kt @@ -31,6 +31,15 @@ import kotlinx.coroutines.launch private const val TAG = "CaptureScreenControllerImpl" +/** + * Implementation of [CaptureScreenController] that handles UI events on the capture screen. + * + * @param viewModelScope The [CoroutineScope] for launching coroutines. + * @param cameraSystem The [CameraSystem] to interact with. + * @param trackedCaptureUiState The [MutableStateFlow] of the tracked capture UI state. + * @param mediaRepository The [MediaRepository] to interact with media. + * @param captureUiState The [StateFlow] of the capture UI state. + */ class CaptureScreenControllerImpl( private val viewModelScope: CoroutineScope, private val cameraSystem: CameraSystem, @@ -38,6 +47,7 @@ class CaptureScreenControllerImpl( private val mediaRepository: MediaRepository, private val captureUiState: StateFlow ) : CaptureScreenController { + override fun setDisplayRotation(deviceRotation: DeviceRotation) { viewModelScope.launch { cameraSystem.setDeviceRotation(deviceRotation) diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt index a70be0c07..28fe0f715 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/Utils.kt @@ -42,6 +42,15 @@ object Utils { } } + /** + * Determines the next [SaveLocation] and optional [IntProgress] for a capture. + * + * @param saveMode The current [SaveMode]. + * @param externalCaptureMode The current [ExternalCaptureMode]. + * @param externalCapturesCallback A callback to retrieve the save location and progress for + * external captures. + * @return A [Pair] containing the [SaveLocation] and optional [IntProgress]. + */ fun nextSaveLocation( saveMode: SaveMode, externalCaptureMode: ExternalCaptureMode, diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt index 2bb8242d3..b624d3435 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomController.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2026 The Android Open Source Project + * Copyright (C) 2024 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. @@ -17,7 +17,21 @@ package com.google.jetpackcamera.ui.components.capture.controller import com.google.jetpackcamera.model.CameraZoomRatio +/** + * Interface for controlling camera zoom. + */ interface ZoomController { + /** + * Sets the camera's zoom ratio. + * + * @param zoomRatio The [CameraZoomRatio] to set. + */ fun setZoomRatio(zoomRatio: CameraZoomRatio) + + /** + * Sets the target value for the zoom animation. + * + * @param targetValue The target zoom ratio for the animation, or null to clear it. + */ fun setZoomAnimationState(targetValue: Float?) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt index f0ed8c541..09fa44fec 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/controller/ZoomControllerImpl.kt @@ -21,10 +21,17 @@ import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +/** + * Implementation of [ZoomController] that updates the camera's zoom and tracked UI state. + * + * @param cameraSystem The camera system to update zoom on. + * @param trackedCaptureUiState State for tracking zoom changes. + */ class ZoomControllerImpl( private val cameraSystem: CameraSystem, private val trackedCaptureUiState: MutableStateFlow ) : ZoomController { + override fun setZoomRatio(zoomRatio: CameraZoomRatio) { cameraSystem.changeZoomRatio( newZoomState = zoomRatio diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt index fa1a2924d..bd3f26f9d 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugController.kt @@ -17,6 +17,10 @@ package com.google.jetpackcamera.ui.components.capture.debug.controller import com.google.jetpackcamera.model.TestPattern +/** + * This file contains the [DebugController] interface for managing debug actions. + */ + /** * Interface for controlling debug features. */ diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt index 93902c33f..74bf57fb6 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/debug/controller/DebugControllerImpl.kt @@ -21,6 +21,13 @@ import com.google.jetpackcamera.ui.uistate.capture.TrackedCaptureUiState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +/** + * Implementation of [DebugController] that interacts with [CameraSystem] and updates + * [trackedCaptureUiState]. + * + * @property cameraSystem The camera system to control. + * @property trackedCaptureUiState The state flow to update with debug information. + */ class DebugControllerImpl( private val cameraSystem: CameraSystem, private val trackedCaptureUiState: MutableStateFlow diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt index 30e761e69..a4271b698 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsController.kt @@ -25,15 +25,79 @@ import com.google.jetpackcamera.model.LensFacing import com.google.jetpackcamera.model.StreamConfig import com.google.jetpackcamera.ui.uistate.capture.compound.FocusedQuickSetting +/** + * This file contains the [QuickSettingsController] interface for managing quick settings actions. + */ + +/** + * Interface for controlling quick settings. + */ interface QuickSettingsController { + /** + * Toggles the visibility of the quick settings menu. + */ fun toggleQuickSettings() + + /** + * Sets the currently focused quick setting. + * + * @param focusedQuickSetting The quick setting to focus. + */ fun setFocusedSetting(focusedQuickSetting: FocusedQuickSetting) + + /** + * Sets the lens facing (front or back camera). + * + * @param lensFace The lens facing to set. + */ fun setLensFacing(lensFace: LensFacing) + + /** + * Sets the flash mode. + * + * @param flashMode The flash mode to set. + */ fun setFlash(flashMode: FlashMode) + + /** + * Sets the aspect ratio for capture. + * + * @param aspectRatio The aspect ratio to set. + */ fun setAspectRatio(aspectRatio: AspectRatio) + + /** + * Sets the stream configuration (e.g. preview, video capture). + * + * @param streamConfig The stream configuration to set. + */ fun setStreamConfig(streamConfig: StreamConfig) + + /** + * Sets the dynamic range (e.g. SDR, HDR). + * + * @param dynamicRange The dynamic range to set. + */ fun setDynamicRange(dynamicRange: DynamicRange) + + /** + * Sets the image output format (e.g. JPEG, RAW). + * + * @param imageOutputFormat The image format to set. + */ fun setImageFormat(imageOutputFormat: ImageOutputFormat) + + /** + * Sets the concurrent camera mode. + * + * @param concurrentCameraMode The concurrent camera mode to set. + */ fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) + + /** + * Sets the capture mode (e.g. multi-camera, single-camera). + * + * @param captureMode The capture mode to set. + */ fun setCaptureMode(captureMode: CaptureMode) } diff --git a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt index 2943013b2..ea34b8670 100644 --- a/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt +++ b/ui/components/capture/src/main/java/com/google/jetpackcamera/ui/components/capture/quicksettings/controller/QuickSettingsControllerImpl.kt @@ -32,6 +32,15 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +/** + * Implementation of [QuickSettingsController] that interacts with [CameraSystem] and updates + * [trackedCaptureUiState]. + * + * @property trackedCaptureUiState The state flow to update with quick settings information. + * @property viewModelScope The coroutine scope for launching camera operations. + * @property cameraSystem The camera system to control. + * @property externalCaptureMode The current external capture mode. + */ class QuickSettingsControllerImpl( private val trackedCaptureUiState: MutableStateFlow, private val viewModelScope: CoroutineScope, From 231f11a7cc92c05de0f9eed11d3d4da7b1f6e911 Mon Sep 17 00:00:00 2001 From: David Jia Date: Wed, 25 Feb 2026 15:22:49 -0800 Subject: [PATCH 6/6] spotless --- .../com/google/jetpackcamera/feature/preview/PreviewScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt index ce6f736b2..9287567b8 100644 --- a/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt +++ b/feature/preview/src/main/java/com/google/jetpackcamera/feature/preview/PreviewScreen.kt @@ -90,9 +90,9 @@ import com.google.jetpackcamera.ui.components.capture.TestableSnackbar import com.google.jetpackcamera.ui.components.capture.VIDEO_QUALITY_TAG import com.google.jetpackcamera.ui.components.capture.VideoQualityIcon import com.google.jetpackcamera.ui.components.capture.ZoomButtonRow +import com.google.jetpackcamera.ui.components.capture.ZoomStateManager import com.google.jetpackcamera.ui.components.capture.controller.CaptureController import com.google.jetpackcamera.ui.components.capture.controller.CaptureScreenController -import com.google.jetpackcamera.ui.components.capture.ZoomStateManager import com.google.jetpackcamera.ui.components.capture.debouncedOrientationFlow import com.google.jetpackcamera.ui.components.capture.debug.DebugOverlay import com.google.jetpackcamera.ui.components.capture.debug.controller.DebugController