• 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.annotation.SuppressLint
19 import android.content.ContentResolver
20 import android.net.Uri
21 import android.util.Log
22 import androidx.camera.core.SurfaceRequest
23 import androidx.compose.foundation.background
24 import androidx.compose.foundation.layout.Arrangement
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.Column
27 import androidx.compose.foundation.layout.fillMaxSize
28 import androidx.compose.foundation.layout.size
29 import androidx.compose.material3.CircularProgressIndicator
30 import androidx.compose.material3.MaterialTheme
31 import androidx.compose.material3.Scaffold
32 import androidx.compose.material3.SnackbarHost
33 import androidx.compose.material3.SnackbarHostState
34 import androidx.compose.material3.Text
35 import androidx.compose.material3.darkColorScheme
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.collectAsState
39 import androidx.compose.runtime.getValue
40 import androidx.compose.runtime.remember
41 import androidx.compose.runtime.rememberUpdatedState
42 import androidx.compose.runtime.snapshotFlow
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.graphics.Color
46 import androidx.compose.ui.platform.LocalContext
47 import androidx.compose.ui.platform.testTag
48 import androidx.compose.ui.res.stringResource
49 import androidx.compose.ui.tooling.preview.Preview
50 import androidx.compose.ui.unit.dp
51 import androidx.hilt.navigation.compose.hiltViewModel
52 import androidx.lifecycle.compose.LifecycleStartEffect
53 import androidx.tracing.Trace
54 import com.google.jetpackcamera.core.camera.VideoRecordingState
55 import com.google.jetpackcamera.core.common.getLastImageUri
56 import com.google.jetpackcamera.feature.preview.quicksettings.QuickSettingsScreenOverlay
57 import com.google.jetpackcamera.feature.preview.ui.CameraControlsOverlay
58 import com.google.jetpackcamera.feature.preview.ui.PreviewDisplay
59 import com.google.jetpackcamera.feature.preview.ui.ScreenFlashScreen
60 import com.google.jetpackcamera.feature.preview.ui.TestableSnackbar
61 import com.google.jetpackcamera.feature.preview.ui.TestableToast
62 import com.google.jetpackcamera.feature.preview.ui.ZoomLevelDisplayState
63 import com.google.jetpackcamera.feature.preview.ui.debouncedOrientationFlow
64 import com.google.jetpackcamera.feature.preview.ui.debug.DebugOverlayComponent
65 import com.google.jetpackcamera.settings.model.AspectRatio
66 import com.google.jetpackcamera.settings.model.CaptureMode
67 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
68 import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
69 import com.google.jetpackcamera.settings.model.DynamicRange
70 import com.google.jetpackcamera.settings.model.FlashMode
71 import com.google.jetpackcamera.settings.model.ImageOutputFormat
72 import com.google.jetpackcamera.settings.model.LensFacing
73 import com.google.jetpackcamera.settings.model.StreamConfig
74 import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS
75 import kotlinx.coroutines.flow.transformWhile
76 
77 private const val TAG = "PreviewScreen"
78 
79 /**
80  * Screen used for the Preview feature.
81  */
82 @Composable
83 fun PreviewScreen(
84     onNavigateToSettings: () -> Unit,
85     onNavigateToPostCapture: (uri: Uri?) -> Unit,
86     previewMode: PreviewMode,
87     isDebugMode: Boolean,
88     modifier: Modifier = Modifier,
89     onRequestWindowColorMode: (Int) -> Unit = {},
<lambda>null90     onFirstFrameCaptureCompleted: () -> Unit = {},
91     viewModel: PreviewViewModel = hiltViewModel<PreviewViewModel, PreviewViewModel.Factory>
factorynull92         { factory -> factory.create(previewMode, isDebugMode) }
93 ) {
94     Log.d(TAG, "PreviewScreen")
95 
96     val previewUiState: PreviewUiState by viewModel.previewUiState.collectAsState()
97 
98     val screenFlashUiState: ScreenFlash.ScreenFlashUiState
99         by viewModel.screenFlash.screenFlashUiState.collectAsState()
100 
101     val surfaceRequest: SurfaceRequest?
102         by viewModel.surfaceRequest.collectAsState()
103 
<lambda>null104     LifecycleStartEffect(Unit) {
105         viewModel.startCamera()
106         onStopOrDispose {
107             viewModel.stopCamera()
108         }
109     }
110 
111     if (Trace.isEnabled()) {
<lambda>null112         LaunchedEffect(onFirstFrameCaptureCompleted) {
113             snapshotFlow { previewUiState }
114                 .transformWhile {
115                     var continueCollecting = true
116                     (it as? PreviewUiState.Ready)?.let { ready ->
117                         if (ready.sessionFirstFrameTimestamp > 0) {
118                             emit(Unit)
119                             continueCollecting = false
120                         }
121                     }
122                     continueCollecting
123                 }.collect {
124                     onFirstFrameCaptureCompleted()
125                 }
126         }
127     }
128 
currentUiStatenull129     when (val currentUiState = previewUiState) {
130         is PreviewUiState.NotReady -> LoadingScreen()
131         is PreviewUiState.Ready -> {
132             val context = LocalContext.current
133             LaunchedEffect(Unit) {
134                 debouncedOrientationFlow(context).collect(viewModel::setDisplayRotation)
135             }
136 
137             ContentScreen(
138                 modifier = modifier,
139                 previewUiState = currentUiState,
140                 screenFlashUiState = screenFlashUiState,
141                 surfaceRequest = surfaceRequest,
142                 onNavigateToSettings = onNavigateToSettings,
143                 onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness,
144                 onSetLensFacing = viewModel::setLensFacing,
145                 onTapToFocus = viewModel::tapToFocus,
146                 onChangeZoomScale = viewModel::setZoomScale,
147                 onChangeFlash = viewModel::setFlash,
148                 onChangeAspectRatio = viewModel::setAspectRatio,
149                 onSetStreamConfig = viewModel::setStreamConfig,
150                 onChangeDynamicRange = viewModel::setDynamicRange,
151                 onChangeConcurrentCameraMode = viewModel::setConcurrentCameraMode,
152                 onChangeImageFormat = viewModel::setImageFormat,
153                 onToggleWhenDisabled = viewModel::showSnackBarForDisabledHdrToggle,
154                 onToggleQuickSettings = viewModel::toggleQuickSettings,
155                 onToggleDebugOverlay = viewModel::toggleDebugOverlay,
156                 onSetPause = viewModel::setPaused,
157                 onSetAudioEnabled = viewModel::setAudioEnabled,
158                 onCaptureImageWithUri = viewModel::captureImageWithUri,
159                 onStartVideoRecording = viewModel::startVideoRecording,
160                 onStopVideoRecording = viewModel::stopVideoRecording,
161                 onLockVideoRecording = viewModel::setLockedRecording,
162                 onToastShown = viewModel::onToastShown,
163                 onRequestWindowColorMode = onRequestWindowColorMode,
164                 onSnackBarResult = viewModel::onSnackBarResult,
165                 isDebugMode = isDebugMode,
166                 onImageWellClick = { uri -> onNavigateToPostCapture(uri) }
167             )
168 
169             // TODO(yasith): Remove and use ImageRepository after implementing
170             LaunchedEffect(Unit) {
171                 val lastCapturedImageUri = getLastImageUri(context)
172                 lastCapturedImageUri?.let { uri ->
173                     viewModel.updateLastCapturedImageUri(uri)
174                 }
175             }
176         }
177     }
178 }
179 
180 @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
181 @Composable
ContentScreennull182 private fun ContentScreen(
183     previewUiState: PreviewUiState.Ready,
184     screenFlashUiState: ScreenFlash.ScreenFlashUiState,
185     surfaceRequest: SurfaceRequest?,
186     modifier: Modifier = Modifier,
187     onNavigateToSettings: () -> Unit = {},
<lambda>null188     onClearUiScreenBrightness: (Float) -> Unit = {},
<lambda>null189     onSetLensFacing: (newLensFacing: LensFacing) -> Unit = {},
_null190     onTapToFocus: (x: Float, y: Float) -> Unit = { _, _ -> },
<lambda>null191     onChangeZoomScale: (Float) -> Unit = {},
<lambda>null192     onChangeFlash: (FlashMode) -> Unit = {},
<lambda>null193     onChangeAspectRatio: (AspectRatio) -> Unit = {},
<lambda>null194     onSetStreamConfig: (StreamConfig) -> Unit = {},
<lambda>null195     onChangeDynamicRange: (DynamicRange) -> Unit = {},
<lambda>null196     onChangeConcurrentCameraMode: (ConcurrentCameraMode) -> Unit = {},
<lambda>null197     onChangeImageFormat: (ImageOutputFormat) -> Unit = {},
<lambda>null198     onToggleWhenDisabled: (CaptureModeToggleUiState.DisabledReason) -> Unit = {},
<lambda>null199     onToggleQuickSettings: () -> Unit = {},
<lambda>null200     onToggleDebugOverlay: () -> Unit = {},
<lambda>null201     onSetPause: (Boolean) -> Unit = {},
<lambda>null202     onSetAudioEnabled: (Boolean) -> Unit = {},
203     onCaptureImageWithUri: (
204         ContentResolver,
205         Uri?,
206         Boolean,
207         (PreviewViewModel.ImageCaptureEvent, Int) -> Unit
_null208     ) -> Unit = { _, _, _, _ -> },
209     onStartVideoRecording: (
210         Uri?,
211         Boolean,
212         (PreviewViewModel.VideoCaptureEvent) -> Unit
_null213     ) -> Unit = { _, _, _ -> },
<lambda>null214     onStopVideoRecording: () -> Unit = {},
<lambda>null215     onLockVideoRecording: (Boolean) -> Unit = {},
<lambda>null216     onToastShown: () -> Unit = {},
<lambda>null217     onRequestWindowColorMode: (Int) -> Unit = {},
<lambda>null218     onSnackBarResult: (String) -> Unit = {},
219     isDebugMode: Boolean = false,
<lambda>null220     onImageWellClick: (uri: Uri?) -> Unit = {}
221 ) {
<lambda>null222     val snackbarHostState = remember { SnackbarHostState() }
223     Scaffold(
<lambda>null224         snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
<lambda>null225     ) {
226         val lensFacing by rememberUpdatedState(
227             previewUiState.currentCameraSettings.cameraLensFacing
228         )
229 
230         val onFlipCamera = { onSetLensFacing(lensFacing.flip()) }
231 
232         val isAudioEnabled = remember(previewUiState) {
233             previewUiState.currentCameraSettings.audioEnabled
234         }
235         val onToggleAudio = remember(isAudioEnabled) {
236             {
237                 onSetAudioEnabled(!isAudioEnabled)
238             }
239         }
240 
241         Box(modifier.fillMaxSize()) {
242             // display camera feed. this stays behind everything else
243             PreviewDisplay(
244                 previewUiState = previewUiState,
245                 onFlipCamera = onFlipCamera,
246                 onTapToFocus = onTapToFocus,
247                 onZoomChange = onChangeZoomScale,
248                 aspectRatio = previewUiState.currentCameraSettings.aspectRatio,
249                 surfaceRequest = surfaceRequest,
250                 onRequestWindowColorMode = onRequestWindowColorMode
251             )
252 
253             QuickSettingsScreenOverlay(
254                 modifier = Modifier,
255                 previewUiState = previewUiState,
256                 isOpen = previewUiState.quickSettingsIsOpen,
257                 toggleIsOpen = onToggleQuickSettings,
258                 currentCameraSettings = previewUiState.currentCameraSettings,
259                 onLensFaceClick = onSetLensFacing,
260                 onFlashModeClick = onChangeFlash,
261                 onAspectRatioClick = onChangeAspectRatio,
262                 onStreamConfigClick = onSetStreamConfig,
263                 onDynamicRangeClick = onChangeDynamicRange,
264                 onImageOutputFormatClick = onChangeImageFormat,
265                 onConcurrentCameraModeClick = onChangeConcurrentCameraMode
266             )
267             // relative-grid style overlay on top of preview display
268             CameraControlsOverlay(
269                 previewUiState = previewUiState,
270                 onNavigateToSettings = onNavigateToSettings,
271                 onFlipCamera = onFlipCamera,
272                 onChangeFlash = onChangeFlash,
273                 onToggleAudio = onToggleAudio,
274                 onToggleQuickSettings = onToggleQuickSettings,
275                 onToggleDebugOverlay = onToggleDebugOverlay,
276                 onChangeImageFormat = onChangeImageFormat,
277                 onToggleWhenDisabled = onToggleWhenDisabled,
278                 onSetPause = onSetPause,
279                 onCaptureImageWithUri = onCaptureImageWithUri,
280                 onStartVideoRecording = onStartVideoRecording,
281                 onStopVideoRecording = onStopVideoRecording,
282                 zoomLevelDisplayState = remember { ZoomLevelDisplayState(isDebugMode) },
283                 onImageWellClick = onImageWellClick,
284                 onLockVideoRecording = onLockVideoRecording
285             )
286 
287             DebugOverlayComponent(
288                 toggleIsOpen = onToggleDebugOverlay,
289                 previewUiState = previewUiState,
290                 onChangeZoomScale = onChangeZoomScale
291             )
292 
293             // displays toast when there is a message to show
294             if (previewUiState.toastMessageToShow != null) {
295                 TestableToast(
296                     modifier = Modifier.testTag(previewUiState.toastMessageToShow.testTag),
297                     toastMessage = previewUiState.toastMessageToShow,
298                     onToastShown = onToastShown
299                 )
300             }
301 
302             if (previewUiState.snackBarToShow != null) {
303                 TestableSnackbar(
304                     modifier = Modifier.testTag(previewUiState.snackBarToShow.testTag),
305                     snackbarToShow = previewUiState.snackBarToShow,
306                     snackbarHostState = snackbarHostState,
307                     onSnackbarResult = onSnackBarResult
308                 )
309             }
310             // Screen flash overlay that stays on top of everything but invisible normally. This should
311             // not be enabled based on whether screen flash is enabled because a previous image capture
312             // may still be running after flash mode change and clear actions (e.g. brightness restore)
313             // may need to be handled later. Compose smart recomposition should be able to optimize this
314             // if the relevant states are no longer changing.
315             ScreenFlashScreen(
316                 screenFlashUiState = screenFlashUiState,
317                 onInitialBrightnessCalculated = onClearUiScreenBrightness
318             )
319         }
320     }
321 }
322 
323 @Composable
LoadingScreennull324 private fun LoadingScreen(modifier: Modifier = Modifier) {
325     Column(
326         modifier = modifier
327             .fillMaxSize()
328             .background(Color.Black),
329         verticalArrangement = Arrangement.Center,
330         horizontalAlignment = Alignment.CenterHorizontally
331     ) {
332         CircularProgressIndicator(modifier = Modifier.size(50.dp))
333         Text(text = stringResource(R.string.camera_not_ready), color = Color.White)
334     }
335 }
336 
337 @Preview
338 @Composable
ContentScreenPreviewnull339 private fun ContentScreenPreview() {
340     MaterialTheme {
341         ContentScreen(
342             previewUiState = FAKE_PREVIEW_UI_STATE_READY,
343             screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
344             surfaceRequest = null
345         )
346     }
347 }
348 
349 @Preview
350 @Composable
ContentScreen_Standard_Idlenull351 private fun ContentScreen_Standard_Idle() {
352     MaterialTheme(colorScheme = darkColorScheme()) {
353         ContentScreen(
354             previewUiState = FAKE_PREVIEW_UI_STATE_READY.copy(),
355             screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
356             surfaceRequest = null
357         )
358     }
359 }
360 
361 @Preview
362 @Composable
ContentScreen_ImageOnly_Idlenull363 private fun ContentScreen_ImageOnly_Idle() {
364     MaterialTheme(colorScheme = darkColorScheme()) {
365         ContentScreen(
366             previewUiState = FAKE_PREVIEW_UI_STATE_READY.copy(
367                 captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.IMAGE_ONLY)
368             ),
369             screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
370             surfaceRequest = null
371         )
372     }
373 }
374 
375 @Preview
376 @Composable
ContentScreen_VideoOnly_Idlenull377 private fun ContentScreen_VideoOnly_Idle() {
378     MaterialTheme(colorScheme = darkColorScheme()) {
379         ContentScreen(
380             previewUiState = FAKE_PREVIEW_UI_STATE_READY.copy(
381                 captureButtonUiState = CaptureButtonUiState.Enabled.Idle(CaptureMode.VIDEO_ONLY)
382             ),
383             screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
384             surfaceRequest = null
385         )
386     }
387 }
388 
389 @Preview
390 @Composable
ContentScreen_Standard_Recordingnull391 private fun ContentScreen_Standard_Recording() {
392     MaterialTheme(colorScheme = darkColorScheme()) {
393         ContentScreen(
394             previewUiState = FAKE_PREVIEW_UI_STATE_PRESSED_RECORDING,
395             screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
396             surfaceRequest = null
397         )
398     }
399 }
400 
401 @Preview
402 @Composable
ContentScreen_Locked_Recordingnull403 private fun ContentScreen_Locked_Recording() {
404     MaterialTheme(colorScheme = darkColorScheme()) {
405         ContentScreen(
406             previewUiState = FAKE_PREVIEW_UI_STATE_LOCKED_RECORDING,
407             screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
408             surfaceRequest = null
409         )
410     }
411 }
412 
413 private val FAKE_PREVIEW_UI_STATE_READY = PreviewUiState.Ready(
414     currentCameraSettings = DEFAULT_CAMERA_APP_SETTINGS,
415     videoRecordingState = VideoRecordingState.Inactive(),
416     systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS,
<lambda>null417     previewMode = PreviewMode.StandardMode {},
418     captureModeToggleUiState = CaptureModeToggleUiState.Invisible
419 )
420 
421 private val FAKE_PREVIEW_UI_STATE_PRESSED_RECORDING = FAKE_PREVIEW_UI_STATE_READY.copy(
422     videoRecordingState = VideoRecordingState.Active.Recording(0, 0.0, 0),
423     captureButtonUiState = CaptureButtonUiState.Enabled.Recording.PressedRecording,
424     audioUiState = AudioUiState.Enabled.On(1.0)
425 )
426 
427 private val FAKE_PREVIEW_UI_STATE_LOCKED_RECORDING = FAKE_PREVIEW_UI_STATE_READY.copy(
428     videoRecordingState = VideoRecordingState.Active.Recording(0, 0.0, 0),
429     captureButtonUiState = CaptureButtonUiState.Enabled.Recording.LockedRecording,
430     audioUiState = AudioUiState.Enabled.On(1.0)
431 )
432