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.quicksettings.ui
17 
18 import androidx.compose.foundation.clickable
19 import androidx.compose.foundation.layout.Arrangement
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.layout.size
25 import androidx.compose.foundation.layout.wrapContentSize
26 import androidx.compose.foundation.lazy.grid.GridCells
27 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
28 import androidx.compose.material.icons.Icons
29 import androidx.compose.material.icons.filled.ExpandMore
30 import androidx.compose.material3.Icon
31 import androidx.compose.material3.LocalContentColor
32 import androidx.compose.material3.Text
33 import androidx.compose.runtime.Composable
34 import androidx.compose.runtime.CompositionLocalProvider
35 import androidx.compose.ui.Alignment
36 import androidx.compose.ui.Modifier
37 import androidx.compose.ui.draw.scale
38 import androidx.compose.ui.graphics.Color
39 import androidx.compose.ui.graphics.painter.Painter
40 import androidx.compose.ui.platform.LocalConfiguration
41 import androidx.compose.ui.platform.testTag
42 import androidx.compose.ui.res.dimensionResource
43 import androidx.compose.ui.res.stringResource
44 import androidx.compose.ui.semantics.contentDescription
45 import androidx.compose.ui.semantics.semantics
46 import androidx.compose.ui.text.style.TextAlign
47 import androidx.compose.ui.unit.dp
48 import com.google.jetpackcamera.feature.preview.PreviewMode
49 import com.google.jetpackcamera.feature.preview.R
50 import com.google.jetpackcamera.feature.preview.quicksettings.CameraAspectRatio
51 import com.google.jetpackcamera.feature.preview.quicksettings.CameraCaptureMode
52 import com.google.jetpackcamera.feature.preview.quicksettings.CameraConcurrentCameraMode
53 import com.google.jetpackcamera.feature.preview.quicksettings.CameraDynamicRange
54 import com.google.jetpackcamera.feature.preview.quicksettings.CameraFlashMode
55 import com.google.jetpackcamera.feature.preview.quicksettings.CameraLensFace
56 import com.google.jetpackcamera.feature.preview.quicksettings.CameraLowLightBoost
57 import com.google.jetpackcamera.feature.preview.quicksettings.QuickSettingsEnum
58 import com.google.jetpackcamera.settings.model.AspectRatio
59 import com.google.jetpackcamera.settings.model.CaptureMode
60 import com.google.jetpackcamera.settings.model.ConcurrentCameraMode
61 import com.google.jetpackcamera.settings.model.DynamicRange
62 import com.google.jetpackcamera.settings.model.FlashMode
63 import com.google.jetpackcamera.settings.model.ImageOutputFormat
64 import com.google.jetpackcamera.settings.model.LensFacing
65 import com.google.jetpackcamera.settings.model.LowLightBoost
66 import kotlin.math.min
67 
68 // completed components ready to go into preview screen
69 
70 @Composable
71 fun ExpandedQuickSetRatio(
72     setRatio: (aspectRatio: AspectRatio) -> Unit,
73     currentRatio: AspectRatio,
74     modifier: Modifier = Modifier
75 ) {
76     val buttons: Array<@Composable () -> Unit> =
77         arrayOf(
78             {
79                 QuickSetRatio(
80                     modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_3_4_BUTTON),
81                     onClick = { setRatio(AspectRatio.THREE_FOUR) },
82                     ratio = AspectRatio.THREE_FOUR,
83                     currentRatio = currentRatio,
84                     isHighlightEnabled = true
85                 )
86             },
87             {
88                 QuickSetRatio(
89                     modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_9_16_BUTTON),
90                     onClick = { setRatio(AspectRatio.NINE_SIXTEEN) },
91                     ratio = AspectRatio.NINE_SIXTEEN,
92                     currentRatio = currentRatio,
93                     isHighlightEnabled = true
94                 )
95             },
96             {
97                 QuickSetRatio(
98                     modifier = Modifier.testTag(QUICK_SETTINGS_RATIO_1_1_BUTTON),
99                     onClick = { setRatio(AspectRatio.ONE_ONE) },
100                     ratio = AspectRatio.ONE_ONE,
101                     currentRatio = currentRatio,
102                     isHighlightEnabled = true
103                 )
104             }
105         )
106     ExpandedQuickSetting(modifier = modifier, quickSettingButtons = buttons)
107 }
108 
109 @Composable
QuickSetHdrnull110 fun QuickSetHdr(
111     modifier: Modifier = Modifier,
112     onClick: (dynamicRange: DynamicRange, imageOutputFormat: ImageOutputFormat) -> Unit,
113     selectedDynamicRange: DynamicRange,
114     selectedImageOutputFormat: ImageOutputFormat,
115     hdrDynamicRange: DynamicRange,
116     hdrImageFormat: ImageOutputFormat,
117     hdrDynamicRangeSupported: Boolean,
118     previewMode: PreviewMode,
119     enabled: Boolean
120 ) {
121     val enum =
122         if (selectedDynamicRange == hdrDynamicRange ||
123             selectedImageOutputFormat == hdrImageFormat
124         ) {
125             CameraDynamicRange.HDR
126         } else {
127             CameraDynamicRange.SDR
128         }
129 
130     QuickSettingUiItem(
131         modifier = modifier,
132         enum = enum,
133         onClick = {
134             val newDynamicRange =
135                 if (selectedDynamicRange == DynamicRange.SDR && hdrDynamicRangeSupported) {
136                     hdrDynamicRange
137                 } else {
138                     DynamicRange.SDR
139                 }
140             val newImageOutputFormat =
141                 if (!hdrDynamicRangeSupported ||
142                     previewMode is PreviewMode.ExternalImageCaptureMode
143                 ) {
144                     hdrImageFormat
145                 } else {
146                     ImageOutputFormat.JPEG
147                 }
148             onClick(newDynamicRange, newImageOutputFormat)
149         },
150         isHighLighted = (selectedDynamicRange != DynamicRange.SDR),
151         enabled = enabled
152     )
153 }
154 
155 @Composable
QuickSetLowLightBoostnull156 fun QuickSetLowLightBoost(
157     modifier: Modifier = Modifier,
158     onClick: (lowLightBoost: LowLightBoost) -> Unit,
159     selectedLowLightBoost: LowLightBoost
160 ) {
161     val enum = when (selectedLowLightBoost) {
162         LowLightBoost.DISABLED -> CameraLowLightBoost.DISABLED
163         LowLightBoost.ENABLED -> CameraLowLightBoost.ENABLED
164     }
165 
166     QuickSettingUiItem(
167         modifier = modifier,
168         enum = enum,
169         onClick = {
170             when (selectedLowLightBoost) {
171                 LowLightBoost.DISABLED -> onClick(LowLightBoost.ENABLED)
172                 LowLightBoost.ENABLED -> onClick(LowLightBoost.DISABLED)
173             }
174         },
175         isHighLighted = false
176     )
177 }
178 
179 @Composable
QuickSetRationull180 fun QuickSetRatio(
181     onClick: () -> Unit,
182     ratio: AspectRatio,
183     currentRatio: AspectRatio,
184     modifier: Modifier = Modifier,
185     isHighlightEnabled: Boolean = false
186 ) {
187     val enum =
188         when (ratio) {
189             AspectRatio.THREE_FOUR -> CameraAspectRatio.THREE_FOUR
190             AspectRatio.NINE_SIXTEEN -> CameraAspectRatio.NINE_SIXTEEN
191             AspectRatio.ONE_ONE -> CameraAspectRatio.ONE_ONE
192             else -> CameraAspectRatio.ONE_ONE
193         }
194     QuickSettingUiItem(
195         modifier = modifier,
196         enum = enum,
197         onClick = { onClick() },
198         isHighLighted = isHighlightEnabled && (ratio == currentRatio)
199     )
200 }
201 
202 @Composable
QuickSetFlashnull203 fun QuickSetFlash(
204     onClick: (FlashMode) -> Unit,
205     currentFlashMode: FlashMode,
206     modifier: Modifier = Modifier
207 ) {
208     val enum = when (currentFlashMode) {
209         FlashMode.OFF -> CameraFlashMode.OFF
210         FlashMode.AUTO -> CameraFlashMode.AUTO
211         FlashMode.ON -> CameraFlashMode.ON
212     }
213     QuickSettingUiItem(
214         modifier = modifier
215             .semantics {
216                 contentDescription =
217                     when (enum) {
218                         CameraFlashMode.OFF -> "QUICK SETTINGS FLASH IS OFF"
219                         CameraFlashMode.AUTO -> "QUICK SETTINGS FLASH IS AUTO"
220                         CameraFlashMode.ON -> "QUICK SETTINGS FLASH IS ON"
221                     }
222             },
223         enum = enum,
224         isHighLighted = currentFlashMode == FlashMode.ON,
225         onClick =
226         {
227             onClick(currentFlashMode.getNextFlashMode())
228         }
229     )
230 }
231 
232 @Composable
QuickFlipCameranull233 fun QuickFlipCamera(
234     setLensFacing: (LensFacing) -> Unit,
235     currentLensFacing: LensFacing,
236     modifier: Modifier = Modifier
237 ) {
238     val enum =
239         when (currentLensFacing) {
240             LensFacing.FRONT -> CameraLensFace.FRONT
241             LensFacing.BACK -> CameraLensFace.BACK
242         }
243     QuickSettingUiItem(
244         modifier = modifier,
245         enum = enum,
246         onClick = { setLensFacing(currentLensFacing.flip()) }
247     )
248 }
249 
250 @Composable
QuickSetCaptureModenull251 fun QuickSetCaptureMode(
252     setCaptureMode: (CaptureMode) -> Unit,
253     currentCaptureMode: CaptureMode,
254     modifier: Modifier = Modifier,
255     enabled: Boolean = true
256 ) {
257     val enum: CameraCaptureMode =
258         when (currentCaptureMode) {
259             CaptureMode.MULTI_STREAM -> CameraCaptureMode.MULTI_STREAM
260             CaptureMode.SINGLE_STREAM -> CameraCaptureMode.SINGLE_STREAM
261         }
262     QuickSettingUiItem(
263         modifier = modifier,
264         enum = enum,
265         onClick = {
266             when (currentCaptureMode) {
267                 CaptureMode.MULTI_STREAM -> setCaptureMode(CaptureMode.SINGLE_STREAM)
268                 CaptureMode.SINGLE_STREAM -> setCaptureMode(CaptureMode.MULTI_STREAM)
269             }
270         },
271         enabled = enabled
272     )
273 }
274 
275 @Composable
QuickSetConcurrentCameranull276 fun QuickSetConcurrentCamera(
277     setConcurrentCameraMode: (ConcurrentCameraMode) -> Unit,
278     currentConcurrentCameraMode: ConcurrentCameraMode,
279     modifier: Modifier = Modifier,
280     enabled: Boolean = true
281 ) {
282     val enum: CameraConcurrentCameraMode =
283         when (currentConcurrentCameraMode) {
284             ConcurrentCameraMode.OFF -> CameraConcurrentCameraMode.OFF
285             ConcurrentCameraMode.DUAL -> CameraConcurrentCameraMode.DUAL
286         }
287     QuickSettingUiItem(
288         modifier = modifier,
289         enum = enum,
290         onClick = {
291             when (currentConcurrentCameraMode) {
292                 ConcurrentCameraMode.OFF -> setConcurrentCameraMode(ConcurrentCameraMode.DUAL)
293                 ConcurrentCameraMode.DUAL -> setConcurrentCameraMode(ConcurrentCameraMode.OFF)
294             }
295         },
296         enabled = enabled
297     )
298 }
299 
300 /**
301  * Button to toggle quick settings
302  */
303 @Composable
ToggleQuickSettingsButtonnull304 fun ToggleQuickSettingsButton(
305     toggleDropDown: () -> Unit,
306     isOpen: Boolean,
307     modifier: Modifier = Modifier
308 ) {
309     Row(
310         horizontalArrangement = Arrangement.Center,
311         verticalAlignment = Alignment.CenterVertically,
312         modifier = modifier
313     ) {
314         // dropdown icon
315         Icon(
316             imageVector = Icons.Filled.ExpandMore,
317             contentDescription = if (isOpen) {
318                 stringResource(R.string.quick_settings_dropdown_open_description)
319             } else {
320                 stringResource(R.string.quick_settings_dropdown_closed_description)
321             },
322             modifier = Modifier
323                 .testTag(QUICK_SETTINGS_DROP_DOWN)
324                 .size(72.dp)
325                 .clickable {
326                     toggleDropDown()
327                 }
328                 .scale(1f, if (isOpen) -1f else 1f)
329         )
330     }
331 }
332 
333 // subcomponents used to build completed components
334 
335 @Composable
QuickSettingUiItemnull336 fun QuickSettingUiItem(
337     enum: QuickSettingsEnum,
338     onClick: () -> Unit,
339     modifier: Modifier = Modifier,
340     isHighLighted: Boolean = false,
341     enabled: Boolean = true
342 ) {
343     QuickSettingUiItem(
344         modifier = modifier,
345         painter = enum.getPainter(),
346         text = stringResource(id = enum.getTextResId()),
347         accessibilityText = stringResource(id = enum.getDescriptionResId()),
348         onClick = { onClick() },
349         isHighLighted = isHighLighted,
350         enabled = enabled
351     )
352 }
353 
354 /**
355  * The itemized UI component representing each button in quick settings.
356  */
357 @Composable
QuickSettingUiItemnull358 fun QuickSettingUiItem(
359     text: String,
360     painter: Painter,
361     accessibilityText: String,
362     onClick: () -> Unit,
363     modifier: Modifier = Modifier,
364     isHighLighted: Boolean = false,
365     enabled: Boolean = true
366 ) {
367     Column(
368         modifier =
369         modifier
370             .wrapContentSize()
371             .padding(dimensionResource(id = R.dimen.quick_settings_ui_item_padding))
372             .clickable(onClick = onClick, enabled = enabled),
373         verticalArrangement = Arrangement.Center,
374         horizontalAlignment = Alignment.CenterHorizontally
375     ) {
376         val contentColor = (if (isHighLighted) Color.Yellow else Color.White).let {
377             // When in disabled state, material3 guidelines say the element's opacity should be 38%
378             // See: https://m3.material.io/foundations/interaction/states/applying-states#3c3032e8-b07a-42ac-a508-a32f573cc7e1
379             // and: https://developer.android.com/develop/ui/compose/designsystems/material2-material3#emphasis-and
380             if (!enabled) it.copy(alpha = 0.38f) else it
381         }
382         CompositionLocalProvider(LocalContentColor provides contentColor) {
383             Icon(
384                 painter = painter,
385                 contentDescription = accessibilityText,
386                 modifier = Modifier.size(
387                     dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size)
388                 )
389             )
390 
391             Text(text = text, textAlign = TextAlign.Center)
392         }
393     }
394 }
395 
396 /**
397  * Should you want to have an expanded view of a single quick setting
398  */
399 @Composable
ExpandedQuickSettingnull400 fun ExpandedQuickSetting(
401     modifier: Modifier = Modifier,
402     vararg quickSettingButtons: @Composable () -> Unit
403 ) {
404     val expandedNumOfColumns =
405         min(
406             quickSettingButtons.size,
407             (
408                 (
409                     LocalConfiguration.current.screenWidthDp.dp - (
410                         dimensionResource(
411                             id = R.dimen.quick_settings_ui_horizontal_padding
412                         ) * 2
413                         )
414                     ) /
415                     (
416                         dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
417                             (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
418                         )
419                 ).toInt()
420         )
421     LazyVerticalGrid(
422         modifier = modifier.fillMaxWidth(),
423         columns = GridCells.Fixed(count = expandedNumOfColumns)
424     ) {
425         items(quickSettingButtons.size) { i ->
426             quickSettingButtons[i]()
427         }
428     }
429 }
430 
431 /**
432  * Algorithm to determine dimensions of QuickSettings Icon layout
433  */
434 @Composable
QuickSettingsGridnull435 fun QuickSettingsGrid(
436     modifier: Modifier = Modifier,
437     quickSettingsButtons: List<@Composable () -> Unit>
438 ) {
439     val initialNumOfColumns =
440         min(
441             quickSettingsButtons.size,
442             (
443                 (
444                     LocalConfiguration.current.screenWidthDp.dp - (
445                         dimensionResource(
446                             id = R.dimen.quick_settings_ui_horizontal_padding
447                         ) * 2
448                         )
449                     ) /
450                     (
451                         dimensionResource(id = R.dimen.quick_settings_ui_item_icon_size) +
452                             (dimensionResource(id = R.dimen.quick_settings_ui_item_padding) * 2)
453                         )
454                 ).toInt()
455         )
456 
457     LazyVerticalGrid(
458         modifier = modifier.fillMaxWidth(),
459         columns = GridCells.Fixed(count = initialNumOfColumns)
460     ) {
461         items(quickSettingsButtons.size) { i ->
462             quickSettingsButtons[i]()
463         }
464     }
465 }
466 
467 /**
468  * The top bar indicators for quick settings items.
469  */
470 @Composable
Indicatornull471 fun Indicator(enum: QuickSettingsEnum, onClick: () -> Unit, modifier: Modifier = Modifier) {
472     Icon(
473         painter = enum.getPainter(),
474         contentDescription = stringResource(id = enum.getDescriptionResId()),
475         modifier = modifier
476             .size(dimensionResource(id = R.dimen.quick_settings_indicator_size))
477             .clickable { onClick() }
478     )
479 }
480 
481 @Composable
FlashModeIndicatornull482 fun FlashModeIndicator(currentFlashMode: FlashMode, onClick: (flashMode: FlashMode) -> Unit) {
483     val enum = when (currentFlashMode) {
484         FlashMode.OFF -> CameraFlashMode.OFF
485         FlashMode.AUTO -> CameraFlashMode.AUTO
486         FlashMode.ON -> CameraFlashMode.ON
487     }
488     Indicator(
489         enum = enum,
490         onClick = {
491             onClick(currentFlashMode.getNextFlashMode())
492         }
493     )
494 }
495 
496 @Composable
QuickSettingsIndicatorsnull497 fun QuickSettingsIndicators(
498     currentFlashMode: FlashMode,
499     onFlashModeClick: (flashMode: FlashMode) -> Unit,
500     modifier: Modifier = Modifier
501 ) {
502     Row(modifier) {
503         FlashModeIndicator(currentFlashMode, onFlashModeClick)
504     }
505 }
506 
getNextFlashModenull507 fun FlashMode.getNextFlashMode(): FlashMode {
508     return when (this) {
509         FlashMode.OFF -> FlashMode.ON
510         FlashMode.ON -> FlashMode.AUTO
511         FlashMode.AUTO -> FlashMode.OFF
512     }
513 }
514