• 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.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