1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.jetpackcamera.feature.preview
17 
18 import android.content.ContentResolver
19 import android.net.Uri
20 import android.os.SystemClock
21 import android.util.Log
22 import androidx.camera.core.SurfaceRequest
23 import androidx.lifecycle.ViewModel
24 import androidx.lifecycle.viewModelScope
25 import androidx.tracing.Trace
26 import androidx.tracing.traceAsync
27 import com.google.jetpackcamera.core.camera.CameraUseCase
28 import com.google.jetpackcamera.core.common.traceFirstFramePreview
29 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
30 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_FAILURE_TAG
31 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_SUCCESS_TAG
32 import com.google.jetpackcamera.feature.preview.ui.SnackbarData
33 import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
34 import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_FAILURE_TAG
35 import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_SUCCESS_TAG
36 import com.google.jetpackcamera.settings.ConstraintsRepository
37 import com.google.jetpackcamera.settings.SettingsRepository
38 import com.google.jetpackcamera.settings.model.AspectRatio
39 import com.google.jetpackcamera.settings.model.CameraAppSettings
40 import com.google.jetpackcamera.settings.model.CameraConstraints
41 import com.google.jetpackcamera.settings.model.CaptureMode
42 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
43 import com.google.jetpackcamera.settings.model.DeviceRotation
44 import com.google.jetpackcamera.settings.model.DynamicRange
45 import com.google.jetpackcamera.settings.model.FlashMode
46 import com.google.jetpackcamera.settings.model.ImageOutputFormat
47 import com.google.jetpackcamera.settings.model.LensFacing
48 import com.google.jetpackcamera.settings.model.LowLightBoost
49 import com.google.jetpackcamera.settings.model.Stabilization
50 import com.google.jetpackcamera.settings.model.SystemConstraints
51 import com.google.jetpackcamera.settings.model.forCurrentLens
52 import dagger.assisted.Assisted
53 import dagger.assisted.AssistedFactory
54 import dagger.assisted.AssistedInject
55 import dagger.hilt.android.lifecycle.HiltViewModel
56 import kotlin.reflect.KProperty
57 import kotlin.reflect.full.memberProperties
58 import kotlin.time.Duration.Companion.seconds
59 import kotlinx.atomicfu.atomic
60 import kotlinx.coroutines.CoroutineStart
61 import kotlinx.coroutines.Deferred
62 import kotlinx.coroutines.Job
63 import kotlinx.coroutines.async
64 import kotlinx.coroutines.delay
65 import kotlinx.coroutines.flow.MutableStateFlow
66 import kotlinx.coroutines.flow.StateFlow
67 import kotlinx.coroutines.flow.asStateFlow
68 import kotlinx.coroutines.flow.combine
69 import kotlinx.coroutines.flow.filterNotNull
70 import kotlinx.coroutines.flow.first
71 import kotlinx.coroutines.flow.transform
72 import kotlinx.coroutines.flow.transformWhile
73 import kotlinx.coroutines.flow.update
74 import kotlinx.coroutines.launch
75 
76 private const val TAG = "PreviewViewModel"
77 private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture"
78 
79 /**
80  * [ViewModel] for [PreviewScreen].
81  */
82 @HiltViewModel(assistedFactory = PreviewViewModel.Factory::class)
83 class PreviewViewModel @AssistedInject constructor(
84     @Assisted val previewMode: PreviewMode,
85     @Assisted val isDebugMode: Boolean,
86     private val cameraUseCase: CameraUseCase,
87     private val settingsRepository: SettingsRepository,
88     private val constraintsRepository: ConstraintsRepository
89 ) : ViewModel() {
90     private val _previewUiState: MutableStateFlow<PreviewUiState> =
91         MutableStateFlow(PreviewUiState.NotReady)
92 
93     val previewUiState: StateFlow<PreviewUiState> =
94         _previewUiState.asStateFlow()
95 
96     val surfaceRequest: StateFlow<SurfaceRequest?> = cameraUseCase.getSurfaceRequest()
97 
98     private var runningCameraJob: Job? = null
99 
100     private var recordingJob: Job? = null
101 
102     val screenFlash = ScreenFlash(cameraUseCase, viewModelScope)
103 
104     private val snackBarCount = atomic(0)
105     private val videoCaptureStartedCount = atomic(0)
106 
107     // Eagerly initialize the CameraUseCase and encapsulate in a Deferred that can be
108     // used to ensure we don't start the camera before initialization is complete.
109     private var initializationDeferred: Deferred<Unit> = viewModelScope.async {
110         cameraUseCase.initialize(
111             cameraAppSettings = settingsRepository.defaultCameraAppSettings.first(),
112             previewMode.toUseCaseMode(),
113             isDebugMode
114         )
115     }
116 
117     init {
118         viewModelScope.launch {
119             launch {
120                 var oldCameraAppSettings: CameraAppSettings? = null
121                 settingsRepository.defaultCameraAppSettings.transform { new ->
122                     val old = oldCameraAppSettings
123                     if (old != null) {
124                         emit(getSettingsDiff(old, new))
125                     }
126                     oldCameraAppSettings = new
127                 }.collect { diffQueue ->
128                     applySettingsDiff(diffQueue)
129                 }
130             }
131             combine(
132                 cameraUseCase.getCurrentSettings().filterNotNull(),
133                 constraintsRepository.systemConstraints.filterNotNull(),
134                 cameraUseCase.getCurrentCameraState()
135             ) { cameraAppSettings, systemConstraints, cameraState ->
136                 _previewUiState.update { old ->
137                     when (old) {
138                         is PreviewUiState.Ready ->
139                             old.copy(
140                                 currentCameraSettings = cameraAppSettings,
141                                 systemConstraints = systemConstraints,
142                                 zoomScale = cameraState.zoomScale,
143                                 sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp,
144                                 captureModeToggleUiState = getCaptureToggleUiState(
145                                     systemConstraints,
146                                     cameraAppSettings
147                                 ),
148                                 isDebugMode = isDebugMode,
149                                 currentLogicalCameraId = cameraState.debugInfo.logicalCameraId,
150                                 currentPhysicalCameraId = cameraState.debugInfo.physicalCameraId
151                             )
152 
153                         is PreviewUiState.NotReady ->
154                             PreviewUiState.Ready(
155                                 currentCameraSettings = cameraAppSettings,
156                                 systemConstraints = systemConstraints,
157                                 zoomScale = cameraState.zoomScale,
158                                 sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp,
159                                 previewMode = previewMode,
160                                 captureModeToggleUiState = getCaptureToggleUiState(
161                                     systemConstraints,
162                                     cameraAppSettings
163                                 ),
164                                 isDebugMode = isDebugMode,
165                                 currentLogicalCameraId = cameraState.debugInfo.logicalCameraId,
166                                 currentPhysicalCameraId = cameraState.debugInfo.physicalCameraId
167                             )
168                     }
169                 }
170             }.collect {}
171         }
172     }
173 
174     private fun PreviewMode.toUseCaseMode() = when (this) {
175         is PreviewMode.ExternalImageCaptureMode -> CameraUseCase.UseCaseMode.IMAGE_ONLY
176         is PreviewMode.ExternalVideoCaptureMode -> CameraUseCase.UseCaseMode.VIDEO_ONLY
177         is PreviewMode.StandardMode -> CameraUseCase.UseCaseMode.STANDARD
178     }
179 
180     /**
181      * Returns the difference between two [CameraAppSettings] as a mapping of <[KProperty], [Any]>.
182      */
183     private fun getSettingsDiff(
184         oldCameraAppSettings: CameraAppSettings,
185         newCameraAppSettings: CameraAppSettings
186     ): Map<KProperty<Any?>, Any?> = buildMap<KProperty<Any?>, Any?> {
187         CameraAppSettings::class.memberProperties.forEach { property ->
188             if (property.get(oldCameraAppSettings) != property.get(newCameraAppSettings)) {
189                 put(property, property.get(newCameraAppSettings))
190             }
191         }
192     }
193 
194     /**
195      * Iterates through a queue of [Pair]<[KProperty], [Any]> and attempt to apply them to
196      * [CameraUseCase].
197      */
198     private suspend fun applySettingsDiff(diffSettingsMap: Map<KProperty<Any?>, Any?>) {
199         diffSettingsMap.entries.forEach { entry ->
200             when (entry.key) {
201                 CameraAppSettings::cameraLensFacing -> {
202                     cameraUseCase.setLensFacing(entry.value as LensFacing)
203                 }
204 
205                 CameraAppSettings::flashMode -> {
206                     cameraUseCase.setFlashMode(entry.value as FlashMode)
207                 }
208 
209                 CameraAppSettings::captureMode -> {
210                     cameraUseCase.setCaptureMode(entry.value as CaptureMode)
211                 }
212 
213                 CameraAppSettings::aspectRatio -> {
214                     cameraUseCase.setAspectRatio(entry.value as AspectRatio)
215                 }
216 
217                 CameraAppSettings::previewStabilization -> {
218                     cameraUseCase.setPreviewStabilization(entry.value as Stabilization)
219                 }
220 
221                 CameraAppSettings::videoCaptureStabilization -> {
222                     cameraUseCase.setVideoCaptureStabilization(
223                         entry.value as Stabilization
224                     )
225                 }
226 
227                 CameraAppSettings::targetFrameRate -> {
228                     cameraUseCase.setTargetFrameRate(entry.value as Int)
229                 }
230 
231                 CameraAppSettings::darkMode -> {}
232 
233                 else -> TODO("Unhandled CameraAppSetting $entry")
234             }
235         }
236     }
237 
238     private fun getCaptureToggleUiState(
239         systemConstraints: SystemConstraints,
240         cameraAppSettings: CameraAppSettings
241     ): CaptureModeToggleUiState {
242         val cameraConstraints: CameraConstraints? = systemConstraints.forCurrentLens(
243             cameraAppSettings
244         )
245         val hdrDynamicRangeSupported = cameraConstraints?.let {
246             it.supportedDynamicRanges.size > 1
247         } ?: false
248         val hdrImageFormatSupported =
249             cameraConstraints?.supportedImageFormatsMap?.get(cameraAppSettings.captureMode)?.let {
250                 it.size > 1
251             } ?: false
252         val isShown = previewMode is PreviewMode.ExternalImageCaptureMode ||
253             previewMode is PreviewMode.ExternalVideoCaptureMode ||
254             cameraAppSettings.imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR ||
255             cameraAppSettings.dynamicRange == DynamicRange.HLG10 ||
256             cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL
257         val enabled = previewMode !is PreviewMode.ExternalImageCaptureMode &&
258             previewMode !is PreviewMode.ExternalVideoCaptureMode &&
259             hdrDynamicRangeSupported &&
260             hdrImageFormatSupported &&
261             cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.OFF
262         return if (isShown) {
263             val currentMode = if (
264                 cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.OFF &&
265                 previewMode is PreviewMode.ExternalImageCaptureMode ||
266                 cameraAppSettings.imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR
267             ) {
268                 CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_IMAGE
269             } else {
270                 CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_VIDEO
271             }
272             if (enabled) {
273                 CaptureModeToggleUiState.Enabled(currentMode)
274             } else {
275                 CaptureModeToggleUiState.Disabled(
276                     currentMode,
277                     getCaptureToggleUiStateDisabledReason(
278                         currentMode,
279                         hdrDynamicRangeSupported,
280                         hdrImageFormatSupported,
281                         systemConstraints,
282                         cameraAppSettings.cameraLensFacing,
283                         cameraAppSettings.captureMode,
284                         cameraAppSettings.concurrentCameraMode
285                     )
286                 )
287             }
288         } else {
289             CaptureModeToggleUiState.Invisible
290         }
291     }
292 
293     private fun getCaptureToggleUiStateDisabledReason(
294         captureModeToggleUiState: CaptureModeToggleUiState.ToggleMode,
295         hdrDynamicRangeSupported: Boolean,
296         hdrImageFormatSupported: Boolean,
297         systemConstraints: SystemConstraints,
298         currentLensFacing: LensFacing,
299         currentCaptureMode: CaptureMode,
300         concurrentCameraMode: ConcurrentCameraMode
301     ): CaptureModeToggleUiState.DisabledReason {
302         when (captureModeToggleUiState) {
303             CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_VIDEO -> {
304                 if (previewMode is PreviewMode.ExternalVideoCaptureMode) {
305                     return CaptureModeToggleUiState.DisabledReason
306                         .IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED
307                 }
308 
309                 if (concurrentCameraMode == ConcurrentCameraMode.DUAL) {
310                     return CaptureModeToggleUiState.DisabledReason
311                         .IMAGE_CAPTURE_UNSUPPORTED_CONCURRENT_CAMERA
312                 }
313 
314                 if (!hdrImageFormatSupported) {
315                     // First check if Ultra HDR image is supported on other capture modes
316                     if (systemConstraints
317                             .perLensConstraints[currentLensFacing]
318                             ?.supportedImageFormatsMap
319                             ?.anySupportsUltraHdr { it != currentCaptureMode } == true
320                     ) {
321                         return when (currentCaptureMode) {
322                             CaptureMode.MULTI_STREAM ->
323                                 CaptureModeToggleUiState.DisabledReason
324                                     .HDR_IMAGE_UNSUPPORTED_ON_MULTI_STREAM
325 
326                             CaptureMode.SINGLE_STREAM ->
327                                 CaptureModeToggleUiState.DisabledReason
328                                     .HDR_IMAGE_UNSUPPORTED_ON_SINGLE_STREAM
329                         }
330                     }
331 
332                     // Check if any other lens supports HDR image
333                     if (systemConstraints.anySupportsUltraHdr { it != currentLensFacing }) {
334                         return CaptureModeToggleUiState.DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_LENS
335                     }
336 
337                     // No lenses support HDR image on device
338                     return CaptureModeToggleUiState.DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_DEVICE
339                 }
340 
341                 throw RuntimeException("Unknown DisabledReason for video mode.")
342             }
343 
344             CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_IMAGE -> {
345                 if (previewMode is PreviewMode.ExternalImageCaptureMode) {
346                     return CaptureModeToggleUiState.DisabledReason
347                         .VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED
348                 }
349 
350                 if (!hdrDynamicRangeSupported) {
351                     if (systemConstraints.anySupportsHdrDynamicRange { it != currentLensFacing }) {
352                         return CaptureModeToggleUiState.DisabledReason.HDR_VIDEO_UNSUPPORTED_ON_LENS
353                     }
354                     return CaptureModeToggleUiState.DisabledReason.HDR_VIDEO_UNSUPPORTED_ON_DEVICE
355                 }
356 
357                 throw RuntimeException("Unknown DisabledReason for image mode.")
358             }
359         }
360     }
361 
362     private fun SystemConstraints.anySupportsHdrDynamicRange(
363         lensFilter: (LensFacing) -> Boolean
364     ): Boolean = perLensConstraints.asSequence().firstOrNull {
365         lensFilter(it.key) && it.value.supportedDynamicRanges.size > 1
366     } != null
367 
368     private fun Map<CaptureMode, Set<ImageOutputFormat>>.anySupportsUltraHdr(
369         captureModeFilter: (CaptureMode) -> Boolean
370     ): Boolean = asSequence().firstOrNull {
371         captureModeFilter(it.key) && it.value.contains(ImageOutputFormat.JPEG_ULTRA_HDR)
372     } != null
373 
374     private fun SystemConstraints.anySupportsUltraHdr(
375         captureModeFilter: (CaptureMode) -> Boolean = { true },
376         lensFilter: (LensFacing) -> Boolean
377     ): Boolean = perLensConstraints.asSequence().firstOrNull { lensConstraints ->
378         lensFilter(lensConstraints.key) &&
379             lensConstraints.value.supportedImageFormatsMap.anySupportsUltraHdr {
380                 captureModeFilter(it)
381             }
382     } != null
383 
384     fun startCamera() {
385         Log.d(TAG, "startCamera")
386         stopCamera()
387         runningCameraJob = viewModelScope.launch {
388             if (Trace.isEnabled()) {
389                 launch(start = CoroutineStart.UNDISPATCHED) {
390                     val startTraceTimestamp: Long = SystemClock.elapsedRealtimeNanos()
391                     traceFirstFramePreview(cookie = 1) {
392                         _previewUiState.transformWhile {
393                             var continueCollecting = true
394                             (it as? PreviewUiState.Ready)?.let { uiState ->
395                                 if (uiState.sessionFirstFrameTimestamp > startTraceTimestamp) {
396                                     emit(Unit)
397                                     continueCollecting = false
398                                 }
399                             }
400                             continueCollecting
401                         }.collect {}
402                     }
403                 }
404             }
405             // Ensure CameraUseCase is initialized before starting camera
406             initializationDeferred.await()
407             // TODO(yasith): Handle Exceptions from binding use cases
408             cameraUseCase.runCamera()
409         }
410     }
411 
412     fun stopCamera() {
413         Log.d(TAG, "stopCamera")
414         runningCameraJob?.apply {
415             if (isActive) {
416                 cancel()
417             }
418         }
419     }
420 
421     fun setFlash(flashMode: FlashMode) {
422         viewModelScope.launch {
423             // apply to cameraUseCase
424             cameraUseCase.setFlashMode(flashMode)
425         }
426     }
427 
428     fun setAspectRatio(aspectRatio: AspectRatio) {
429         viewModelScope.launch {
430             cameraUseCase.setAspectRatio(aspectRatio)
431         }
432     }
433 
434     fun setCaptureMode(captureMode: CaptureMode) {
435         viewModelScope.launch {
436             cameraUseCase.setCaptureMode(captureMode)
437         }
438     }
439 
440     /** Sets the camera to a designated lens facing */
441     fun setLensFacing(newLensFacing: LensFacing) {
442         viewModelScope.launch {
443             // apply to cameraUseCase
444             cameraUseCase.setLensFacing(newLensFacing)
445         }
446     }
447 
448     fun setAudioMuted(shouldMuteAudio: Boolean) {
449         viewModelScope.launch {
450             cameraUseCase.setAudioMuted(shouldMuteAudio)
451         }
452 
453         Log.d(
454             TAG,
455             "Toggle Audio ${
456                 (previewUiState.value as PreviewUiState.Ready)
457                     .currentCameraSettings.audioMuted
458             }"
459         )
460     }
461 
462     private fun showExternalVideoCaptureUnsupportedToast() {
463         viewModelScope.launch {
464             _previewUiState.update { old ->
465                 (old as? PreviewUiState.Ready)?.copy(
466                     snackBarToShow = SnackbarData(
467                         cookie = "Image-ExternalVideoCaptureMode",
468                         stringResource = R.string.toast_image_capture_external_unsupported,
469                         withDismissAction = true,
470                         testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
471                     )
472                 ) ?: old
473             }
474         }
475     }
476 
477     fun captureImage() {
478         if (previewUiState.value is PreviewUiState.Ready &&
479             (previewUiState.value as PreviewUiState.Ready).previewMode is
480                 PreviewMode.ExternalVideoCaptureMode
481         ) {
482             showExternalVideoCaptureUnsupportedToast()
483             return
484         }
485         Log.d(TAG, "captureImage")
486         viewModelScope.launch {
487             captureImageInternal(
488                 doTakePicture = {
489                     cameraUseCase.takePicture {
490                         _previewUiState.update { old ->
491                             (old as? PreviewUiState.Ready)?.copy(
492                                 lastBlinkTimeStamp = System.currentTimeMillis()
493                             ) ?: old
494                         }
495                     }
496                 }
497             )
498         }
499     }
500 
501     fun captureImageWithUri(
502         contentResolver: ContentResolver,
503         imageCaptureUri: Uri?,
504         ignoreUri: Boolean = false,
505         onImageCapture: (ImageCaptureEvent) -> Unit
506     ) {
507         if (previewUiState.value is PreviewUiState.Ready &&
508             (previewUiState.value as PreviewUiState.Ready).previewMode is
509                 PreviewMode.ExternalVideoCaptureMode
510         ) {
511             showExternalVideoCaptureUnsupportedToast()
512             return
513         }
514 
515         if (previewUiState.value is PreviewUiState.Ready &&
516             (previewUiState.value as PreviewUiState.Ready).previewMode is
517                 PreviewMode.ExternalVideoCaptureMode
518         ) {
519             viewModelScope.launch {
520                 _previewUiState.update { old ->
521                     (old as? PreviewUiState.Ready)?.copy(
522                         snackBarToShow = SnackbarData(
523                             cookie = "Image-ExternalVideoCaptureMode",
524                             stringResource = R.string.toast_image_capture_external_unsupported,
525                             withDismissAction = true,
526                             testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
527                         )
528                     ) ?: old
529                 }
530             }
531             return
532         }
533         Log.d(TAG, "captureImageWithUri")
534         viewModelScope.launch {
535             captureImageInternal(
536                 doTakePicture = {
537                     cameraUseCase.takePicture({
538                         _previewUiState.update { old ->
539                             (old as? PreviewUiState.Ready)?.copy(
540                                 lastBlinkTimeStamp = System.currentTimeMillis()
541                             ) ?: old
542                         }
543                     }, contentResolver, imageCaptureUri, ignoreUri).savedUri
544                 },
545                 onSuccess = { savedUri -> onImageCapture(ImageCaptureEvent.ImageSaved(savedUri)) },
546                 onFailure = { exception ->
547                     onImageCapture(ImageCaptureEvent.ImageCaptureError(exception))
548                 }
549             )
550         }
551     }
552 
553     private suspend fun <T> captureImageInternal(
554         doTakePicture: suspend () -> T,
555         onSuccess: (T) -> Unit = {},
556         onFailure: (exception: Exception) -> Unit = {}
557     ) {
558         val cookieInt = snackBarCount.incrementAndGet()
559         val cookie = "Image-$cookieInt"
560         try {
561             traceAsync(IMAGE_CAPTURE_TRACE, cookieInt) {
562                 doTakePicture()
563             }.also { result ->
564                 onSuccess(result)
565             }
566             Log.d(TAG, "cameraUseCase.takePicture success")
567             SnackbarData(
568                 cookie = cookie,
569                 stringResource = R.string.toast_image_capture_success,
570                 withDismissAction = true,
571                 testTag = IMAGE_CAPTURE_SUCCESS_TAG
572             )
573         } catch (exception: Exception) {
574             onFailure(exception)
575             Log.d(TAG, "cameraUseCase.takePicture error", exception)
576             SnackbarData(
577                 cookie = cookie,
578                 stringResource = R.string.toast_capture_failure,
579                 withDismissAction = true,
580                 testTag = IMAGE_CAPTURE_FAILURE_TAG
581             )
582         }.also { snackBarData ->
583             _previewUiState.update { old ->
584                 (old as? PreviewUiState.Ready)?.copy(
585                     // todo: remove snackBar after postcapture screen implemented
586                     snackBarToShow = snackBarData
587                 ) ?: old
588             }
589         }
590     }
591 
592     fun showSnackBarForDisabledHdrToggle(disabledReason: CaptureModeToggleUiState.DisabledReason) {
593         val cookieInt = snackBarCount.incrementAndGet()
594         val cookie = "DisabledHdrToggle-$cookieInt"
595         viewModelScope.launch {
596             _previewUiState.update { old ->
597                 (old as? PreviewUiState.Ready)?.copy(
598                     snackBarToShow = SnackbarData(
599                         cookie = cookie,
600                         stringResource = disabledReason.reasonTextResId,
601                         withDismissAction = true,
602                         testTag = disabledReason.testTag
603                     )
604                 ) ?: old
605             }
606         }
607     }
608 
609     fun startVideoRecording(
610         videoCaptureUri: Uri?,
611         shouldUseUri: Boolean,
612         onVideoCapture: (VideoCaptureEvent) -> Unit
613     ) {
614         if (previewUiState.value is PreviewUiState.Ready &&
615             (previewUiState.value as PreviewUiState.Ready).previewMode is
616                 PreviewMode.ExternalImageCaptureMode
617         ) {
618             Log.d(TAG, "externalVideoRecording")
619             viewModelScope.launch {
620                 _previewUiState.update { old ->
621                     (old as? PreviewUiState.Ready)?.copy(
622                         snackBarToShow = SnackbarData(
623                             cookie = "Video-ExternalImageCaptureMode",
624                             stringResource = R.string.toast_video_capture_external_unsupported,
625                             withDismissAction = true,
626                             testTag = VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
627                         )
628                     ) ?: old
629                 }
630             }
631             return
632         }
633         Log.d(TAG, "startVideoRecording")
634         recordingJob = viewModelScope.launch {
635             val cookie = "Video-${videoCaptureStartedCount.incrementAndGet()}"
636             try {
637                 cameraUseCase.startVideoRecording(videoCaptureUri, shouldUseUri) {
638                     var audioAmplitude = 0.0
639                     var snackbarToShow: SnackbarData? = null
640                     when (it) {
641                         is CameraUseCase.OnVideoRecordEvent.OnVideoRecorded -> {
642                             Log.d(TAG, "cameraUseCase.startRecording OnVideoRecorded")
643                             onVideoCapture(VideoCaptureEvent.VideoSaved(it.savedUri))
644                             snackbarToShow = SnackbarData(
645                                 cookie = cookie,
646                                 stringResource = R.string.toast_video_capture_success,
647                                 withDismissAction = true,
648                                 testTag = VIDEO_CAPTURE_SUCCESS_TAG
649                             )
650                         }
651 
652                         is CameraUseCase.OnVideoRecordEvent.OnVideoRecordError -> {
653                             Log.d(TAG, "cameraUseCase.startRecording OnVideoRecordError")
654                             onVideoCapture(VideoCaptureEvent.VideoCaptureError(it.error))
655                             snackbarToShow = SnackbarData(
656                                 cookie = cookie,
657                                 stringResource = R.string.toast_video_capture_failure,
658                                 withDismissAction = true,
659                                 testTag = VIDEO_CAPTURE_FAILURE_TAG
660                             )
661                         }
662 
663                         is CameraUseCase.OnVideoRecordEvent.OnVideoRecordStatus -> {
664                             audioAmplitude = it.audioAmplitude
665                         }
666                     }
667 
668                     viewModelScope.launch {
669                         _previewUiState.update { old ->
670                             (old as? PreviewUiState.Ready)?.copy(
671                                 snackBarToShow = snackbarToShow,
672                                 audioAmplitude = audioAmplitude
673                             ) ?: old
674                         }
675                     }
676                 }
677                 _previewUiState.update { old ->
678                     (old as? PreviewUiState.Ready)?.copy(
679                         videoRecordingState = VideoRecordingState.ACTIVE
680                     ) ?: old
681                 }
682                 Log.d(TAG, "cameraUseCase.startRecording success")
683             } catch (exception: IllegalStateException) {
684                 Log.d(TAG, "cameraUseCase.startVideoRecording error", exception)
685             }
686         }
687     }
688 
689     fun stopVideoRecording() {
690         Log.d(TAG, "stopVideoRecording")
691         viewModelScope.launch {
692             _previewUiState.update { old ->
693                 (old as? PreviewUiState.Ready)?.copy(
694                     videoRecordingState = VideoRecordingState.INACTIVE
695                 ) ?: old
696             }
697         }
698         cameraUseCase.stopVideoRecording()
699         recordingJob?.cancel()
700     }
701 
702     fun setZoomScale(scale: Float) {
703         cameraUseCase.setZoomScale(scale = scale)
704     }
705 
706     fun setDynamicRange(dynamicRange: DynamicRange) {
707         viewModelScope.launch {
708             cameraUseCase.setDynamicRange(dynamicRange)
709         }
710     }
711 
712     fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) {
713         viewModelScope.launch {
714             cameraUseCase.setConcurrentCameraMode(concurrentCameraMode)
715         }
716     }
717 
718     fun setLowLightBoost(lowLightBoost: LowLightBoost) {
719         viewModelScope.launch {
720             cameraUseCase.setLowLightBoost(lowLightBoost)
721         }
722     }
723 
724     fun setImageFormat(imageFormat: ImageOutputFormat) {
725         viewModelScope.launch {
726             cameraUseCase.setImageFormat(imageFormat)
727         }
728     }
729 
730     // modify ui values
731     fun toggleQuickSettings() {
732         viewModelScope.launch {
733             _previewUiState.update { old ->
734                 (old as? PreviewUiState.Ready)?.copy(
735                     quickSettingsIsOpen = !old.quickSettingsIsOpen
736                 ) ?: old
737             }
738         }
739     }
740 
741     fun tapToFocus(x: Float, y: Float) {
742         Log.d(TAG, "tapToFocus")
743         viewModelScope.launch {
744             cameraUseCase.tapToFocus(x, y)
745         }
746     }
747 
748     /**
749      * Sets current value of [PreviewUiState.Ready.toastMessageToShow] to null.
750      */
751     fun onToastShown() {
752         viewModelScope.launch {
753             // keeps the composable up on screen longer to be detected by UiAutomator
754             delay(2.seconds)
755             _previewUiState.update { old ->
756                 (old as? PreviewUiState.Ready)?.copy(
757                     toastMessageToShow = null
758                 ) ?: old
759             }
760         }
761     }
762 
763     fun onSnackBarResult(cookie: String) {
764         viewModelScope.launch {
765             _previewUiState.update { old ->
766                 (old as? PreviewUiState.Ready)?.snackBarToShow?.let {
767                     if (it.cookie == cookie) {
768                         // If the latest snackbar had a result, then clear snackBarToShow
769                         old.copy(snackBarToShow = null)
770                     } else {
771                         old
772                     }
773                 } ?: old
774             }
775         }
776     }
777 
778     fun setDisplayRotation(deviceRotation: DeviceRotation) {
779         viewModelScope.launch {
780             cameraUseCase.setDeviceRotation(deviceRotation)
781         }
782     }
783 
784     @AssistedFactory
785     interface Factory {
786         fun create(previewMode: PreviewMode, isDebugMode: Boolean): PreviewViewModel
787     }
788 
789     sealed interface ImageCaptureEvent {
790         data class ImageSaved(
791             val savedUri: Uri? = null
792         ) : ImageCaptureEvent
793 
794         data class ImageCaptureError(
795             val exception: Exception
796         ) : ImageCaptureEvent
797     }
798 
799     sealed interface VideoCaptureEvent {
800         data class VideoSaved(
801             val savedUri: Uri
802         ) : VideoCaptureEvent
803 
804         data class VideoCaptureError(
805             val error: Throwable?
806         ) : VideoCaptureEvent
807     }
808 }
809