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