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