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