• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.util.Size
23 import androidx.camera.core.SurfaceRequest
24 import androidx.lifecycle.ViewModel
25 import androidx.lifecycle.viewModelScope
26 import androidx.tracing.Trace
27 import androidx.tracing.traceAsync
28 import com.google.jetpackcamera.core.camera.CameraState
29 import com.google.jetpackcamera.core.camera.CameraUseCase
30 import com.google.jetpackcamera.core.camera.VideoRecordingState
31 import com.google.jetpackcamera.core.common.traceFirstFramePreview
32 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
33 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_FAILURE_TAG
34 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_SUCCESS_TAG
35 import com.google.jetpackcamera.feature.preview.ui.ImageWellUiState
36 import com.google.jetpackcamera.feature.preview.ui.SnackbarData
37 import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
38 import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_FAILURE_TAG
39 import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_SUCCESS_TAG
40 import com.google.jetpackcamera.settings.ConstraintsRepository
41 import com.google.jetpackcamera.settings.SettingsRepository
42 import com.google.jetpackcamera.settings.model.AspectRatio
43 import com.google.jetpackcamera.settings.model.CameraAppSettings
44 import com.google.jetpackcamera.settings.model.CameraConstraints
45 import com.google.jetpackcamera.settings.model.CaptureMode
46 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
47 import com.google.jetpackcamera.settings.model.DeviceRotation
48 import com.google.jetpackcamera.settings.model.DynamicRange
49 import com.google.jetpackcamera.settings.model.FlashMode
50 import com.google.jetpackcamera.settings.model.ImageOutputFormat
51 import com.google.jetpackcamera.settings.model.LensFacing
52 import com.google.jetpackcamera.settings.model.LowLightBoostState
53 import com.google.jetpackcamera.settings.model.StabilizationMode
54 import com.google.jetpackcamera.settings.model.StreamConfig
55 import com.google.jetpackcamera.settings.model.SystemConstraints
56 import com.google.jetpackcamera.settings.model.VideoQuality
57 import com.google.jetpackcamera.settings.model.forCurrentLens
58 import dagger.assisted.Assisted
59 import dagger.assisted.AssistedFactory
60 import dagger.assisted.AssistedInject
61 import dagger.hilt.android.lifecycle.HiltViewModel
62 import kotlin.reflect.KProperty
63 import kotlin.reflect.full.memberProperties
64 import kotlin.time.Duration.Companion.seconds
65 import kotlinx.atomicfu.atomic
66 import kotlinx.coroutines.CoroutineStart
67 import kotlinx.coroutines.Deferred
68 import kotlinx.coroutines.Job
69 import kotlinx.coroutines.async
70 import kotlinx.coroutines.delay
71 import kotlinx.coroutines.flow.MutableStateFlow
72 import kotlinx.coroutines.flow.StateFlow
73 import kotlinx.coroutines.flow.asStateFlow
74 import kotlinx.coroutines.flow.combine
75 import kotlinx.coroutines.flow.distinctUntilChanged
76 import kotlinx.coroutines.flow.filterNotNull
77 import kotlinx.coroutines.flow.first
78 import kotlinx.coroutines.flow.transform
79 import kotlinx.coroutines.flow.transformWhile
80 import kotlinx.coroutines.flow.update
81 import kotlinx.coroutines.launch
82 
83 private const val TAG = "PreviewViewModel"
84 private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture"
85 
86 /**
87  * [ViewModel] for [PreviewScreen].
88  */
89 @HiltViewModel(assistedFactory = PreviewViewModel.Factory::class)
90 class PreviewViewModel @AssistedInject constructor(
91     @Assisted val previewMode: PreviewMode,
92     @Assisted val isDebugMode: Boolean,
93     private val cameraUseCase: CameraUseCase,
94     private val settingsRepository: SettingsRepository,
95     private val constraintsRepository: ConstraintsRepository
96 ) : ViewModel() {
97     private val _previewUiState: MutableStateFlow<PreviewUiState> =
98         MutableStateFlow(PreviewUiState.NotReady)
99     private val lockedRecordingState: MutableStateFlow<Boolean> = MutableStateFlow(false)
100 
101     val previewUiState: StateFlow<PreviewUiState> =
102         _previewUiState.asStateFlow()
103 
104     val surfaceRequest: StateFlow<SurfaceRequest?> = cameraUseCase.getSurfaceRequest()
105 
106     private var runningCameraJob: Job? = null
107 
108     private var recordingJob: Job? = null
109 
110     private var externalUriIndex: Int = 0
111 
112     private var cameraPropertiesJSON = ""
113 
114     val screenFlash = ScreenFlash(cameraUseCase, viewModelScope)
115 
116     private val snackBarCount = atomic(0)
117     private val videoCaptureStartedCount = atomic(0)
118 
119     // Eagerly initialize the CameraUseCase and encapsulate in a Deferred that can be
120     // used to ensure we don't start the camera before initialization is complete.
121     private var initializationDeferred: Deferred<Unit> = viewModelScope.async {
122         cameraUseCase.initialize(
123             cameraAppSettings = settingsRepository.defaultCameraAppSettings.first()
124                 .applyPreviewMode(previewMode),
125             isDebugMode = isDebugMode
126         ) { cameraPropertiesJSON = it }
127     }
128 
129     /**
130      * updates the capture mode based on the preview mode
131      */
132     private fun CameraAppSettings.applyPreviewMode(previewMode: PreviewMode): CameraAppSettings {
133         val captureMode = previewMode.toCaptureMode()
134         return if (captureMode == this.captureMode) {
135             this
136         } else {
137             this.copy(captureMode = captureMode)
138         }
139     }
140 
141     init {
142         viewModelScope.launch {
143             launch {
144                 var oldCameraAppSettings: CameraAppSettings? = null
145                 settingsRepository.defaultCameraAppSettings.transform { new ->
146                     val old = oldCameraAppSettings
147                     if (old != null) {
148                         emit(getSettingsDiff(old, new))
149                     }
150                     oldCameraAppSettings = new
151                 }.collect { diffQueue ->
152                     applySettingsDiff(diffQueue)
153                 }
154             }
155             combine(
156                 cameraUseCase.getCurrentSettings().filterNotNull(),
157                 constraintsRepository.systemConstraints.filterNotNull(),
158                 cameraUseCase.getCurrentCameraState(),
159                 lockedRecordingState.filterNotNull().distinctUntilChanged()
160             ) { cameraAppSettings, systemConstraints, cameraState, lockedState ->
161 
162                 var flashModeUiState: FlashModeUiState
163                 _previewUiState.update { old ->
164                     when (old) {
165                         is PreviewUiState.NotReady -> {
166                             // Generate initial FlashModeUiState
167                             val supportedFlashModes =
168                                 systemConstraints.forCurrentLens(cameraAppSettings)
169                                     ?.supportedFlashModes
170                                     ?: setOf(FlashMode.OFF)
171                             flashModeUiState = FlashModeUiState.createFrom(
172                                 selectedFlashMode = cameraAppSettings.flashMode,
173                                 supportedFlashModes = supportedFlashModes
174                             )
175                             // This is the first PreviewUiState.Ready. Create the initial
176                             // PreviewUiState.Ready from defaults and initialize it below.
177                             PreviewUiState.Ready()
178                         }
179 
180                         is PreviewUiState.Ready -> {
181                             val previousCameraSettings = old.currentCameraSettings
182                             val previousConstraints = old.systemConstraints
183 
184                             flashModeUiState = old.flashModeUiState.updateFrom(
185                                 currentCameraSettings = cameraAppSettings,
186                                 previousCameraSettings = previousCameraSettings,
187                                 currentConstraints = systemConstraints,
188                                 previousConstraints = previousConstraints,
189                                 cameraState = cameraState
190                             )
191 
192                             // We have a previous `PreviewUiState.Ready`, return it here and
193                             // update it below.
194                             old
195                         }
196                     }.copy(
197                         // Update or initialize PreviewUiState.Ready
198                         previewMode = previewMode,
199                         currentCameraSettings = cameraAppSettings.applyPreviewMode(previewMode),
200                         systemConstraints = systemConstraints,
201                         zoomScale = cameraState.zoomScale,
202                         videoRecordingState = cameraState.videoRecordingState,
203                         sessionFirstFrameTimestamp = cameraState.sessionFirstFrameTimestamp,
204                         captureModeToggleUiState = getCaptureToggleUiState(
205                             systemConstraints,
206                             cameraAppSettings
207                         ),
208                         currentLogicalCameraId = cameraState.debugInfo.logicalCameraId,
209                         currentPhysicalCameraId = cameraState.debugInfo.physicalCameraId,
210                         debugUiState = DebugUiState(
211                             cameraPropertiesJSON = cameraPropertiesJSON,
212                             videoResolution = Size(
213                                 cameraState.videoQualityInfo.width,
214                                 cameraState.videoQualityInfo.height
215                             ),
216                             isDebugMode = isDebugMode
217                         ),
218                         stabilizationUiState = stabilizationUiStateFrom(
219                             cameraAppSettings,
220                             cameraState
221                         ),
222                         flashModeUiState = flashModeUiState,
223                         videoQuality = cameraState.videoQualityInfo.quality,
224                         audioUiState = getAudioUiState(
225                             cameraAppSettings.audioEnabled,
226                             cameraState.videoRecordingState
227                         ),
228                         elapsedTimeUiState = getElapsedTimeUiState(cameraState.videoRecordingState),
229                         captureButtonUiState = getCaptureButtonUiState(
230                             cameraAppSettings,
231                             cameraState,
232                             lockedState
233                         )
234                     )
235                 }
236             }.collect {}
237         }
238     }
239 
240     fun updateLastCapturedImageUri(uri: Uri) {
241         viewModelScope.launch {
242             _previewUiState.update { old ->
243                 (old as PreviewUiState.Ready)
244                     .copy(imageWellUiState = ImageWellUiState.LastCapture(uri))
245             }
246         }
247     }
248 
249     private fun getElapsedTimeUiState(
250         videoRecordingState: VideoRecordingState
251     ): ElapsedTimeUiState = when (videoRecordingState) {
252         is VideoRecordingState.Active ->
253             ElapsedTimeUiState.Enabled(videoRecordingState.elapsedTimeNanos)
254 
255         is VideoRecordingState.Inactive ->
256             ElapsedTimeUiState.Enabled(videoRecordingState.finalElapsedTimeNanos)
257 
258         VideoRecordingState.Starting -> ElapsedTimeUiState.Enabled(0L)
259     }
260 
261     /**
262      * Updates the FlashModeUiState based on the changes in flash mode or constraints
263      */
264     private fun FlashModeUiState.updateFrom(
265         currentCameraSettings: CameraAppSettings,
266         previousCameraSettings: CameraAppSettings,
267         currentConstraints: SystemConstraints,
268         previousConstraints: SystemConstraints,
269         cameraState: CameraState
270     ): FlashModeUiState {
271         val currentFlashMode = currentCameraSettings.flashMode
272         val currentSupportedFlashModes =
273             currentConstraints.forCurrentLens(currentCameraSettings)?.supportedFlashModes
274         return when (this) {
275             is FlashModeUiState.Unavailable -> {
276                 // When previous state was "Unavailable", we'll try to create a new FlashModeUiState
277                 FlashModeUiState.createFrom(
278                     selectedFlashMode = currentFlashMode,
279                     supportedFlashModes = currentSupportedFlashModes ?: setOf(FlashMode.OFF)
280                 )
281             }
282 
283             is FlashModeUiState.Available -> {
284                 val previousFlashMode = previousCameraSettings.flashMode
285                 val previousSupportedFlashModes =
286                     previousConstraints.forCurrentLens(previousCameraSettings)?.supportedFlashModes
287                 if (previousSupportedFlashModes != currentSupportedFlashModes) {
288                     // Supported flash modes have changed, generate a new FlashModeUiState
289                     FlashModeUiState.createFrom(
290                         selectedFlashMode = currentFlashMode,
291                         supportedFlashModes = currentSupportedFlashModes ?: setOf(FlashMode.OFF)
292                     )
293                 } else if (previousFlashMode != currentFlashMode) {
294                     // Only the selected flash mode has changed, just update the flash mode
295                     copy(selectedFlashMode = currentFlashMode)
296                 } else {
297                     if (currentFlashMode == FlashMode.LOW_LIGHT_BOOST) {
298                         copy(
299                             isActive = cameraState.lowLightBoostState == LowLightBoostState.ACTIVE
300                         )
301                     } else {
302                         // Nothing has changed
303                         this
304                     }
305                 }
306             }
307         }
308     }
309 
310     private fun getAudioUiState(
311         isAudioEnabled: Boolean,
312         videoRecordingState: VideoRecordingState
313     ): AudioUiState = if (isAudioEnabled) {
314         if (videoRecordingState is VideoRecordingState.Active) {
315             AudioUiState.Enabled.On(videoRecordingState.audioAmplitude)
316         } else {
317             AudioUiState.Enabled.On(0.0)
318         }
319     } else {
320         AudioUiState.Enabled.Mute
321     }
322 
323     private fun stabilizationUiStateFrom(
324         cameraAppSettings: CameraAppSettings,
325         cameraState: CameraState
326     ): StabilizationUiState {
327         val expectedMode = cameraAppSettings.stabilizationMode
328         val actualMode = cameraState.stabilizationMode
329         check(actualMode != StabilizationMode.AUTO) {
330             "CameraState should never resolve to AUTO stabilization mode"
331         }
332         return when (expectedMode) {
333             StabilizationMode.OFF -> StabilizationUiState.Disabled
334             StabilizationMode.AUTO -> {
335                 if (actualMode !in setOf(StabilizationMode.ON, StabilizationMode.OPTICAL)) {
336                     StabilizationUiState.Disabled
337                 } else {
338                     StabilizationUiState.Auto(actualMode)
339                 }
340             }
341 
342             StabilizationMode.ON,
343             StabilizationMode.HIGH_QUALITY,
344             StabilizationMode.OPTICAL ->
345                 StabilizationUiState.Specific(
346                     stabilizationMode = expectedMode,
347                     active = expectedMode == actualMode
348                 )
349         }
350     }
351 
352     private fun PreviewMode.toCaptureMode() = when (this) {
353         is PreviewMode.ExternalImageCaptureMode -> CaptureMode.IMAGE_ONLY
354         is PreviewMode.ExternalMultipleImageCaptureMode -> CaptureMode.IMAGE_ONLY
355         is PreviewMode.ExternalVideoCaptureMode -> CaptureMode.VIDEO_ONLY
356         is PreviewMode.StandardMode -> CaptureMode.STANDARD
357     }
358 
359     /**
360      * Returns the difference between two [CameraAppSettings] as a mapping of <[KProperty], [Any]>.
361      */
362     private fun getSettingsDiff(
363         oldCameraAppSettings: CameraAppSettings,
364         newCameraAppSettings: CameraAppSettings
365     ): Map<KProperty<Any?>, Any?> = buildMap<KProperty<Any?>, Any?> {
366         CameraAppSettings::class.memberProperties.forEach { property ->
367             if (property.get(oldCameraAppSettings) != property.get(newCameraAppSettings)) {
368                 put(property, property.get(newCameraAppSettings))
369             }
370         }
371     }
372 
373     /**
374      * Iterates through a queue of [Pair]<[KProperty], [Any]> and attempt to apply them to
375      * [CameraUseCase].
376      */
377     private suspend fun applySettingsDiff(diffSettingsMap: Map<KProperty<Any?>, Any?>) {
378         diffSettingsMap.entries.forEach { entry ->
379             when (entry.key) {
380                 CameraAppSettings::cameraLensFacing -> {
381                     cameraUseCase.setLensFacing(entry.value as LensFacing)
382                 }
383 
384                 CameraAppSettings::flashMode -> {
385                     cameraUseCase.setFlashMode(entry.value as FlashMode)
386                 }
387 
388                 CameraAppSettings::streamConfig -> {
389                     cameraUseCase.setStreamConfig(entry.value as StreamConfig)
390                 }
391 
392                 CameraAppSettings::aspectRatio -> {
393                     cameraUseCase.setAspectRatio(entry.value as AspectRatio)
394                 }
395 
396                 CameraAppSettings::stabilizationMode -> {
397                     cameraUseCase.setStabilizationMode(entry.value as StabilizationMode)
398                 }
399 
400                 CameraAppSettings::targetFrameRate -> {
401                     cameraUseCase.setTargetFrameRate(entry.value as Int)
402                 }
403 
404                 CameraAppSettings::maxVideoDurationMillis -> {
405                     cameraUseCase.setMaxVideoDuration(entry.value as Long)
406                 }
407 
408                 CameraAppSettings::videoQuality -> {
409                     cameraUseCase.setVideoQuality(entry.value as VideoQuality)
410                 }
411 
412                 CameraAppSettings::audioEnabled -> {
413                     cameraUseCase.setAudioEnabled(entry.value as Boolean)
414                 }
415 
416                 CameraAppSettings::darkMode -> {}
417 
418                 else -> TODO("Unhandled CameraAppSetting $entry")
419             }
420         }
421     }
422     fun getCaptureButtonUiState(
423         cameraAppSettings: CameraAppSettings,
424         cameraState: CameraState,
425         lockedState: Boolean
426     ): CaptureButtonUiState = when (cameraState.videoRecordingState) {
427         // if not currently recording, check capturemode to determine idle capture button UI
428         is VideoRecordingState.Inactive ->
429             CaptureButtonUiState
430                 .Enabled.Idle(captureMode = cameraAppSettings.captureMode)
431 
432         // display different capture button UI depending on if recording is pressed or locked
433         is VideoRecordingState.Active.Recording, is VideoRecordingState.Active.Paused ->
434             if (lockedState) {
435                 CaptureButtonUiState.Enabled.Recording.LockedRecording
436             } else {
437                 CaptureButtonUiState.Enabled.Recording.PressedRecording
438             }
439 
440         VideoRecordingState.Starting ->
441             CaptureButtonUiState
442                 .Enabled.Idle(captureMode = cameraAppSettings.captureMode)
443     }
444 
445     private fun getCaptureToggleUiState(
446         systemConstraints: SystemConstraints,
447         cameraAppSettings: CameraAppSettings
448     ): CaptureModeToggleUiState {
449         val cameraConstraints: CameraConstraints? = systemConstraints.forCurrentLens(
450             cameraAppSettings
451         )
452         val hdrDynamicRangeSupported = cameraConstraints?.let {
453             it.supportedDynamicRanges.size > 1
454         } ?: false
455         val hdrImageFormatSupported =
456             cameraConstraints?.supportedImageFormatsMap?.get(cameraAppSettings.streamConfig)?.let {
457                 it.size > 1
458             } ?: false
459         val isShown = previewMode is PreviewMode.ExternalImageCaptureMode ||
460             previewMode is PreviewMode.ExternalVideoCaptureMode ||
461             cameraAppSettings.imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR ||
462             cameraAppSettings.dynamicRange == DynamicRange.HLG10 ||
463             cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.DUAL
464         val enabled = previewMode !is PreviewMode.ExternalImageCaptureMode &&
465             previewMode !is PreviewMode.ExternalVideoCaptureMode &&
466             hdrDynamicRangeSupported &&
467             hdrImageFormatSupported &&
468             cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.OFF
469         return if (isShown) {
470             val currentMode = if (
471                 cameraAppSettings.concurrentCameraMode == ConcurrentCameraMode.OFF &&
472                 previewMode is PreviewMode.ExternalImageCaptureMode ||
473                 cameraAppSettings.imageFormat == ImageOutputFormat.JPEG_ULTRA_HDR
474             ) {
475                 CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_IMAGE
476             } else {
477                 CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_VIDEO
478             }
479             if (enabled) {
480                 CaptureModeToggleUiState.Enabled(currentMode)
481             } else {
482                 CaptureModeToggleUiState.Disabled(
483                     currentMode,
484                     getCaptureToggleUiStateDisabledReason(
485                         currentMode,
486                         hdrDynamicRangeSupported,
487                         hdrImageFormatSupported,
488                         systemConstraints,
489                         cameraAppSettings.cameraLensFacing,
490                         cameraAppSettings.streamConfig,
491                         cameraAppSettings.concurrentCameraMode
492                     )
493                 )
494             }
495         } else {
496             CaptureModeToggleUiState.Invisible
497         }
498     }
499 
500     private fun getCaptureToggleUiStateDisabledReason(
501         captureModeToggleUiState: CaptureModeToggleUiState.ToggleMode,
502         hdrDynamicRangeSupported: Boolean,
503         hdrImageFormatSupported: Boolean,
504         systemConstraints: SystemConstraints,
505         currentLensFacing: LensFacing,
506         currentStreamConfig: StreamConfig,
507         concurrentCameraMode: ConcurrentCameraMode
508     ): CaptureModeToggleUiState.DisabledReason {
509         when (captureModeToggleUiState) {
510             CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_VIDEO -> {
511                 if (previewMode is PreviewMode.ExternalVideoCaptureMode) {
512                     return CaptureModeToggleUiState.DisabledReason
513                         .IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED
514                 }
515 
516                 if (concurrentCameraMode == ConcurrentCameraMode.DUAL) {
517                     return CaptureModeToggleUiState.DisabledReason
518                         .IMAGE_CAPTURE_UNSUPPORTED_CONCURRENT_CAMERA
519                 }
520 
521                 if (!hdrImageFormatSupported) {
522                     // First check if Ultra HDR image is supported on other capture modes
523                     if (systemConstraints
524                             .perLensConstraints[currentLensFacing]
525                             ?.supportedImageFormatsMap
526                             ?.anySupportsUltraHdr { it != currentStreamConfig } == true
527                     ) {
528                         return when (currentStreamConfig) {
529                             StreamConfig.MULTI_STREAM ->
530                                 CaptureModeToggleUiState.DisabledReason
531                                     .HDR_IMAGE_UNSUPPORTED_ON_MULTI_STREAM
532 
533                             StreamConfig.SINGLE_STREAM ->
534                                 CaptureModeToggleUiState.DisabledReason
535                                     .HDR_IMAGE_UNSUPPORTED_ON_SINGLE_STREAM
536                         }
537                     }
538 
539                     // Check if any other lens supports HDR image
540                     if (systemConstraints.anySupportsUltraHdr { it != currentLensFacing }) {
541                         return CaptureModeToggleUiState.DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_LENS
542                     }
543 
544                     // No lenses support HDR image on device
545                     return CaptureModeToggleUiState.DisabledReason.HDR_IMAGE_UNSUPPORTED_ON_DEVICE
546                 }
547 
548                 throw RuntimeException("Unknown DisabledReason for video mode.")
549             }
550 
551             CaptureModeToggleUiState.ToggleMode.CAPTURE_TOGGLE_IMAGE -> {
552                 if (previewMode is PreviewMode.ExternalImageCaptureMode) {
553                     return CaptureModeToggleUiState.DisabledReason
554                         .VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED
555                 }
556 
557                 if (!hdrDynamicRangeSupported) {
558                     if (systemConstraints.anySupportsHdrDynamicRange { it != currentLensFacing }) {
559                         return CaptureModeToggleUiState.DisabledReason.HDR_VIDEO_UNSUPPORTED_ON_LENS
560                     }
561                     return CaptureModeToggleUiState.DisabledReason.HDR_VIDEO_UNSUPPORTED_ON_DEVICE
562                 }
563 
564                 throw RuntimeException("Unknown DisabledReason for image mode.")
565             }
566         }
567     }
568 
569     private fun SystemConstraints.anySupportsHdrDynamicRange(
570         lensFilter: (LensFacing) -> Boolean
571     ): Boolean = perLensConstraints.asSequence().firstOrNull {
572         lensFilter(it.key) && it.value.supportedDynamicRanges.size > 1
573     } != null
574 
575     private fun Map<StreamConfig, Set<ImageOutputFormat>>.anySupportsUltraHdr(
576         captureModeFilter: (StreamConfig) -> Boolean
577     ): Boolean = asSequence().firstOrNull {
578         captureModeFilter(it.key) && it.value.contains(ImageOutputFormat.JPEG_ULTRA_HDR)
579     } != null
580 
581     private fun SystemConstraints.anySupportsUltraHdr(
582         captureModeFilter: (StreamConfig) -> Boolean = { true },
583         lensFilter: (LensFacing) -> Boolean
584     ): Boolean = perLensConstraints.asSequence().firstOrNull { lensConstraints ->
585         lensFilter(lensConstraints.key) &&
586             lensConstraints.value.supportedImageFormatsMap.anySupportsUltraHdr {
587                 captureModeFilter(it)
588             }
589     } != null
590 
591     fun startCamera() {
592         Log.d(TAG, "startCamera")
593         stopCamera()
594         runningCameraJob = viewModelScope.launch {
595             if (Trace.isEnabled()) {
596                 launch(start = CoroutineStart.UNDISPATCHED) {
597                     val startTraceTimestamp: Long = SystemClock.elapsedRealtimeNanos()
598                     traceFirstFramePreview(cookie = 1) {
599                         _previewUiState.transformWhile {
600                             var continueCollecting = true
601                             (it as? PreviewUiState.Ready)?.let { uiState ->
602                                 if (uiState.sessionFirstFrameTimestamp > startTraceTimestamp) {
603                                     emit(Unit)
604                                     continueCollecting = false
605                                 }
606                             }
607                             continueCollecting
608                         }.collect {}
609                     }
610                 }
611             }
612             // Ensure CameraUseCase is initialized before starting camera
613             initializationDeferred.await()
614             // TODO(yasith): Handle Exceptions from binding use cases
615             cameraUseCase.runCamera()
616         }
617     }
618 
619     fun stopCamera() {
620         Log.d(TAG, "stopCamera")
621         runningCameraJob?.apply {
622             if (isActive) {
623                 cancel()
624             }
625         }
626     }
627 
628     fun setFlash(flashMode: FlashMode) {
629         viewModelScope.launch {
630             // apply to cameraUseCase
631             cameraUseCase.setFlashMode(flashMode)
632         }
633     }
634 
635     fun setAspectRatio(aspectRatio: AspectRatio) {
636         viewModelScope.launch {
637             cameraUseCase.setAspectRatio(aspectRatio)
638         }
639     }
640 
641     fun setStreamConfig(streamConfig: StreamConfig) {
642         viewModelScope.launch {
643             cameraUseCase.setStreamConfig(streamConfig)
644         }
645     }
646 
647     /** Sets the camera to a designated lens facing */
648     fun setLensFacing(newLensFacing: LensFacing) {
649         viewModelScope.launch {
650             // apply to cameraUseCase
651             cameraUseCase.setLensFacing(newLensFacing)
652         }
653     }
654 
655     fun setAudioEnabled(shouldEnableAudio: Boolean) {
656         viewModelScope.launch {
657             cameraUseCase.setAudioEnabled(shouldEnableAudio)
658         }
659 
660         Log.d(
661             TAG,
662             "Toggle Audio: $shouldEnableAudio"
663         )
664     }
665 
666     fun setPaused(shouldBePaused: Boolean) {
667         viewModelScope.launch {
668             if (shouldBePaused) {
669                 cameraUseCase.pauseVideoRecording()
670             } else {
671                 cameraUseCase.resumeVideoRecording()
672             }
673         }
674     }
675 
676     private fun showExternalVideoCaptureUnsupportedToast() {
677         viewModelScope.launch {
678             _previewUiState.update { old ->
679                 (old as? PreviewUiState.Ready)?.copy(
680                     snackBarToShow = SnackbarData(
681                         cookie = "Image-ExternalVideoCaptureMode",
682                         stringResource = R.string.toast_image_capture_external_unsupported,
683                         withDismissAction = true,
684                         testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
685                     )
686                 ) ?: old
687             }
688         }
689     }
690 
691     fun captureImageWithUri(
692         contentResolver: ContentResolver,
693         imageCaptureUri: Uri?,
694         ignoreUri: Boolean = false,
695         onImageCapture: (ImageCaptureEvent, Int) -> Unit
696     ) {
697         if (previewUiState.value is PreviewUiState.Ready &&
698             (previewUiState.value as PreviewUiState.Ready).previewMode is
699                 PreviewMode.ExternalVideoCaptureMode
700         ) {
701             showExternalVideoCaptureUnsupportedToast()
702             return
703         }
704 
705         if (previewUiState.value is PreviewUiState.Ready &&
706             (previewUiState.value as PreviewUiState.Ready).previewMode is
707                 PreviewMode.ExternalVideoCaptureMode
708         ) {
709             viewModelScope.launch {
710                 _previewUiState.update { old ->
711                     (old as? PreviewUiState.Ready)?.copy(
712                         snackBarToShow = SnackbarData(
713                             cookie = "Image-ExternalVideoCaptureMode",
714                             stringResource = R.string.toast_image_capture_external_unsupported,
715                             withDismissAction = true,
716                             testTag = IMAGE_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
717                         )
718                     ) ?: old
719                 }
720             }
721             return
722         }
723         Log.d(TAG, "captureImageWithUri")
724         viewModelScope.launch {
725             val (uriIndex: Int, finalImageUri: Uri?) =
726                 (
727                     (previewUiState.value as? PreviewUiState.Ready)?.previewMode as?
728                         PreviewMode.ExternalMultipleImageCaptureMode
729                     )?.let {
730                     val uri = if (ignoreUri || it.imageCaptureUris.isNullOrEmpty()) {
731                         null
732                     } else {
733                         it.imageCaptureUris[externalUriIndex]
734                     }
735                     Pair(externalUriIndex, uri)
736                 } ?: Pair(-1, imageCaptureUri)
737             captureImageInternal(
738                 doTakePicture = {
739                     cameraUseCase.takePicture({
740                         _previewUiState.update { old ->
741                             (old as? PreviewUiState.Ready)?.copy(
742                                 lastBlinkTimeStamp = System.currentTimeMillis()
743                             ) ?: old
744                         }
745                     }, contentResolver, finalImageUri, ignoreUri).savedUri
746                 },
747                 onSuccess = { savedUri ->
748                     savedUri?.let {
749                         updateLastCapturedImageUri(it)
750                     }
751                     onImageCapture(ImageCaptureEvent.ImageSaved(savedUri), uriIndex)
752                 },
753                 onFailure = { exception ->
754                     onImageCapture(ImageCaptureEvent.ImageCaptureError(exception), uriIndex)
755                 }
756             )
757             incrementExternalMultipleImageCaptureModeUriIndexIfNeeded()
758         }
759     }
760 
761     private fun incrementExternalMultipleImageCaptureModeUriIndexIfNeeded() {
762         (
763             (previewUiState.value as? PreviewUiState.Ready)
764                 ?.previewMode as? PreviewMode.ExternalMultipleImageCaptureMode
765             )?.let {
766             if (!it.imageCaptureUris.isNullOrEmpty()) {
767                 externalUriIndex++
768                 Log.d(TAG, "Uri index for multiple image capture at $externalUriIndex")
769             }
770         }
771     }
772 
773     private suspend fun <T> captureImageInternal(
774         doTakePicture: suspend () -> T,
775         onSuccess: (T) -> Unit = {},
776         onFailure: (exception: Exception) -> Unit = {}
777     ) {
778         val cookieInt = snackBarCount.incrementAndGet()
779         val cookie = "Image-$cookieInt"
780         try {
781             traceAsync(IMAGE_CAPTURE_TRACE, cookieInt) {
782                 doTakePicture()
783             }.also { result ->
784                 onSuccess(result)
785             }
786             Log.d(TAG, "cameraUseCase.takePicture success")
787             SnackbarData(
788                 cookie = cookie,
789                 stringResource = R.string.toast_image_capture_success,
790                 withDismissAction = true,
791                 testTag = IMAGE_CAPTURE_SUCCESS_TAG
792             )
793         } catch (exception: Exception) {
794             onFailure(exception)
795             Log.d(TAG, "cameraUseCase.takePicture error", exception)
796             SnackbarData(
797                 cookie = cookie,
798                 stringResource = R.string.toast_capture_failure,
799                 withDismissAction = true,
800                 testTag = IMAGE_CAPTURE_FAILURE_TAG
801             )
802         }.also { snackBarData ->
803             _previewUiState.update { old ->
804                 (old as? PreviewUiState.Ready)?.copy(
805                     // todo: remove snackBar after postcapture screen implemented
806                     snackBarToShow = snackBarData
807                 ) ?: old
808             }
809         }
810     }
811 
812     fun showSnackBarForDisabledHdrToggle(disabledReason: CaptureModeToggleUiState.DisabledReason) {
813         val cookieInt = snackBarCount.incrementAndGet()
814         val cookie = "DisabledHdrToggle-$cookieInt"
815         viewModelScope.launch {
816             _previewUiState.update { old ->
817                 (old as? PreviewUiState.Ready)?.copy(
818                     snackBarToShow = SnackbarData(
819                         cookie = cookie,
820                         stringResource = disabledReason.reasonTextResId,
821                         withDismissAction = true,
822                         testTag = disabledReason.testTag
823                     )
824                 ) ?: old
825             }
826         }
827     }
828 
829     fun startVideoRecording(
830         videoCaptureUri: Uri?,
831         shouldUseUri: Boolean,
832         onVideoCapture: (VideoCaptureEvent) -> Unit
833     ) {
834         if (previewUiState.value is PreviewUiState.Ready &&
835             (previewUiState.value as PreviewUiState.Ready).previewMode is
836                 PreviewMode.ExternalImageCaptureMode
837         ) {
838             Log.d(TAG, "externalVideoRecording")
839             viewModelScope.launch {
840                 _previewUiState.update { old ->
841                     (old as? PreviewUiState.Ready)?.copy(
842                         snackBarToShow = SnackbarData(
843                             cookie = "Video-ExternalImageCaptureMode",
844                             stringResource = R.string.toast_video_capture_external_unsupported,
845                             withDismissAction = true,
846                             testTag = VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG
847                         )
848                     ) ?: old
849                 }
850             }
851             return
852         }
853         Log.d(TAG, "startVideoRecording")
854         recordingJob = viewModelScope.launch {
855             val cookie = "Video-${videoCaptureStartedCount.incrementAndGet()}"
856             try {
857                 cameraUseCase.startVideoRecording(videoCaptureUri, shouldUseUri) {
858                     var snackbarToShow: SnackbarData? = null
859                     when (it) {
860                         is CameraUseCase.OnVideoRecordEvent.OnVideoRecorded -> {
861                             Log.d(TAG, "cameraUseCase.startRecording OnVideoRecorded")
862                             onVideoCapture(VideoCaptureEvent.VideoSaved(it.savedUri))
863                             snackbarToShow = SnackbarData(
864                                 cookie = cookie,
865                                 stringResource = R.string.toast_video_capture_success,
866                                 withDismissAction = true,
867                                 testTag = VIDEO_CAPTURE_SUCCESS_TAG
868                             )
869                         }
870 
871                         is CameraUseCase.OnVideoRecordEvent.OnVideoRecordError -> {
872                             Log.d(TAG, "cameraUseCase.startRecording OnVideoRecordError")
873                             onVideoCapture(VideoCaptureEvent.VideoCaptureError(it.error))
874                             snackbarToShow = SnackbarData(
875                                 cookie = cookie,
876                                 stringResource = R.string.toast_video_capture_failure,
877                                 withDismissAction = true,
878                                 testTag = VIDEO_CAPTURE_FAILURE_TAG
879                             )
880                         }
881                     }
882 
883                     viewModelScope.launch {
884                         _previewUiState.update { old ->
885                             (old as? PreviewUiState.Ready)?.copy(
886                                 snackBarToShow = snackbarToShow
887                             ) ?: old
888                         }
889                     }
890                 }
891                 Log.d(TAG, "cameraUseCase.startRecording success")
892             } catch (exception: IllegalStateException) {
893                 Log.d(TAG, "cameraUseCase.startVideoRecording error", exception)
894             }
895         }
896     }
897 
898     fun stopVideoRecording() {
899         Log.d(TAG, "stopVideoRecording")
900         viewModelScope.launch {
901             cameraUseCase.stopVideoRecording()
902             recordingJob?.cancel()
903         }
904         setLockedRecording(false)
905     }
906 
907     /**
908      "Locks" the video recording such that the user no longer needs to keep their finger pressed on the capture button
909      */
910     fun setLockedRecording(isLocked: Boolean) {
911         viewModelScope.launch {
912             lockedRecordingState.update {
913                 isLocked
914             }
915         }
916     }
917 
918     fun setZoomScale(scale: Float) {
919         cameraUseCase.setZoomScale(scale = scale)
920     }
921 
922     fun setDynamicRange(dynamicRange: DynamicRange) {
923         viewModelScope.launch {
924             cameraUseCase.setDynamicRange(dynamicRange)
925         }
926     }
927 
928     fun setConcurrentCameraMode(concurrentCameraMode: ConcurrentCameraMode) {
929         viewModelScope.launch {
930             cameraUseCase.setConcurrentCameraMode(concurrentCameraMode)
931         }
932     }
933 
934     fun setImageFormat(imageFormat: ImageOutputFormat) {
935         viewModelScope.launch {
936             cameraUseCase.setImageFormat(imageFormat)
937         }
938     }
939 
940     // modify ui values
941     fun toggleQuickSettings() {
942         viewModelScope.launch {
943             _previewUiState.update { old ->
944                 (old as? PreviewUiState.Ready)?.copy(
945                     quickSettingsIsOpen = !old.quickSettingsIsOpen
946                 ) ?: old
947             }
948         }
949     }
950 
951     fun toggleDebugOverlay() {
952         viewModelScope.launch {
953             _previewUiState.update { old ->
954                 (old as? PreviewUiState.Ready)?.copy(
955                     debugUiState = DebugUiState(
956                         old.debugUiState.cameraPropertiesJSON,
957                         old.debugUiState.videoResolution,
958                         old.debugUiState.isDebugMode,
959                         !old.debugUiState.isDebugOverlayOpen
960                     )
961                 ) ?: old
962             }
963         }
964     }
965 
966     fun tapToFocus(x: Float, y: Float) {
967         Log.d(TAG, "tapToFocus")
968         viewModelScope.launch {
969             cameraUseCase.tapToFocus(x, y)
970         }
971     }
972 
973     /**
974      * Sets current value of [PreviewUiState.Ready.toastMessageToShow] to null.
975      */
976     fun onToastShown() {
977         viewModelScope.launch {
978             // keeps the composable up on screen longer to be detected by UiAutomator
979             delay(2.seconds)
980             _previewUiState.update { old ->
981                 (old as? PreviewUiState.Ready)?.copy(
982                     toastMessageToShow = null
983                 ) ?: old
984             }
985         }
986     }
987 
988     fun onSnackBarResult(cookie: String) {
989         viewModelScope.launch {
990             _previewUiState.update { old ->
991                 (old as? PreviewUiState.Ready)?.snackBarToShow?.let {
992                     if (it.cookie == cookie) {
993                         // If the latest snackbar had a result, then clear snackBarToShow
994                         old.copy(snackBarToShow = null)
995                     } else {
996                         old
997                     }
998                 } ?: old
999             }
1000         }
1001     }
1002 
1003     fun setDisplayRotation(deviceRotation: DeviceRotation) {
1004         viewModelScope.launch {
1005             cameraUseCase.setDeviceRotation(deviceRotation)
1006         }
1007     }
1008 
1009     @AssistedFactory
1010     interface Factory {
1011         fun create(previewMode: PreviewMode, isDebugMode: Boolean): PreviewViewModel
1012     }
1013 
1014     sealed interface ImageCaptureEvent {
1015         data class ImageSaved(val savedUri: Uri? = null) : ImageCaptureEvent
1016 
1017         data class ImageCaptureError(val exception: Exception) : ImageCaptureEvent
1018     }
1019 
1020     sealed interface VideoCaptureEvent {
1021         data class VideoSaved(val savedUri: Uri) : VideoCaptureEvent
1022 
1023         data class VideoCaptureError(val error: Throwable?) : VideoCaptureEvent
1024     }
1025 }
1026