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