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.util.Log 21 import android.view.Display 22 import androidx.camera.core.SurfaceRequest 23 import androidx.lifecycle.ViewModel 24 import androidx.lifecycle.viewModelScope 25 import androidx.tracing.traceAsync 26 import com.google.jetpackcamera.domain.camera.CameraUseCase 27 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_FAILURE_TAG 28 import com.google.jetpackcamera.feature.preview.ui.IMAGE_CAPTURE_SUCCESS_TAG 29 import com.google.jetpackcamera.feature.preview.ui.SnackbarData 30 import com.google.jetpackcamera.feature.preview.ui.VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG 31 import com.google.jetpackcamera.settings.ConstraintsRepository 32 import com.google.jetpackcamera.settings.model.AspectRatio 33 import com.google.jetpackcamera.settings.model.CaptureMode 34 import com.google.jetpackcamera.settings.model.DynamicRange 35 import com.google.jetpackcamera.settings.model.FlashMode 36 import com.google.jetpackcamera.settings.model.LensFacing 37 import dagger.assisted.Assisted 38 import dagger.assisted.AssistedFactory 39 import dagger.assisted.AssistedInject 40 import dagger.hilt.android.lifecycle.HiltViewModel 41 import kotlin.time.Duration.Companion.seconds 42 import kotlinx.atomicfu.atomic 43 import kotlinx.coroutines.Deferred 44 import kotlinx.coroutines.Job 45 import kotlinx.coroutines.async 46 import kotlinx.coroutines.delay 47 import kotlinx.coroutines.flow.MutableStateFlow 48 import kotlinx.coroutines.flow.StateFlow 49 import kotlinx.coroutines.flow.asStateFlow 50 import kotlinx.coroutines.flow.combine 51 import kotlinx.coroutines.flow.filterNotNull 52 import kotlinx.coroutines.flow.update 53 import kotlinx.coroutines.launch 54 55 private const val TAG = "PreviewViewModel" 56 private const val IMAGE_CAPTURE_TRACE = "JCA Image Capture" 57 58 /** 59 * [ViewModel] for [PreviewScreen]. 60 */ 61 @HiltViewModel(assistedFactory = PreviewViewModel.Factory::class) 62 class PreviewViewModel @AssistedInject constructor( 63 @Assisted previewMode: PreviewMode, 64 private val cameraUseCase: CameraUseCase, 65 private val constraintsRepository: ConstraintsRepository 66 67 ) : ViewModel() { 68 private val _previewUiState: MutableStateFlow<PreviewUiState> = 69 MutableStateFlow(PreviewUiState.NotReady) 70 71 val previewUiState: StateFlow<PreviewUiState> = 72 _previewUiState.asStateFlow() 73 74 val surfaceRequest: StateFlow<SurfaceRequest?> = cameraUseCase.getSurfaceRequest() 75 76 private var runningCameraJob: Job? = null 77 78 private var recordingJob: Job? = null 79 80 val screenFlash = ScreenFlash(cameraUseCase, viewModelScope) 81 82 private val imageCaptureCalledCount = atomic(0) 83 private val videoCaptureStartedCount = atomic(0) 84 85 // Eagerly initialize the CameraUseCase and encapsulate in a Deferred that can be 86 // used to ensure we don't start the camera before initialization is complete. 87 private var initializationDeferred: Deferred<Unit> = viewModelScope.async { 88 cameraUseCase.initialize(previewMode is PreviewMode.ExternalImageCaptureMode) 89 } 90 91 init { 92 viewModelScope.launch { 93 combine( 94 cameraUseCase.getCurrentSettings().filterNotNull(), 95 constraintsRepository.systemConstraints.filterNotNull(), 96 cameraUseCase.getZoomScale() 97 ) { cameraAppSettings, systemConstraints, zoomScale -> 98 _previewUiState.update { old -> 99 when (old) { 100 is PreviewUiState.Ready -> 101 old.copy( 102 currentCameraSettings = cameraAppSettings, 103 systemConstraints = systemConstraints, 104 zoomScale = zoomScale, 105 previewMode = previewMode 106 ) 107 108 is PreviewUiState.NotReady -> 109 PreviewUiState.Ready( 110 currentCameraSettings = cameraAppSettings, 111 systemConstraints = systemConstraints, 112 zoomScale = zoomScale, 113 previewMode = previewMode 114 ) 115 } 116 } 117 }.collect {} 118 } 119 } 120 121 fun startCamera() { 122 Log.d(TAG, "startCamera") 123 stopCamera() 124 runningCameraJob = viewModelScope.launch { 125 // Ensure CameraUseCase is initialized before starting camera 126 initializationDeferred.await() 127 // TODO(yasith): Handle Exceptions from binding use cases 128 cameraUseCase.runCamera() 129 } 130 } 131 132 fun stopCamera() { 133 Log.d(TAG, "stopCamera") 134 runningCameraJob?.apply { 135 if (isActive) { 136 cancel() 137 } 138 } 139 } 140 141 fun setFlash(flashMode: FlashMode) { 142 viewModelScope.launch { 143 // apply to cameraUseCase 144 cameraUseCase.setFlashMode(flashMode) 145 } 146 } 147 148 fun setAspectRatio(aspectRatio: AspectRatio) { 149 viewModelScope.launch { 150 cameraUseCase.setAspectRatio(aspectRatio) 151 } 152 } 153 154 fun setCaptureMode(captureMode: CaptureMode) { 155 viewModelScope.launch { 156 // apply to cameraUseCase 157 cameraUseCase.setCaptureMode(captureMode) 158 } 159 } 160 161 /** Sets the camera to a designated lens facing */ 162 fun setLensFacing(newLensFacing: LensFacing) { 163 viewModelScope.launch { 164 // apply to cameraUseCase 165 cameraUseCase.setLensFacing(newLensFacing) 166 } 167 } 168 169 fun captureImage() { 170 Log.d(TAG, "captureImage") 171 viewModelScope.launch { 172 captureImageInternal( 173 doTakePicture = { 174 cameraUseCase.takePicture { 175 _previewUiState.update { old -> 176 (old as? PreviewUiState.Ready)?.copy( 177 lastBlinkTimeStamp = System.currentTimeMillis() 178 ) ?: old 179 } 180 } 181 } 182 ) 183 } 184 } 185 186 fun captureImageWithUri( 187 contentResolver: ContentResolver, 188 imageCaptureUri: Uri?, 189 ignoreUri: Boolean = false, 190 onImageCapture: (ImageCaptureEvent) -> Unit 191 ) { 192 Log.d(TAG, "captureImageWithUri") 193 viewModelScope.launch { 194 captureImageInternal( 195 doTakePicture = { 196 cameraUseCase.takePicture({ 197 _previewUiState.update { old -> 198 (old as? PreviewUiState.Ready)?.copy( 199 lastBlinkTimeStamp = System.currentTimeMillis() 200 ) ?: old 201 } 202 }, contentResolver, imageCaptureUri, ignoreUri).savedUri 203 }, 204 onSuccess = { savedUri -> onImageCapture(ImageCaptureEvent.ImageSaved(savedUri)) }, 205 onFailure = { exception -> 206 onImageCapture(ImageCaptureEvent.ImageCaptureError(exception)) 207 } 208 ) 209 } 210 } 211 212 private suspend fun <T> captureImageInternal( 213 doTakePicture: suspend () -> T, 214 onSuccess: (T) -> Unit = {}, 215 onFailure: (exception: Exception) -> Unit = {} 216 ) { 217 val cookieInt = imageCaptureCalledCount.incrementAndGet() 218 val cookie = "Image-$cookieInt" 219 try { 220 traceAsync(IMAGE_CAPTURE_TRACE, cookieInt) { 221 doTakePicture() 222 }.also { result -> 223 onSuccess(result) 224 } 225 Log.d(TAG, "cameraUseCase.takePicture success") 226 SnackbarData( 227 cookie = cookie, 228 stringResource = R.string.toast_image_capture_success, 229 withDismissAction = true, 230 testTag = IMAGE_CAPTURE_SUCCESS_TAG 231 ) 232 } catch (exception: Exception) { 233 onFailure(exception) 234 Log.d(TAG, "cameraUseCase.takePicture error", exception) 235 SnackbarData( 236 cookie = cookie, 237 stringResource = R.string.toast_capture_failure, 238 withDismissAction = true, 239 testTag = IMAGE_CAPTURE_FAILURE_TAG 240 ) 241 }.also { snackBarData -> 242 _previewUiState.update { old -> 243 (old as? PreviewUiState.Ready)?.copy( 244 // todo: remove snackBar after postcapture screen implemented 245 snackBarToShow = snackBarData 246 ) ?: old 247 } 248 } 249 } 250 251 fun startVideoRecording() { 252 if (previewUiState.value is PreviewUiState.Ready && 253 (previewUiState.value as PreviewUiState.Ready).previewMode is 254 PreviewMode.ExternalImageCaptureMode 255 ) { 256 Log.d(TAG, "externalVideoRecording") 257 viewModelScope.launch { 258 _previewUiState.update { old -> 259 (old as? PreviewUiState.Ready)?.copy( 260 snackBarToShow = SnackbarData( 261 cookie = "Video-ExternalImageCaptureMode", 262 stringResource = R.string.toast_video_capture_external_unsupported, 263 withDismissAction = true, 264 testTag = VIDEO_CAPTURE_EXTERNAL_UNSUPPORTED_TAG 265 ) 266 ) ?: old 267 } 268 } 269 return 270 } 271 Log.d(TAG, "startVideoRecording") 272 recordingJob = viewModelScope.launch { 273 val cookie = "Video-${videoCaptureStartedCount.incrementAndGet()}" 274 try { 275 cameraUseCase.startVideoRecording { 276 var audioAmplitude = 0.0 277 var snackbarToShow: SnackbarData? = null 278 when (it) { 279 CameraUseCase.OnVideoRecordEvent.OnVideoRecorded -> { 280 snackbarToShow = SnackbarData( 281 cookie = cookie, 282 stringResource = R.string.toast_video_capture_success, 283 withDismissAction = true 284 ) 285 } 286 287 CameraUseCase.OnVideoRecordEvent.OnVideoRecordError -> { 288 snackbarToShow = SnackbarData( 289 cookie = cookie, 290 stringResource = R.string.toast_video_capture_failure, 291 withDismissAction = true 292 ) 293 } 294 295 is CameraUseCase.OnVideoRecordEvent.OnVideoRecordStatus -> { 296 audioAmplitude = it.audioAmplitude 297 } 298 } 299 300 viewModelScope.launch { 301 _previewUiState.update { old -> 302 (old as? PreviewUiState.Ready)?.copy( 303 snackBarToShow = snackbarToShow, 304 audioAmplitude = audioAmplitude 305 ) ?: old 306 } 307 } 308 } 309 _previewUiState.update { old -> 310 (old as? PreviewUiState.Ready)?.copy( 311 videoRecordingState = VideoRecordingState.ACTIVE 312 ) ?: old 313 } 314 Log.d(TAG, "cameraUseCase.startRecording success") 315 } catch (exception: IllegalStateException) { 316 Log.d(TAG, "cameraUseCase.startVideoRecording error", exception) 317 } 318 } 319 } 320 321 fun stopVideoRecording() { 322 Log.d(TAG, "stopVideoRecording") 323 viewModelScope.launch { 324 _previewUiState.update { old -> 325 (old as? PreviewUiState.Ready)?.copy( 326 videoRecordingState = VideoRecordingState.INACTIVE 327 ) ?: old 328 } 329 } 330 cameraUseCase.stopVideoRecording() 331 recordingJob?.cancel() 332 } 333 334 fun setZoomScale(scale: Float) { 335 cameraUseCase.setZoomScale(scale = scale) 336 } 337 338 fun setDynamicRange(dynamicRange: DynamicRange) { 339 viewModelScope.launch { 340 cameraUseCase.setDynamicRange(dynamicRange) 341 } 342 } 343 344 // modify ui values 345 fun toggleQuickSettings() { 346 viewModelScope.launch { 347 _previewUiState.update { old -> 348 (old as? PreviewUiState.Ready)?.copy( 349 quickSettingsIsOpen = !old.quickSettingsIsOpen 350 ) ?: old 351 } 352 } 353 } 354 355 fun tapToFocus(display: Display, surfaceWidth: Int, surfaceHeight: Int, x: Float, y: Float) { 356 cameraUseCase.tapToFocus( 357 display = display, 358 surfaceWidth = surfaceWidth, 359 surfaceHeight = surfaceHeight, 360 x = x, 361 y = y 362 ) 363 } 364 365 /** 366 * Sets current value of [PreviewUiState.Ready.toastMessageToShow] to null. 367 */ 368 fun onToastShown() { 369 viewModelScope.launch { 370 // keeps the composable up on screen longer to be detected by UiAutomator 371 delay(2.seconds) 372 _previewUiState.update { old -> 373 (old as? PreviewUiState.Ready)?.copy( 374 toastMessageToShow = null 375 ) ?: old 376 } 377 } 378 } 379 380 fun onSnackBarResult(cookie: String) { 381 viewModelScope.launch { 382 _previewUiState.update { old -> 383 (old as? PreviewUiState.Ready)?.snackBarToShow?.let { 384 if (it.cookie == cookie) { 385 // If the latest snackbar had a result, then clear snackBarToShow 386 old.copy(snackBarToShow = null) 387 } else { 388 old 389 } 390 } ?: old 391 } 392 } 393 } 394 395 @AssistedFactory 396 interface Factory { 397 fun create(previewMode: PreviewMode): PreviewViewModel 398 } 399 400 sealed interface ImageCaptureEvent { 401 data class ImageSaved( 402 val savedUri: Uri? = null 403 ) : ImageCaptureEvent 404 405 data class ImageCaptureError( 406 val exception: Exception 407 ) : ImageCaptureEvent 408 } 409 } 410