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.snapshotFlow
42 import androidx.compose.ui.Alignment
43 import androidx.compose.ui.Modifier
44 import androidx.compose.ui.graphics.Color
45 import androidx.compose.ui.platform.LocalContext
46 import androidx.compose.ui.platform.testTag
47 import androidx.compose.ui.res.stringResource
48 import androidx.compose.ui.tooling.preview.Preview
49 import androidx.compose.ui.unit.dp
50 import androidx.hilt.navigation.compose.hiltViewModel
51 import androidx.lifecycle.compose.LifecycleStartEffect
52 import androidx.tracing.Trace
53 import com.google.jetpackcamera.feature.preview.quicksettings.QuickSettingsScreenOverlay
54 import com.google.jetpackcamera.feature.preview.ui.CameraControlsOverlay
55 import com.google.jetpackcamera.feature.preview.ui.PreviewDisplay
56 import com.google.jetpackcamera.feature.preview.ui.ScreenFlashScreen
57 import com.google.jetpackcamera.feature.preview.ui.TestableSnackbar
58 import com.google.jetpackcamera.feature.preview.ui.TestableToast
59 import com.google.jetpackcamera.feature.preview.ui.debouncedOrientationFlow
60 import com.google.jetpackcamera.settings.model.AspectRatio
61 import com.google.jetpackcamera.settings.model.CaptureMode
62 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
63 import com.google.jetpackcamera.settings.model.DEFAULT_CAMERA_APP_SETTINGS
64 import com.google.jetpackcamera.settings.model.DynamicRange
65 import com.google.jetpackcamera.settings.model.FlashMode
66 import com.google.jetpackcamera.settings.model.ImageOutputFormat
67 import com.google.jetpackcamera.settings.model.LensFacing
68 import com.google.jetpackcamera.settings.model.LowLightBoost
69 import com.google.jetpackcamera.settings.model.TYPICAL_SYSTEM_CONSTRAINTS
70 import kotlinx.coroutines.flow.transformWhile
71
72 private const val TAG = "PreviewScreen"
73
74 /**
75 * Screen used for the Preview feature.
76 */
77 @Composable
78 fun PreviewScreen(
79 onNavigateToSettings: () -> Unit,
80 previewMode: PreviewMode,
81 isDebugMode: Boolean,
82 modifier: Modifier = Modifier,
83 onRequestWindowColorMode: (Int) -> Unit = {},
<lambda>null84 onFirstFrameCaptureCompleted: () -> Unit = {},
85 viewModel: PreviewViewModel = hiltViewModel<PreviewViewModel, PreviewViewModel.Factory>
factorynull86 { factory -> factory.create(previewMode, isDebugMode) }
87 ) {
88 Log.d(TAG, "PreviewScreen")
89
90 val previewUiState: PreviewUiState by viewModel.previewUiState.collectAsState()
91
92 val screenFlashUiState: ScreenFlash.ScreenFlashUiState
93 by viewModel.screenFlash.screenFlashUiState.collectAsState()
94
95 val surfaceRequest: SurfaceRequest?
96 by viewModel.surfaceRequest.collectAsState()
97
<lambda>null98 LifecycleStartEffect(Unit) {
99 viewModel.startCamera()
100 onStopOrDispose {
101 viewModel.stopCamera()
102 }
103 }
104
105 if (Trace.isEnabled()) {
<lambda>null106 LaunchedEffect(onFirstFrameCaptureCompleted) {
107 snapshotFlow { previewUiState }
108 .transformWhile {
109 var continueCollecting = true
110 (it as? PreviewUiState.Ready)?.let { ready ->
111 if (ready.sessionFirstFrameTimestamp > 0) {
112 emit(Unit)
113 continueCollecting = false
114 }
115 }
116 continueCollecting
117 }.collect {
118 onFirstFrameCaptureCompleted()
119 }
120 }
121 }
122
currentUiStatenull123 when (val currentUiState = previewUiState) {
124 is PreviewUiState.NotReady -> LoadingScreen()
125 is PreviewUiState.Ready -> {
126 val context = LocalContext.current
127 LaunchedEffect(Unit) {
128 debouncedOrientationFlow(context).collect(viewModel::setDisplayRotation)
129 }
130
131 ContentScreen(
132 modifier = modifier,
133 previewUiState = currentUiState,
134 screenFlashUiState = screenFlashUiState,
135 surfaceRequest = surfaceRequest,
136 onNavigateToSettings = onNavigateToSettings,
137 onClearUiScreenBrightness = viewModel.screenFlash::setClearUiScreenBrightness,
138 onSetLensFacing = viewModel::setLensFacing,
139 onTapToFocus = viewModel::tapToFocus,
140 onChangeZoomScale = viewModel::setZoomScale,
141 onChangeFlash = viewModel::setFlash,
142 onChangeAspectRatio = viewModel::setAspectRatio,
143 onChangeCaptureMode = viewModel::setCaptureMode,
144 onChangeDynamicRange = viewModel::setDynamicRange,
145 onChangeConcurrentCameraMode = viewModel::setConcurrentCameraMode,
146 onLowLightBoost = viewModel::setLowLightBoost,
147 onChangeImageFormat = viewModel::setImageFormat,
148 onToggleWhenDisabled = viewModel::showSnackBarForDisabledHdrToggle,
149 onToggleQuickSettings = viewModel::toggleQuickSettings,
150 onMuteAudio = viewModel::setAudioMuted,
151 onCaptureImage = viewModel::captureImage,
152 onCaptureImageWithUri = viewModel::captureImageWithUri,
153 onStartVideoRecording = viewModel::startVideoRecording,
154 onStopVideoRecording = viewModel::stopVideoRecording,
155 onToastShown = viewModel::onToastShown,
156 onRequestWindowColorMode = onRequestWindowColorMode,
157 onSnackBarResult = viewModel::onSnackBarResult
158 )
159 }
160 }
161 }
162
163 @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
164 @Composable
ContentScreennull165 private fun ContentScreen(
166 previewUiState: PreviewUiState.Ready,
167 screenFlashUiState: ScreenFlash.ScreenFlashUiState,
168 surfaceRequest: SurfaceRequest?,
169 modifier: Modifier = Modifier,
170 onNavigateToSettings: () -> Unit = {},
<lambda>null171 onClearUiScreenBrightness: (Float) -> Unit = {},
<lambda>null172 onSetLensFacing: (newLensFacing: LensFacing) -> Unit = {},
_null173 onTapToFocus: (x: Float, y: Float) -> Unit = { _, _ -> },
<lambda>null174 onChangeZoomScale: (Float) -> Unit = {},
<lambda>null175 onChangeFlash: (FlashMode) -> Unit = {},
<lambda>null176 onChangeAspectRatio: (AspectRatio) -> Unit = {},
<lambda>null177 onChangeCaptureMode: (CaptureMode) -> Unit = {},
<lambda>null178 onChangeDynamicRange: (DynamicRange) -> Unit = {},
<lambda>null179 onChangeConcurrentCameraMode: (ConcurrentCameraMode) -> Unit = {},
<lambda>null180 onLowLightBoost: (LowLightBoost) -> Unit = {},
<lambda>null181 onChangeImageFormat: (ImageOutputFormat) -> Unit = {},
<lambda>null182 onToggleWhenDisabled: (CaptureModeToggleUiState.DisabledReason) -> Unit = {},
<lambda>null183 onToggleQuickSettings: () -> Unit = {},
<lambda>null184 onMuteAudio: (Boolean) -> Unit = {},
<lambda>null185 onCaptureImage: () -> Unit = {},
186 onCaptureImageWithUri: (
187 ContentResolver,
188 Uri?,
189 Boolean,
190 (PreviewViewModel.ImageCaptureEvent) -> Unit
_null191 ) -> Unit = { _, _, _, _ -> },
192 onStartVideoRecording: (
193 Uri?,
194 Boolean,
195 (PreviewViewModel.VideoCaptureEvent) -> Unit
_null196 ) -> Unit = { _, _, _ -> },
<lambda>null197 onStopVideoRecording: () -> Unit = {},
<lambda>null198 onToastShown: () -> Unit = {},
<lambda>null199 onRequestWindowColorMode: (Int) -> Unit = {},
<lambda>null200 onSnackBarResult: (String) -> Unit = {}
201 ) {
<lambda>null202 val snackbarHostState = remember { SnackbarHostState() }
203 Scaffold(
<lambda>null204 snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
<lambda>null205 ) {
206 val lensFacing = remember(previewUiState) {
207 previewUiState.currentCameraSettings.cameraLensFacing
208 }
209
210 val onFlipCamera = remember(lensFacing) {
211 {
212 onSetLensFacing(lensFacing.flip())
213 }
214 }
215
216 val isMuted = remember(previewUiState) {
217 previewUiState.currentCameraSettings.audioMuted
218 }
219 val onToggleMuteAudio = remember(isMuted) {
220 {
221 onMuteAudio(!isMuted)
222 }
223 }
224
225 Box(modifier.fillMaxSize()) {
226 // display camera feed. this stays behind everything else
227 PreviewDisplay(
228 previewUiState = previewUiState,
229 onFlipCamera = onFlipCamera,
230 onTapToFocus = onTapToFocus,
231 onZoomChange = onChangeZoomScale,
232 aspectRatio = previewUiState.currentCameraSettings.aspectRatio,
233 surfaceRequest = surfaceRequest,
234 onRequestWindowColorMode = onRequestWindowColorMode
235 )
236
237 QuickSettingsScreenOverlay(
238 modifier = Modifier,
239 previewUiState = previewUiState,
240 isOpen = previewUiState.quickSettingsIsOpen,
241 toggleIsOpen = onToggleQuickSettings,
242 currentCameraSettings = previewUiState.currentCameraSettings,
243 onLensFaceClick = onSetLensFacing,
244 onFlashModeClick = onChangeFlash,
245 onAspectRatioClick = onChangeAspectRatio,
246 onCaptureModeClick = onChangeCaptureMode,
247 onDynamicRangeClick = onChangeDynamicRange,
248 onImageOutputFormatClick = onChangeImageFormat,
249 onConcurrentCameraModeClick = onChangeConcurrentCameraMode,
250 onLowLightBoostClick = onLowLightBoost
251 )
252 // relative-grid style overlay on top of preview display
253 CameraControlsOverlay(
254 previewUiState = previewUiState,
255 onNavigateToSettings = onNavigateToSettings,
256 onFlipCamera = onFlipCamera,
257 onChangeFlash = onChangeFlash,
258 onMuteAudio = onToggleMuteAudio,
259 onToggleQuickSettings = onToggleQuickSettings,
260 onChangeImageFormat = onChangeImageFormat,
261 onToggleWhenDisabled = onToggleWhenDisabled,
262 onCaptureImage = onCaptureImage,
263 onCaptureImageWithUri = onCaptureImageWithUri,
264 onStartVideoRecording = onStartVideoRecording,
265 onStopVideoRecording = onStopVideoRecording
266 )
267 // displays toast when there is a message to show
268 if (previewUiState.toastMessageToShow != null) {
269 TestableToast(
270 modifier = Modifier.testTag(previewUiState.toastMessageToShow.testTag),
271 toastMessage = previewUiState.toastMessageToShow,
272 onToastShown = onToastShown
273 )
274 }
275
276 if (previewUiState.snackBarToShow != null) {
277 TestableSnackbar(
278 modifier = Modifier.testTag(previewUiState.snackBarToShow.testTag),
279 snackbarToShow = previewUiState.snackBarToShow,
280 snackbarHostState = snackbarHostState,
281 onSnackbarResult = onSnackBarResult
282 )
283 }
284 // Screen flash overlay that stays on top of everything but invisible normally. This should
285 // not be enabled based on whether screen flash is enabled because a previous image capture
286 // may still be running after flash mode change and clear actions (e.g. brightness restore)
287 // may need to be handled later. Compose smart recomposition should be able to optimize this
288 // if the relevant states are no longer changing.
289 ScreenFlashScreen(
290 screenFlashUiState = screenFlashUiState,
291 onInitialBrightnessCalculated = onClearUiScreenBrightness
292 )
293 }
294 }
295 }
296
297 @Composable
LoadingScreennull298 private fun LoadingScreen(modifier: Modifier = Modifier) {
299 Column(
300 modifier = modifier
301 .fillMaxSize()
302 .background(Color.Black),
303 verticalArrangement = Arrangement.Center,
304 horizontalAlignment = Alignment.CenterHorizontally
305 ) {
306 CircularProgressIndicator(modifier = Modifier.size(50.dp))
307 Text(text = stringResource(R.string.camera_not_ready), color = Color.White)
308 }
309 }
310
311 @Preview
312 @Composable
ContentScreenPreviewnull313 private fun ContentScreenPreview() {
314 MaterialTheme {
315 ContentScreen(
316 previewUiState = FAKE_PREVIEW_UI_STATE_READY,
317 screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
318 surfaceRequest = null
319 )
320 }
321 }
322
323 @Preview
324 @Composable
ContentScreen_WhileRecordingnull325 private fun ContentScreen_WhileRecording() {
326 MaterialTheme(colorScheme = darkColorScheme()) {
327 ContentScreen(
328 previewUiState = FAKE_PREVIEW_UI_STATE_READY.copy(
329 videoRecordingState = VideoRecordingState.ACTIVE
330 ),
331 screenFlashUiState = ScreenFlash.ScreenFlashUiState(),
332 surfaceRequest = null
333 )
334 }
335 }
336
337 private val FAKE_PREVIEW_UI_STATE_READY = PreviewUiState.Ready(
338 currentCameraSettings = DEFAULT_CAMERA_APP_SETTINGS,
339 systemConstraints = TYPICAL_SYSTEM_CONSTRAINTS,
<lambda>null340 previewMode = PreviewMode.StandardMode {},
341 captureModeToggleUiState = CaptureModeToggleUiState.Invisible
342 )
343