• 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.settings.ui
17 
18 import android.content.res.Configuration
19 import androidx.compose.foundation.clickable
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.Row
22 import androidx.compose.foundation.layout.fillMaxWidth
23 import androidx.compose.foundation.layout.padding
24 import androidx.compose.foundation.rememberScrollState
25 import androidx.compose.foundation.selection.selectable
26 import androidx.compose.foundation.selection.selectableGroup
27 import androidx.compose.foundation.selection.toggleable
28 import androidx.compose.foundation.verticalScroll
29 import androidx.compose.material.icons.Icons
30 import androidx.compose.material.icons.automirrored.filled.ArrowBack
31 import androidx.compose.material3.AlertDialog
32 import androidx.compose.material3.ExperimentalMaterial3Api
33 import androidx.compose.material3.Icon
34 import androidx.compose.material3.IconButton
35 import androidx.compose.material3.ListItem
36 import androidx.compose.material3.LocalContentColor
37 import androidx.compose.material3.MaterialTheme
38 import androidx.compose.material3.RadioButton
39 import androidx.compose.material3.Switch
40 import androidx.compose.material3.Text
41 import androidx.compose.material3.TopAppBar
42 import androidx.compose.material3.TopAppBarScrollBehavior
43 import androidx.compose.runtime.Composable
44 import androidx.compose.runtime.MutableState
45 import androidx.compose.runtime.ReadOnlyComposable
46 import androidx.compose.runtime.mutableStateOf
47 import androidx.compose.runtime.remember
48 import androidx.compose.ui.Alignment
49 import androidx.compose.ui.Modifier
50 import androidx.compose.ui.graphics.Color
51 import androidx.compose.ui.platform.testTag
52 import androidx.compose.ui.res.stringResource
53 import androidx.compose.ui.semantics.Role
54 import androidx.compose.ui.text.font.FontStyle
55 import androidx.compose.ui.text.intl.Locale
56 import androidx.compose.ui.text.toUpperCase
57 import androidx.compose.ui.tooling.preview.Preview
58 import androidx.compose.ui.unit.dp
59 import androidx.compose.ui.unit.sp
60 import com.google.jetpackcamera.settings.AspectRatioUiState
61 import com.google.jetpackcamera.settings.AudioUiState
62 import com.google.jetpackcamera.settings.DarkModeUiState
63 import com.google.jetpackcamera.settings.DisabledRationale
64 import com.google.jetpackcamera.settings.FIVE_SECONDS_DURATION
65 import com.google.jetpackcamera.settings.FlashUiState
66 import com.google.jetpackcamera.settings.FlipLensUiState
67 import com.google.jetpackcamera.settings.FpsUiState
68 import com.google.jetpackcamera.settings.MaxVideoDurationUiState
69 import com.google.jetpackcamera.settings.R
70 import com.google.jetpackcamera.settings.SIXTY_SECONDS_DURATION
71 import com.google.jetpackcamera.settings.SingleSelectableState
72 import com.google.jetpackcamera.settings.StabilizationUiState
73 import com.google.jetpackcamera.settings.StreamConfigUiState
74 import com.google.jetpackcamera.settings.TEN_SECONDS_DURATION
75 import com.google.jetpackcamera.settings.THIRTY_SECONDS_DURATION
76 import com.google.jetpackcamera.settings.UNLIMITED_VIDEO_DURATION
77 import com.google.jetpackcamera.settings.VideoQualityUiState
78 import com.google.jetpackcamera.settings.model.AspectRatio
79 import com.google.jetpackcamera.settings.model.CameraConstraints.Companion.FPS_15
80 import com.google.jetpackcamera.settings.model.CameraConstraints.Companion.FPS_30
81 import com.google.jetpackcamera.settings.model.CameraConstraints.Companion.FPS_60
82 import com.google.jetpackcamera.settings.model.CameraConstraints.Companion.FPS_AUTO
83 import com.google.jetpackcamera.settings.model.DarkMode
84 import com.google.jetpackcamera.settings.model.FlashMode
85 import com.google.jetpackcamera.settings.model.LensFacing
86 import com.google.jetpackcamera.settings.model.StabilizationMode
87 import com.google.jetpackcamera.settings.model.StreamConfig
88 import com.google.jetpackcamera.settings.model.VideoQuality
89 import com.google.jetpackcamera.settings.ui.theme.SettingsPreviewTheme
90 
91 /**
92  * MAJOR SETTING UI COMPONENTS
93  * these are ready to be popped into the ui
94  */
95 
96 @OptIn(ExperimentalMaterial3Api::class)
97 @Composable
98 fun SettingsPageHeader(
99     title: String,
100     navBack: () -> Unit,
101     modifier: Modifier = Modifier,
102     scrollBehavior: TopAppBarScrollBehavior? = null
103 ) {
104     TopAppBar(
105         modifier = modifier,
106         title = {
107             Text(title)
108         },
109         navigationIcon = {
110             IconButton(
111                 modifier = Modifier.testTag(BACK_BUTTON),
112                 onClick = { navBack() }
113             ) {
114                 Icon(
115                     Icons.AutoMirrored.Filled.ArrowBack,
116                     stringResource(id = R.string.nav_back_accessibility)
117                 )
118             }
119         },
120         scrollBehavior = scrollBehavior
121     )
122 }
123 
124 @Composable
SectionHeadernull125 fun SectionHeader(title: String, modifier: Modifier = Modifier) {
126     Text(
127         modifier = modifier
128             .padding(start = 20.dp, top = 10.dp),
129         text = title,
130         color = MaterialTheme.colorScheme.primary,
131         fontSize = 18.sp
132     )
133 }
134 
135 @Composable
DarkModeSettingnull136 fun DarkModeSetting(
137     darkModeUiState: DarkModeUiState,
138     setDarkMode: (DarkMode) -> Unit,
139     modifier: Modifier = Modifier
140 ) {
141     BasicPopupSetting(
142         modifier = modifier.testTag(BTN_OPEN_DIALOG_SETTING_DARK_MODE_TAG),
143         title = stringResource(id = R.string.dark_mode_title),
144         leadingIcon = null,
145         enabled = true,
146         description = when (darkModeUiState) {
147             is DarkModeUiState.Enabled -> {
148                 when (darkModeUiState.currentDarkMode) {
149                     DarkMode.SYSTEM -> stringResource(id = R.string.dark_mode_description_system)
150                     DarkMode.DARK -> stringResource(id = R.string.dark_mode_description_dark)
151                     DarkMode.LIGHT -> stringResource(id = R.string.dark_mode_description_light)
152                 }
153             }
154         },
155         popupContents = {
156             Column(Modifier.selectableGroup()) {
157                 SingleChoiceSelector(
158                     modifier = modifier.testTag(BTN_DIALOG_DARK_MODE_OPTION_ON_TAG),
159                     text = stringResource(id = R.string.dark_mode_selector_dark),
160                     selected = darkModeUiState.currentDarkMode == DarkMode.DARK,
161                     enabled = true,
162                     onClick = { setDarkMode(DarkMode.DARK) }
163                 )
164                 SingleChoiceSelector(
165                     modifier = modifier.testTag(BTN_DIALOG_DARK_MODE_OPTION_OFF_TAG),
166                     text = stringResource(id = R.string.dark_mode_selector_light),
167                     selected = darkModeUiState.currentDarkMode == DarkMode.LIGHT,
168                     enabled = true,
169                     onClick = { setDarkMode(DarkMode.LIGHT) }
170                 )
171                 SingleChoiceSelector(
172                     modifier = modifier.testTag(BTN_DIALOG_DARK_MODE_OPTION_SYSTEM_TAG),
173                     text = stringResource(id = R.string.dark_mode_selector_system),
174                     selected = darkModeUiState.currentDarkMode == DarkMode.SYSTEM,
175                     enabled = true,
176                     onClick = { setDarkMode(DarkMode.SYSTEM) }
177                 )
178             }
179         }
180     )
181 }
182 
183 @Composable
DefaultCameraFacingnull184 fun DefaultCameraFacing(
185     modifier: Modifier = Modifier,
186     lensUiState: FlipLensUiState,
187     setDefaultLensFacing: (LensFacing) -> Unit
188 ) {
189     SwitchSettingUI(
190         modifier = modifier.apply {
191             if (lensUiState is FlipLensUiState.Disabled) {
192                 testTag(lensUiState.disabledRationale.testTag)
193             }
194         },
195         title = stringResource(id = R.string.default_facing_camera_title),
196         description = when (lensUiState) {
197             is FlipLensUiState.Disabled -> {
198                 disabledRationaleString(disabledRationale = lensUiState.disabledRationale)
199             }
200 
201             is FlipLensUiState.Enabled -> {
202                 null
203             }
204         },
205         leadingIcon = null,
206         onSwitchChanged = { on ->
207             setDefaultLensFacing(if (on) LensFacing.FRONT else LensFacing.BACK)
208         },
209         settingValue = lensUiState.currentLensFacing == LensFacing.FRONT,
210         enabled = lensUiState is FlipLensUiState.Enabled
211     )
212 }
213 
214 @Composable
FlashModeSettingnull215 fun FlashModeSetting(
216     flashUiState: FlashUiState,
217     setFlashMode: (FlashMode) -> Unit,
218     modifier: Modifier = Modifier
219 ) {
220     BasicPopupSetting(
221         modifier = modifier.testTag(BTN_OPEN_DIALOG_SETTING_FLASH_TAG),
222         title = stringResource(id = R.string.flash_mode_title),
223         leadingIcon = null,
224         enabled = flashUiState is FlashUiState.Enabled,
225         description =
226         when (flashUiState) {
227             is FlashUiState.Enabled -> when (flashUiState.currentFlashMode) {
228                 FlashMode.AUTO -> stringResource(id = R.string.flash_mode_description_auto)
229                 FlashMode.ON -> stringResource(id = R.string.flash_mode_description_on)
230                 FlashMode.OFF -> stringResource(id = R.string.flash_mode_description_off)
231                 FlashMode.LOW_LIGHT_BOOST -> stringResource(
232                     id = R.string.flash_mode_description_llb
233                 )
234             }
235             is FlashUiState.Disabled -> stringResource(
236                 flashUiState.disabledRationale.reasonTextResId,
237                 stringResource(flashUiState.disabledRationale.affectedSettingNameResId)
238             )
239         },
240         popupContents = {
241             if (flashUiState is FlashUiState.Enabled) {
242                 Column(Modifier.selectableGroup()) {
243                     SingleChoiceSelector(
244                         modifier = Modifier.testTag(BTN_DIALOG_FLASH_OPTION_AUTO_TAG),
245                         text = stringResource(id = R.string.flash_mode_selector_auto),
246                         selected = flashUiState.currentFlashMode == FlashMode.AUTO,
247                         enabled = flashUiState.autoSelectableState is
248                             SingleSelectableState.Selectable,
249                         onClick = { setFlashMode(FlashMode.AUTO) }
250                     )
251 
252                     SingleChoiceSelector(
253                         modifier = Modifier.testTag(BTN_DIALOG_FLASH_OPTION_ON_TAG),
254                         text = stringResource(id = R.string.flash_mode_selector_on),
255                         selected = flashUiState.currentFlashMode == FlashMode.ON,
256                         enabled = flashUiState.onSelectableState is
257                             SingleSelectableState.Selectable,
258                         onClick = { setFlashMode(FlashMode.ON) }
259                     )
260 
261                     SingleChoiceSelector(
262                         modifier = Modifier.testTag(BTN_DIALOG_FLASH_OPTION_LLB_TAG),
263                         text = stringResource(id = R.string.flash_mode_selector_llb),
264                         selected = flashUiState.currentFlashMode == FlashMode.LOW_LIGHT_BOOST,
265                         enabled = flashUiState.lowLightSelectableState is
266                             SingleSelectableState.Selectable,
267                         onClick = { setFlashMode(FlashMode.LOW_LIGHT_BOOST) }
268                     )
269 
270                     SingleChoiceSelector(
271                         modifier = Modifier.testTag(BTN_DIALOG_FLASH_OPTION_OFF_TAG),
272                         text = stringResource(id = R.string.flash_mode_selector_off),
273                         selected = flashUiState.currentFlashMode == FlashMode.OFF,
274                         enabled = true,
275                         onClick = { setFlashMode(FlashMode.OFF) }
276                     )
277                 }
278             }
279         }
280     )
281 }
282 
283 @Composable
AspectRatioSettingnull284 fun AspectRatioSetting(
285     aspectRatioUiState: AspectRatioUiState,
286     setAspectRatio: (AspectRatio) -> Unit,
287     modifier: Modifier = Modifier
288 ) {
289     BasicPopupSetting(
290         modifier = modifier.testTag(BTN_OPEN_DIALOG_SETTING_ASPECT_RATIO_TAG),
291         title = stringResource(id = R.string.aspect_ratio_title),
292         leadingIcon = null,
293         description =
294         if (aspectRatioUiState is AspectRatioUiState.Enabled) {
295             when (aspectRatioUiState.currentAspectRatio) {
296                 AspectRatio.NINE_SIXTEEN -> stringResource(
297                     id = R.string.aspect_ratio_description_9_16
298                 )
299 
300                 AspectRatio.THREE_FOUR -> stringResource(id = R.string.aspect_ratio_description_3_4)
301                 AspectRatio.ONE_ONE -> stringResource(id = R.string.aspect_ratio_description_1_1)
302             }
303         } else {
304             TODO("aspect ratio currently has no disabled criteria")
305         },
306         enabled = true,
307         popupContents = {
308             Column(Modifier.selectableGroup()) {
309                 SingleChoiceSelector(
310                     modifier = Modifier.testTag(BTN_DIALOG_ASPECT_RATIO_OPTION_9_16_TAG),
311                     text = stringResource(id = R.string.aspect_ratio_selector_9_16),
312                     selected = aspectRatioUiState.currentAspectRatio == AspectRatio.NINE_SIXTEEN,
313                     enabled = true,
314                     onClick = { setAspectRatio(AspectRatio.NINE_SIXTEEN) }
315                 )
316                 SingleChoiceSelector(
317                     modifier = Modifier.testTag(BTN_DIALOG_ASPECT_RATIO_OPTION_3_4_TAG),
318                     text = stringResource(id = R.string.aspect_ratio_selector_3_4),
319                     selected = aspectRatioUiState.currentAspectRatio == AspectRatio.THREE_FOUR,
320                     enabled = true,
321                     onClick = { setAspectRatio(AspectRatio.THREE_FOUR) }
322                 )
323                 SingleChoiceSelector(
324                     modifier = Modifier.testTag(BTN_DIALOG_ASPECT_RATIO_OPTION_1_1_TAG),
325                     text = stringResource(id = R.string.aspect_ratio_selector_1_1),
326                     selected = aspectRatioUiState.currentAspectRatio == AspectRatio.ONE_ONE,
327                     enabled = true,
328                     onClick = { setAspectRatio(AspectRatio.ONE_ONE) }
329                 )
330             }
331         }
332     )
333 }
334 
335 @Composable
StreamConfigSettingnull336 fun StreamConfigSetting(
337     streamConfigUiState: StreamConfigUiState,
338     setStreamConfig: (StreamConfig) -> Unit,
339     modifier: Modifier = Modifier
340 ) {
341     BasicPopupSetting(
342         modifier = modifier.testTag(BTN_OPEN_DIALOG_SETTING_STREAM_CONFIG_TAG),
343         title = stringResource(R.string.stream_config_title),
344         leadingIcon = null,
345         enabled = true,
346         description =
347         if (streamConfigUiState is StreamConfigUiState.Enabled) {
348             when (streamConfigUiState.currentStreamConfig) {
349                 StreamConfig.MULTI_STREAM -> stringResource(
350                     id = R.string.stream_config_description_multi_stream
351                 )
352 
353                 StreamConfig.SINGLE_STREAM -> stringResource(
354                     id = R.string.stream_config_description_single_stream
355                 )
356             }
357         } else {
358             TODO("stream config currently has no disabled criteria")
359         },
360         popupContents = {
361             Column(Modifier.selectableGroup()) {
362                 SingleChoiceSelector(
363                     modifier = Modifier.testTag(
364                         BTN_DIALOG_STREAM_CONFIG_OPTION_MULTI_STREAM_CAPTURE_TAG
365                     ),
366                     text = stringResource(id = R.string.stream_config_selector_multi_stream),
367                     selected = streamConfigUiState.currentStreamConfig == StreamConfig.MULTI_STREAM,
368                     enabled = true,
369                     onClick = { setStreamConfig(StreamConfig.MULTI_STREAM) }
370                 )
371                 SingleChoiceSelector(
372                     modifier = Modifier.testTag(BTN_DIALOG_STREAM_CONFIG_OPTION_SINGLE_STREAM_TAG),
373                     text = stringResource(id = R.string.stream_config_description_single_stream),
374                     selected = streamConfigUiState.currentStreamConfig ==
375                         StreamConfig.SINGLE_STREAM,
376                     enabled = true,
377                     onClick = { setStreamConfig(StreamConfig.SINGLE_STREAM) }
378                 )
379             }
380         }
381     )
382 }
383 
getMaxVideoDurationTestTagnull384 private fun getMaxVideoDurationTestTag(videoDuration: Long): String = when (videoDuration) {
385     UNLIMITED_VIDEO_DURATION -> BTN_DIALOG_VIDEO_DURATION_OPTION_UNLIMITED_TAG
386     FIVE_SECONDS_DURATION -> BTN_DIALOG_VIDEO_DURATION_OPTION_1S_TAG
387     TEN_SECONDS_DURATION -> BTN_DIALOG_VIDEO_DURATION_OPTION_10S_TAG
388     THIRTY_SECONDS_DURATION -> BTN_DIALOG_VIDEO_DURATION_OPTION_30S_TAG
389     SIXTY_SECONDS_DURATION -> BTN_DIALOG_VIDEO_DURATION_OPTION_60S_TAG
390     else -> BTN_DIALOG_VIDEO_DURATION_OPTION_UNLIMITED_TAG
391 }
392 
393 @Composable
MaxVideoDurationSettingnull394 fun MaxVideoDurationSetting(
395     maxVideoDurationUiState: MaxVideoDurationUiState.Enabled,
396     setMaxDuration: (Long) -> Unit,
397     modifier: Modifier = Modifier
398 ) {
399     BasicPopupSetting(
400         modifier = modifier.testTag(BTN_OPEN_DIALOG_SETTING_VIDEO_DURATION_TAG),
401         enabled = true,
402         title = stringResource(R.string.duration_title),
403         leadingIcon = null,
404         description = when (val maxDuration = maxVideoDurationUiState.currentMaxDurationMillis) {
405             UNLIMITED_VIDEO_DURATION -> stringResource(R.string.duration_description_none)
406             else -> stringResource(R.string.duration_description_seconds, (maxDuration / 1000))
407         },
408         popupContents = {
409             Column(Modifier.selectableGroup()) {
410                 SingleChoiceSelector(
411                     modifier = modifier.testTag(
412                         getMaxVideoDurationTestTag(
413                             UNLIMITED_VIDEO_DURATION
414                         )
415                     ),
416                     enabled = true,
417                     text = stringResource(R.string.duration_description_none),
418                     selected = maxVideoDurationUiState.currentMaxDurationMillis
419                         == UNLIMITED_VIDEO_DURATION,
420                     onClick = { setMaxDuration(UNLIMITED_VIDEO_DURATION) }
421                 )
422                 listOf(
423                     FIVE_SECONDS_DURATION,
424                     TEN_SECONDS_DURATION,
425                     THIRTY_SECONDS_DURATION,
426                     SIXTY_SECONDS_DURATION
427                 ).forEach { maxDuration ->
428                     SingleChoiceSelector(
429                         modifier = Modifier.testTag(getMaxVideoDurationTestTag(maxDuration)),
430                         enabled = true,
431                         text = stringResource(
432                             R.string.duration_description_seconds,
433                             (maxDuration / 1000)
434                         ),
435                         selected = maxVideoDurationUiState.currentMaxDurationMillis == maxDuration,
436                         onClick = { setMaxDuration(maxDuration) }
437                     )
438                 }
439             }
440         }
441     )
442 }
443 
getTargetFpsTestTagnull444 private fun getTargetFpsTestTag(fpsOption: Int): String = when (fpsOption) {
445     FPS_15 -> BTN_DIALOG_FPS_OPTION_15_TAG
446     FPS_30 -> BTN_DIALOG_FPS_OPTION_30_TAG
447     FPS_60 -> BTN_DIALOG_FPS_OPTION_60_TAG
448     FPS_AUTO -> BTN_DIALOG_FPS_OPTION_AUTO_TAG
449     else -> BTN_DIALOG_FPS_OPTION_AUTO_TAG
450 }
451 
452 @Composable
TargetFpsSettingnull453 fun TargetFpsSetting(
454     fpsUiState: FpsUiState,
455     setTargetFps: (Int) -> Unit,
456     modifier: Modifier = Modifier
457 ) {
458     BasicPopupSetting(
459         modifier = modifier
460             .apply {
461                 if (fpsUiState is FpsUiState.Disabled) {
462                     testTag(fpsUiState.disabledRationale.testTag)
463                 } else {
464                     testTag(BTN_OPEN_DIALOG_SETTING_FPS_TAG)
465                 }
466             },
467         title = stringResource(id = R.string.fps_title),
468         enabled = fpsUiState is FpsUiState.Enabled,
469         leadingIcon = null,
470         description = if (fpsUiState is FpsUiState.Enabled) {
471             when (fpsUiState.currentSelection) {
472                 FPS_15 -> stringResource(id = R.string.fps_description, FPS_15)
473                 FPS_30 -> stringResource(id = R.string.fps_description, FPS_30)
474                 FPS_60 -> stringResource(id = R.string.fps_description, FPS_60)
475                 else -> stringResource(
476                     id = R.string.fps_description_auto
477                 )
478             }
479         } else {
480             disabledRationaleString((fpsUiState as FpsUiState.Disabled).disabledRationale)
481         },
482         popupContents = {
483             if (fpsUiState is FpsUiState.Enabled) {
484                 Column(Modifier.selectableGroup()) {
485                     Text(
486                         modifier = Modifier.testTag(getTargetFpsTestTag(FPS_AUTO)),
487                         text = stringResource(id = R.string.fps_stabilization_disclaimer),
488                         fontStyle = FontStyle.Italic,
489                         color = MaterialTheme.colorScheme.onPrimaryContainer
490                     )
491 
492                     SingleChoiceSelector(
493                         text = stringResource(id = R.string.fps_selector_auto),
494                         selected = fpsUiState.currentSelection == FPS_AUTO,
495                         onClick = { setTargetFps(FPS_AUTO) },
496                         enabled = fpsUiState.fpsAutoState is SingleSelectableState.Selectable
497                     )
498                     listOf(FPS_15, FPS_30, FPS_60).forEach { fpsOption ->
499                         SingleChoiceSelector(
500                             modifier = Modifier.testTag(getTargetFpsTestTag(fpsOption)),
501                             text = "%d".format(fpsOption),
502                             selected = fpsUiState.currentSelection == fpsOption,
503                             onClick = { setTargetFps(fpsOption) },
504                             enabled = when (fpsOption) {
505                                 FPS_15 ->
506                                     fpsUiState.fpsFifteenState is
507                                         SingleSelectableState.Selectable
508 
509                                 FPS_30 ->
510                                     fpsUiState.fpsThirtyState is
511                                         SingleSelectableState.Selectable
512 
513                                 FPS_60 ->
514                                     fpsUiState.fpsSixtyState is
515                                         SingleSelectableState.Selectable
516 
517                                 else -> false
518                             }
519                         )
520                     }
521                 }
522             }
523         }
524     )
525 }
526 
527 /**
528  * Returns the description text depending on the preview/video stabilization configuration.
529  * On - preview is on and video is NOT off.
530  * High Quality - preview is unspecified and video is ON.
531  * Off - Every other configuration.
532  */
getStabilizationStringResnull533 private fun getStabilizationStringRes(stabilizationMode: StabilizationMode): Int =
534     when (stabilizationMode) {
535         StabilizationMode.OFF -> R.string.stabilization_description_off
536         StabilizationMode.AUTO -> R.string.stabilization_description_auto
537         StabilizationMode.ON -> R.string.stabilization_description_on
538         StabilizationMode.HIGH_QUALITY -> R.string.stabilization_description_high_quality
539         StabilizationMode.OPTICAL -> R.string.stabilization_description_optical
540     }
541 
getVideoQualityStringResnull542 private fun getVideoQualityStringRes(videoQuality: VideoQuality): Int = when (videoQuality) {
543     VideoQuality.UNSPECIFIED -> R.string.video_quality_value_auto
544     VideoQuality.SD -> R.string.video_quality_value_sd
545     VideoQuality.HD -> R.string.video_quality_value_hd
546     VideoQuality.FHD -> R.string.video_quality_value_fhd
547     VideoQuality.UHD -> R.string.video_quality_value_uhd
548 }
549 
getVideoQualitySecondaryStringResnull550 private fun getVideoQualitySecondaryStringRes(videoQuality: VideoQuality): Int =
551     when (videoQuality) {
552         VideoQuality.UNSPECIFIED -> R.string.video_quality_value_auto_info
553         VideoQuality.SD -> R.string.video_quality_value_sd_info
554         VideoQuality.HD -> R.string.video_quality_value_hd_info
555         VideoQuality.FHD -> R.string.video_quality_value_fhd_info
556         VideoQuality.UHD -> R.string.video_quality_value_uhd_info
557     }
558 
getVideoQualityOptionTestTagnull559 private fun getVideoQualityOptionTestTag(quality: VideoQuality): String = when (quality) {
560     VideoQuality.UNSPECIFIED -> BTN_DIALOG_VIDEO_QUALITY_OPTION_UNSPECIFIED_TAG
561     VideoQuality.SD -> BTN_DIALOG_VIDEO_QUALITY_OPTION_SD_TAG
562     VideoQuality.HD -> BTN_DIALOG_VIDEO_QUALITY_OPTION_HD_TAG
563     VideoQuality.FHD -> BTN_DIALOG_VIDEO_QUALITY_OPTION_FHD_TAG
564     VideoQuality.UHD -> BTN_DIALOG_VIDEO_QUALITY_OPTION_UHD_TAG
565 }
566 
567 /**
568  * A Setting to set preview and video stabilization.
569  *
570  * ON - Both preview and video are stabilized.
571  * HIGH_QUALITY - Video will be stabilized, preview might be stabilized, depending on the device.
572  * OFF - Preview and video stabilization is disabled.
573  *
574  * @param stabilizationUiState the state for this setting.
575  */
576 @Composable
StabilizationSettingnull577 fun StabilizationSetting(
578     stabilizationUiState: StabilizationUiState,
579     setStabilizationMode: (StabilizationMode) -> Unit,
580     modifier: Modifier = Modifier
581 ) {
582     // entire setting disabled when no available fps or target fps = 60
583     // stabilization is unsupported >30 fps
584     BasicPopupSetting(
585         modifier = modifier.apply {
586             when (stabilizationUiState) {
587                 is StabilizationUiState.Disabled ->
588                     testTag(stabilizationUiState.disabledRationale.testTag)
589 
590                 else -> testTag(BTN_OPEN_DIALOG_SETTING_VIDEO_STABILIZATION_TAG)
591             }
592         },
593         title = stringResource(R.string.video_stabilization_title),
594         leadingIcon = null,
595         enabled = stabilizationUiState is StabilizationUiState.Enabled,
596         description = when (stabilizationUiState) {
597             is StabilizationUiState.Enabled ->
598                 stringResource(
599                     id = getStabilizationStringRes(stabilizationUiState.currentStabilizationMode)
600                 )
601 
602             is StabilizationUiState.Disabled -> {
603                 // disabled setting description
604                 disabledRationaleString(stabilizationUiState.disabledRationale)
605             }
606         },
607 
608         popupContents = {
609             Column(Modifier.selectableGroup()) {
610                 Text(
611                     text = stringResource(id = R.string.lens_stabilization_disclaimer),
612                     fontStyle = FontStyle.Italic,
613                     color = MaterialTheme.colorScheme.onPrimaryContainer
614                 )
615 
616                 // on (preview) selector
617                 // disabled if target fps != (30 or off)
618                 // TODO(b/328223562): device always resolves to 30fps when using preview stabilization
619                 when (stabilizationUiState) {
620                     is StabilizationUiState.Enabled -> {
621                         SingleChoiceSelector(
622                             modifier = Modifier.apply {
623                                 if (stabilizationUiState.stabilizationAutoState
624                                         is SingleSelectableState.Disabled
625                                 ) {
626                                     testTag(
627                                         stabilizationUiState.stabilizationAutoState
628                                             .disabledRationale.testTag
629                                     )
630                                 } else {
631                                     testTag(BTN_DIALOG_VIDEO_STABILIZATION_OPTION_AUTO_TAG)
632                                 }
633                             },
634                             text = stringResource(id = R.string.stabilization_selector_auto),
635                             secondaryText = stringResource(
636                                 id = R.string.stabilization_selector_auto_info
637                             ),
638                             enabled = stabilizationUiState.stabilizationAutoState is
639                                 SingleSelectableState.Selectable,
640                             selected = stabilizationUiState.currentStabilizationMode
641                                 == StabilizationMode.AUTO,
642                             onClick = {
643                                 setStabilizationMode(StabilizationMode.AUTO)
644                             }
645                         )
646 
647                         SingleChoiceSelector(
648                             modifier = Modifier.apply {
649                                 if (stabilizationUiState.stabilizationOnState
650                                         is SingleSelectableState.Disabled
651                                 ) {
652                                     testTag(
653                                         stabilizationUiState.stabilizationOnState
654                                             .disabledRationale.testTag
655                                     )
656                                 } else {
657                                     testTag(BTN_DIALOG_VIDEO_STABILIZATION_OPTION_ON_TAG)
658                                 }
659                             },
660                             text = stringResource(id = R.string.stabilization_selector_on),
661                             secondaryText = stringResource(
662                                 id = R.string.stabilization_selector_on_info
663                             ),
664                             enabled = stabilizationUiState.stabilizationOnState is
665                                 SingleSelectableState.Selectable,
666                             selected = stabilizationUiState.currentStabilizationMode
667                                 == StabilizationMode.ON,
668                             onClick = {
669                                 setStabilizationMode(StabilizationMode.ON)
670                             }
671                         )
672 
673                         // high quality selector
674                         // disabled if target fps = 60 (see VideoCapabilities.isStabilizationSupported)
675                         SingleChoiceSelector(
676                             modifier = Modifier.apply {
677                                 if (stabilizationUiState.stabilizationHighQualityState
678                                         is SingleSelectableState.Disabled
679                                 ) {
680                                     testTag(
681                                         stabilizationUiState.stabilizationHighQualityState
682                                             .disabledRationale.testTag
683                                     )
684                                 } else {
685                                     testTag(BTN_DIALOG_VIDEO_STABILIZATION_OPTION_HIGH_QUALITY_TAG)
686                                 }
687                             },
688                             text = stringResource(
689                                 id = R.string.stabilization_selector_high_quality
690                             ),
691                             secondaryText = stringResource(
692                                 id = R.string.stabilization_selector_high_quality_info
693                             ),
694                             enabled = stabilizationUiState.stabilizationHighQualityState
695                                 == SingleSelectableState.Selectable,
696 
697                             selected = stabilizationUiState.currentStabilizationMode
698                                 == StabilizationMode.HIGH_QUALITY,
699                             onClick = {
700                                 setStabilizationMode(StabilizationMode.HIGH_QUALITY)
701                             }
702                         )
703 
704                         // optical selector
705                         SingleChoiceSelector(
706                             modifier = Modifier.apply {
707                                 if (stabilizationUiState.stabilizationOpticalState
708                                         is SingleSelectableState.Disabled
709                                 ) {
710                                     testTag(
711                                         stabilizationUiState.stabilizationOpticalState
712                                             .disabledRationale.testTag
713                                     )
714                                 } else {
715                                     testTag(BTN_DIALOG_VIDEO_STABILIZATION_OPTION_OPTICAL_TAG)
716                                 }
717                             },
718                             text = stringResource(
719                                 id = R.string.stabilization_selector_optical
720                             ),
721                             secondaryText = stringResource(
722                                 id = R.string.stabilization_selector_optical_info
723                             ),
724                             enabled = stabilizationUiState.stabilizationOpticalState
725                                 == SingleSelectableState.Selectable,
726 
727                             selected = stabilizationUiState.currentStabilizationMode
728                                 == StabilizationMode.OPTICAL,
729                             onClick = {
730                                 setStabilizationMode(StabilizationMode.OPTICAL)
731                             }
732                         )
733 
734                         // off selector
735                         SingleChoiceSelector(
736                             modifier = Modifier.testTag(
737                                 BTN_DIALOG_VIDEO_STABILIZATION_OPTION_OFF_TAG
738                             ),
739                             text = stringResource(id = R.string.stabilization_selector_off),
740                             selected = stabilizationUiState.currentStabilizationMode
741                                 == StabilizationMode.OFF,
742                             onClick = {
743                                 setStabilizationMode(StabilizationMode.OFF)
744                             },
745                             enabled = true
746                         )
747                     }
748 
749                     else -> {}
750                 }
751             }
752         }
753     )
754 }
755 
756 @Composable
VideoQualitySettingnull757 fun VideoQualitySetting(
758     videQualityUiState: VideoQualityUiState,
759     setVideoQuality: (VideoQuality) -> Unit,
760     modifier: Modifier = Modifier
761 ) {
762     BasicPopupSetting(
763         modifier = modifier.testTag(BTN_OPEN_DIALOG_SETTING_VIDEO_QUALITY_TAG),
764         title = stringResource(R.string.video_quality_title),
765         leadingIcon = null,
766         enabled = videQualityUiState is VideoQualityUiState.Enabled,
767         description = when (videQualityUiState) {
768             is VideoQualityUiState.Enabled ->
769                 stringResource(getVideoQualityStringRes(videQualityUiState.currentVideoQuality))
770 
771             is VideoQualityUiState.Disabled -> {
772                 disabledRationaleString(
773                     disabledRationale = videQualityUiState.disabledRationale
774                 )
775             }
776         },
777         popupContents = {
778             Column(
779                 Modifier
780                     .selectableGroup()
781                     .verticalScroll(rememberScrollState())
782             ) {
783                 SingleChoiceSelector(
784                     modifier = Modifier.testTag(
785                         getVideoQualityOptionTestTag(VideoQuality.UNSPECIFIED)
786                     ),
787                     text = stringResource(getVideoQualityStringRes(VideoQuality.UNSPECIFIED)),
788                     secondaryText = stringResource(
789                         getVideoQualitySecondaryStringRes(
790                             VideoQuality.UNSPECIFIED
791                         )
792                     ),
793                     selected = (videQualityUiState as VideoQualityUiState.Enabled)
794                         .currentVideoQuality == VideoQuality.UNSPECIFIED,
795                     enabled = videQualityUiState.videoQualityAutoState is
796                         SingleSelectableState.Selectable,
797                     onClick = { setVideoQuality(VideoQuality.UNSPECIFIED) }
798                 )
799                 listOf(VideoQuality.SD, VideoQuality.HD, VideoQuality.FHD, VideoQuality.UHD)
800                     .forEach { videoQuality ->
801                         SingleChoiceSelector(
802                             modifier = Modifier.testTag(getVideoQualityOptionTestTag(videoQuality)),
803                             text = stringResource(getVideoQualityStringRes(videoQuality)),
804                             secondaryText = stringResource(
805                                 getVideoQualitySecondaryStringRes(
806                                     videoQuality
807                                 )
808                             ),
809                             selected = videQualityUiState.currentVideoQuality == videoQuality,
810                             enabled = videQualityUiState.getSelectableState(videoQuality) is
811                                 SingleSelectableState.Selectable,
812                             onClick = { setVideoQuality(videoQuality) }
813                         )
814                     }
815             }
816         }
817     )
818 }
819 
820 @Composable
RecordingAudioSettingnull821 fun RecordingAudioSetting(
822     modifier: Modifier = Modifier,
823     audioUiState: AudioUiState,
824     setDefaultAudio: (Boolean) -> Unit
825 ) {
826     SwitchSettingUI(
827         modifier = modifier.testTag(BTN_SWITCH_SETTING_ENABLE_AUDIO_TAG),
828         title = stringResource(id = R.string.audio_title),
829         description = when (audioUiState) {
830             is AudioUiState.Enabled.On -> {
831                 stringResource(R.string.audio_selector_on)
832             }
833             is AudioUiState.Enabled.Mute -> {
834                 stringResource(R.string.audio_selector_off)
835             }
836             is AudioUiState.Disabled -> {
837                 disabledRationaleString(disabledRationale = audioUiState.disabledRationale)
838             }
839         },
840         leadingIcon = null,
841         onSwitchChanged = { on -> setDefaultAudio(on) },
842         settingValue = when (audioUiState) {
843             is AudioUiState.Enabled.On -> true
844             is AudioUiState.Disabled, is AudioUiState.Enabled.Mute -> false
845         },
846         enabled = audioUiState is AudioUiState.Enabled
847     )
848 }
849 
850 @Composable
VersionInfonull851 fun VersionInfo(versionName: String, modifier: Modifier = Modifier, buildType: String = "") {
852     SettingUI(
853         modifier = modifier,
854         title = stringResource(id = R.string.version_info_title),
855         leadingIcon = null,
856         enabled = true
857     ) {
858         val versionString = versionName +
859             if (buildType.isNotEmpty()) {
860                 "/${buildType.toUpperCase(Locale.current)}"
861             } else {
862                 ""
863             }
864         Text(text = versionString, modifier = Modifier.testTag(TEXT_SETTING_APP_VERSION_TAG))
865     }
866 }
867 
868 /*
869  * Setting UI sub-Components
870  * small and whimsical :)
871  * don't use these directly, use them to build the ready-to-use setting components
872  */
873 
874 /** a composable for creating a simple popup setting **/
875 
876 @Composable
BasicPopupSettingnull877 fun BasicPopupSetting(
878     title: String,
879     description: String?,
880     leadingIcon: @Composable (() -> Unit)?,
881     popupContents: @Composable () -> Unit,
882     modifier: Modifier = Modifier,
883     enabled: Boolean,
884     popupStatus: MutableState<Boolean> = remember { mutableStateOf(false) }
885 ) {
886     SettingUI(
<lambda>null887         modifier = modifier.clickable(enabled = enabled) { popupStatus.value = true },
888         title = title,
889         enabled = enabled,
890         description = description,
891         leadingIcon = leadingIcon,
892         trailingContent = null
893     )
894     if (popupStatus.value) {
895         AlertDialog(
<lambda>null896             onDismissRequest = { popupStatus.value = false },
<lambda>null897             confirmButton = {
898                 Text(
899                     text = "Close",
900                     modifier = Modifier.clickable { popupStatus.value = false }
901                 )
902             },
<lambda>null903             title = { Text(text = title) },
<lambda>null904             text = {
905                 MaterialTheme(
906                     colorScheme = MaterialTheme.colorScheme.copy(surface = Color.Transparent),
907                     content = popupContents
908                 )
909             }
910         )
911     }
912 }
913 
914 /**
915  * A composable for creating a setting with a Switch.
916  *
917  * <p> the value should correspond to the setting's UI state value. the switch will only change
918  * appearance if the UI state has been successfully updated
919  */
920 @Composable
SwitchSettingUInull921 fun SwitchSettingUI(
922     title: String,
923     description: String?,
924     leadingIcon: @Composable (() -> Unit)?,
925     onSwitchChanged: (Boolean) -> Unit,
926     settingValue: Boolean,
927     enabled: Boolean,
928     modifier: Modifier = Modifier
929 ) {
930     SettingUI(
931         modifier = modifier
932             .toggleable(
933                 enabled = enabled,
934                 role = Role.Switch,
935                 value = settingValue,
936                 onValueChange = { value -> onSwitchChanged(value) }
937             )
938             .testTag(BTN_SWITCH_SETTING_LENS_FACING_TAG),
939         enabled = enabled,
940         title = title,
941         description = description,
942         leadingIcon = leadingIcon,
943         trailingContent = {
944             Switch(
945                 enabled = enabled,
946                 checked = settingValue,
947                 onCheckedChange = { value ->
948                     onSwitchChanged(value)
949                 }
950             )
951         }
952     )
953 }
954 
955 /**
956  * A composable used as a template used to construct other settings components
957  */
958 @Composable
SettingUInull959 fun SettingUI(
960     title: String,
961     leadingIcon: @Composable (() -> Unit)?,
962     modifier: Modifier = Modifier,
963     enabled: Boolean,
964     description: String? = null,
965     trailingContent: @Composable (() -> Unit)?
966 ) {
967     ListItem(
968         modifier = modifier,
969         headlineContent = {
970             if (enabled) {
971                 Text(title)
972             } else {
973                 Text(text = title, color = LocalContentColor.current.copy(alpha = .7f))
974             }
975         },
976         supportingContent = {
977             if (description != null) {
978                 if (enabled) {
979                     Text(description)
980                 } else {
981                     Text(
982                         text = description,
983                         color = LocalContentColor.current.copy(alpha = .7f)
984                     )
985                 }
986             }
987         },
988         leadingContent = leadingIcon,
989         trailingContent = trailingContent
990     )
991 }
992 
993 /**
994  * A component for a single-choice selector for a multiple choice list
995  */
996 @Composable
SingleChoiceSelectornull997 fun SingleChoiceSelector(
998     text: String,
999     selected: Boolean,
1000     onClick: () -> Unit,
1001     modifier: Modifier = Modifier,
1002     secondaryText: String? = null,
1003     enabled: Boolean
1004 ) {
1005     Row(
1006         modifier
1007             .fillMaxWidth()
1008             .selectable(
1009                 selected = selected,
1010                 role = Role.RadioButton,
1011                 onClick = onClick,
1012                 enabled = enabled
1013             ),
1014         verticalAlignment = Alignment.CenterVertically
1015     ) {
1016         SettingUI(
1017             title = text,
1018             description = secondaryText,
1019             enabled = enabled,
1020             leadingIcon = {
1021                 RadioButton(
1022                     selected = selected,
1023                     onClick = onClick,
1024                     enabled = enabled
1025                 )
1026             },
1027             trailingContent = null
1028         )
1029     }
1030 }
1031 
1032 @Composable
1033 @ReadOnlyComposable
disabledRationaleStringnull1034 fun disabledRationaleString(disabledRationale: DisabledRationale): String =
1035     when (disabledRationale) {
1036         is DisabledRationale.DeviceUnsupportedRationale -> stringResource(
1037 
1038             disabledRationale.reasonTextResId,
1039             stringResource(disabledRationale.affectedSettingNameResId)
1040         )
1041 
1042         is DisabledRationale.FpsUnsupportedRationale -> stringResource(
1043             disabledRationale.reasonTextResId,
1044             stringResource(disabledRationale.affectedSettingNameResId),
1045             disabledRationale.currentFps
1046         )
1047 
1048         is DisabledRationale.LensUnsupportedRationale -> stringResource(
1049             disabledRationale.reasonTextResId,
1050             stringResource(disabledRationale.affectedSettingNameResId)
1051         )
1052 
1053         is DisabledRationale.StabilizationUnsupportedRationale -> stringResource(
1054             disabledRationale.reasonTextResId,
1055             stringResource(disabledRationale.affectedSettingNameResId)
1056         )
1057 
1058         is DisabledRationale.VideoQualityUnsupportedRationale -> stringResource(
1059             disabledRationale.reasonTextResId,
1060             stringResource(disabledRationale.affectedSettingNameResId)
1061         )
1062 
1063         is DisabledRationale.PermissionRecordAudioNotGrantedRationale -> stringResource(
1064             disabledRationale.reasonTextResId,
1065             stringResource(disabledRationale.affectedSettingNameResId)
1066         )
1067     }
1068 
1069 @Preview(name = "Light Mode")
1070 @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
1071 @Composable
Preview_VersionInfonull1072 private fun Preview_VersionInfo() {
1073     SettingsPreviewTheme {
1074         VersionInfo(versionName = "0.1.0", buildType = "debug")
1075     }
1076 }
1077 
1078 @Preview(name = "Light Mode")
1079 @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
1080 @Composable
Preview_Popupnull1081 private fun Preview_Popup() {
1082     SettingsPreviewTheme {
1083         BasicPopupSetting(
1084             title = "Test Popup",
1085             description = "Test Description",
1086             leadingIcon = null,
1087             popupContents = {
1088                 Column(Modifier.selectableGroup()) {
1089                     Text(
1090                         text = "Test sub-text",
1091                         fontStyle = FontStyle.Italic,
1092                         color = MaterialTheme.colorScheme.onPrimaryContainer
1093                     )
1094                     SingleChoiceSelector(
1095                         text = "Option 1",
1096                         selected = true,
1097                         enabled = true,
1098                         onClick = { }
1099                     )
1100                     SingleChoiceSelector(
1101                         text = "Option 2",
1102                         selected = false,
1103                         enabled = true,
1104                         onClick = { }
1105                     )
1106                 }
1107             },
1108             enabled = true,
1109             popupStatus = remember { mutableStateOf(true) }
1110         )
1111     }
1112 }
1113