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) + } + } + ) +}