1 /*
<lambda>null2  * Copyright (C) 2024 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.android.providers.media.tools.photopickerv2.utils
17 
18 import android.content.ContentResolver
19 import android.database.Cursor
20 import android.net.Uri
21 import android.provider.MediaStore
22 import android.widget.Toast
23 import androidx.compose.foundation.background
24 import androidx.compose.foundation.border
25 import androidx.compose.foundation.clickable
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.Column
29 import androidx.compose.foundation.layout.Row
30 import androidx.compose.foundation.layout.fillMaxWidth
31 import androidx.compose.foundation.layout.heightIn
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.rememberScrollState
34 import androidx.compose.foundation.text.KeyboardOptions
35 import androidx.compose.foundation.verticalScroll
36 import androidx.compose.material3.Button
37 import androidx.compose.material3.ButtonColors
38 import androidx.compose.material3.ButtonDefaults
39 import androidx.compose.material3.HorizontalDivider
40 import androidx.compose.material3.Icon
41 import androidx.compose.material3.MaterialTheme
42 import androidx.compose.material3.NavigationBar
43 import androidx.compose.material3.NavigationBarItem
44 import androidx.compose.material3.NavigationBarItemDefaults
45 import androidx.compose.material3.OutlinedTextField
46 import androidx.compose.material3.Switch
47 import androidx.compose.material3.Text
48 import androidx.compose.runtime.Composable
49 import androidx.compose.runtime.getValue
50 import androidx.compose.runtime.mutableStateOf
51 import androidx.compose.runtime.saveable.rememberSaveable
52 import androidx.compose.runtime.setValue
53 import androidx.compose.ui.Alignment
54 import androidx.compose.ui.Modifier
55 import androidx.compose.ui.graphics.Color
56 import androidx.compose.ui.platform.LocalContext
57 import androidx.compose.ui.res.stringResource
58 import androidx.compose.ui.text.font.FontWeight
59 import androidx.compose.ui.unit.dp
60 import androidx.compose.ui.unit.sp
61 import androidx.compose.ui.window.Popup
62 import androidx.compose.ui.window.PopupProperties
63 import androidx.navigation.NavController
64 import com.android.providers.media.tools.photopickerv2.R
65 import com.android.providers.media.tools.photopickerv2.navigation.NavigationItem
66 import java.text.SimpleDateFormat
67 import java.util.Date
68 import java.util.Locale
69 
70 /**
71  * PhotoPickerTitle is a composable function that displays the title of the PhotoPicker app.
72  *
73  * @param label the label to be displayed as the title of the PhotoPicker app.
74  */
75 @Composable
76 fun PhotoPickerTitle(label: String = stringResource(id = R.string.title_photopicker)) {
77     Text(
78         text = label,
79         fontWeight = FontWeight.Bold,
80         fontSize = 20.sp,
81         modifier = Modifier.padding(bottom = 16.dp)
82     )
83 }
84 
85 /**
86  * SwitchComponent is a composable function that displays a switch component.
87  *
88  * @param label the label to be displayed next to the switch component.
89  * @param checked the state of the switch component.
90  * @param onCheckedChange the callback function to be called when the switch component is changed.
91  */
92 @Composable
SwitchComponentnull93 fun SwitchComponent(
94     label: String,
95     checked: Boolean,
96     onCheckedChange: (Boolean) -> Unit
97 ) {
98     Row(
99         modifier = Modifier
100             .fillMaxWidth()
101             .padding(vertical = 5.dp),
102         verticalAlignment = Alignment.CenterVertically
103     ) {
104         Text(
105             text = label,
106             modifier = Modifier.weight(1f),
107             color = Color.Black,
108             fontWeight = FontWeight.Medium,
109             fontSize = 16.sp,
110         )
111         Switch(
112             checked = checked,
113             onCheckedChange = onCheckedChange
114         )
115     }
116 }
117 
118 /**
119  * TextFieldComponent is a composable function that displays a text field component.
120  *
121  * @param value the value of the text field component.
122  * @param onValueChange the callback function to be called when the text field component is changed.
123  * @param label the label to be displayed next to the text field component.
124  * @param keyboardOptions the keyboard options to be used for the text field component.
125  * @param modifier the modifier to be applied to the text field component.
126  */
127 @Composable
TextFieldComponentnull128 fun TextFieldComponent(
129     value: String,
130     onValueChange: (String) -> Unit,
131     label: String,
132     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
133     modifier: Modifier = Modifier
134 ) {
135     OutlinedTextField(
136         value = value,
137         onValueChange = onValueChange,
138         label = { Text(label) },
139         keyboardOptions = keyboardOptions,
140         modifier = modifier
141             .fillMaxWidth()
142             .background(Color.Transparent)
143     )
144 }
145 
146 /**
147  * ErrorMessage is a composable function that displays an error message.
148  *
149  * @param text the text to be displayed as the error message.
150  */
151 @Composable
ErrorMessagenull152 fun ErrorMessage(
153     text: String
154 ) {
155     val context = LocalContext.current
156 
157     if (text.isNotEmpty()) {
158         Toast.makeText(context, text, Toast.LENGTH_LONG).show()
159     }
160 }
161 
162 /**
163  * ButtonComponent is a composable function that displays a button component.
164  *
165  * @param label the label to be displayed on the button component.
166  * @param onClick the callback function to be called when the button component is clicked.
167  * @param modifier the modifier to be applied to the button component.
168  * @param colors the color of the button.
169  * @param enabled the enabled state of the button component.
170  */
171 @Composable
ButtonComponentnull172 fun ButtonComponent(
173     label: String,
174     onClick: () -> Unit,
175     modifier: Modifier = Modifier,
176     colors: ButtonColors = ButtonDefaults.buttonColors(),
177     enabled: Boolean = true
178 ) {
179     Button(
180         onClick = onClick,
181         colors = colors,
182         modifier = modifier.fillMaxWidth(),
183         enabled = enabled
184     ) {
185         Text(label)
186     }
187 }
188 
189 /**
190  * NavigationComponent is a composable function that displays a navigation component.
191  *
192  * @param navController the navigation controller to be used for the navigation component.
193  * @param items the list of items to be displayed in the navigation component.
194  * @param currentRoute the current route of the navigation component.
195  */
196 @Composable
NavigationComponentnull197 fun NavigationComponent(
198     navController: NavController,
199     items: List<NavigationItem>,
200     currentRoute: String?
201 ) {
202     NavigationBar {
203         items.forEach { item ->
204             NavigationBarItem(
205                 icon = { Icon(item.icon, contentDescription = null) },
206                 label = { Text(stringResource(item.label)) },
207                 selected = currentRoute == item.route,
208                 onClick = {
209                     navController.navigate(item.route) {
210                         popUpTo(navController.graph.startDestinationId) {
211                             saveState = true
212                         }
213                         launchSingleTop = true
214                         restoreState = true
215                     }
216                 },
217                 colors = NavigationBarItemDefaults.colors(
218                     selectedIconColor = MaterialTheme.colorScheme.primary,
219                     unselectedIconColor = MaterialTheme.colorScheme.onSurface
220                 )
221             )
222         }
223     }
224 }
225 
226 /**
227  * DropdownList is a composable function that creates a dropdown list component.
228  *
229  * @param label The label to be displayed above the dropdown list.
230  * @param options A list of options to be displayed in the dropdown list.
231  * @param selectedOption The currently selected option.
232  * @param onOptionSelected A callback function that gets called when an option is selected.
233  * @param enabled A boolean flag to enable or disable the dropdown list.
234  */
235 @Composable
DropdownListnull236 fun DropdownList(
237     label: String,
238     options: List<String>,
239     selectedOption: String,
240     onOptionSelected: (String) -> Unit,
241     enabled: Boolean
242 ) {
243     var isExpanded by rememberSaveable { mutableStateOf(false) }
244     val scrollState = rememberScrollState()
245 
246     Column {
247         Text(
248             text = label,
249             fontWeight = FontWeight.Bold,
250             modifier = Modifier.padding(bottom = 8.dp),
251             color = if (enabled) Color.Black else Color.Gray
252         )
253 
254         Box(
255             modifier = Modifier
256                 .fillMaxWidth()
257                 .background(if (enabled) Color.Transparent else Color.Gray)
258                 .clickable { if (enabled) isExpanded = true },
259             contentAlignment = Alignment.Center
260         ) {
261             Text(
262                 text = selectedOption,
263                 color = if (enabled) Color.Black else Color.Gray,
264                 modifier = Modifier.padding(8.dp)
265             )
266         }
267 
268         if (isExpanded) {
269             Popup(
270                 alignment = Alignment.TopCenter,
271                 properties = PopupProperties(
272                     excludeFromSystemGesture = true,
273                 ),
274                 onDismissRequest = { isExpanded = false }
275             ) {
276                 Column(
277                     modifier = Modifier
278                         .fillMaxWidth()
279                         .heightIn(max = 200.dp)
280                         .verticalScroll(scrollState)
281                         .border(1.dp, Color.Gray)
282                         .background(Color.White),
283                     horizontalAlignment = Alignment.CenterHorizontally,
284                 ) {
285                     options.forEachIndexed { index, option ->
286                         if (index != 0) {
287                             HorizontalDivider(thickness = 1.dp, color = Color.LightGray)
288                         }
289                         Box(
290                             modifier = Modifier
291                                 .fillMaxWidth()
292                                 .clickable {
293                                     if (enabled) {
294                                         onOptionSelected(option)
295                                         isExpanded = false
296                                     }
297                                 }
298                                 .background(if (enabled) Color.Transparent else Color.LightGray),
299                             contentAlignment = Alignment.Center
300                         ) {
301                             Text(
302                                 text = option,
303                                 color = if (enabled) Color.Black else Color.Gray,
304                                 modifier = Modifier.padding(8.dp)
305                             )
306                         }
307                     }
308                 }
309             }
310         }
311     }
312 }
313 
314 @Composable
MetaDataDetailsnull315 fun MetaDataDetails(
316     uri: Uri,
317     contentResolver: ContentResolver,
318     showMetaData: Boolean,
319     inDocsUITab: Boolean
320 ) {
321     Row(
322         modifier = Modifier
323             .fillMaxWidth()
324             .padding(8.dp),
325         horizontalArrangement = Arrangement.SpaceBetween
326     ) {
327         if (showMetaData) {
328             val cursor: Cursor? = contentResolver.query(
329                 uri, null, null, null, null
330             )
331             cursor?.use {
332                 // Metadata Details for PhotoPicker Tab and PickerChoice Tab
333                 if (!inDocsUITab){
334                     if (it.moveToNext()) {
335                         val mediaUri = it.getString(it.getColumnIndexOrThrow(
336                             MediaStore.Images.Media.DATA))
337                         val displayName = it.getString(it.getColumnIndexOrThrow(
338                             MediaStore.Images.Media.DISPLAY_NAME))
339                         val size = it.getLong(it.getColumnIndexOrThrow(
340                             MediaStore.Images.Media.SIZE))
341                         val sizeInKB = size / 1000
342                         val dateTaken = it.getLong(it.getColumnIndexOrThrow(
343                             MediaStore.Images.Media.DATE_TAKEN))
344 
345                         val duration =
346                             it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media.DURATION))
347                         val durationInSec = duration / 1000
348                         val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
349                         val dateString = formatter.format(Date(dateTaken))
350 
351                         Column {
352                             Text(
353                                 text = "Meta Data Details:",
354                                 fontWeight = FontWeight.Medium,
355                                 fontSize = 16.sp,
356                             )
357                             Text(text = "URI: $mediaUri")
358                             Text(text = "Display Name: $displayName")
359                             Text(text = "Size: $sizeInKB KB")
360                             Text(text = "Date Taken: $dateString")
361                             Text(text = "Duration: $durationInSec s")
362                         }
363                     }
364                 } else {
365                     // Metadata Details for DocsUI Tab
366                     if (it.moveToNext()){
367                         val documentID = it.getLong(it.getColumnIndexOrThrow(
368                             MediaStore.Images.Media.DOCUMENT_ID))
369                         val mimeType = it.getString(it.getColumnIndexOrThrow(
370                             MediaStore.Images.Media.MIME_TYPE))
371                         val displayName =
372                             it.getString(it.getColumnIndexOrThrow(
373                                 MediaStore.Images.Media.DISPLAY_NAME))
374                         val size = it.getLong(it.getColumnIndexOrThrow(
375                             MediaStore.Images.Media.SIZE))
376                         val sizeInKB = size / 1000
377                         Column {
378                             Text(
379                                 text = "Meta Data Details:",
380                                 fontWeight = FontWeight.Medium,
381                                 fontSize = 16.sp,
382                             )
383 
384                             Text(text = "Document ID: $documentID")
385                             Text(text = "Display Name: $displayName")
386                             Text(text = "Size: $sizeInKB KB")
387                             Text(text = "Mime Type: $mimeType")
388                         }
389                     }
390                 }
391             }
392         }
393     }
394 }
395 
396 enum class LaunchLocation {
397     PHOTOS_TAB,
398     ALBUMS_TAB;
399 
400     companion object {
getListOfAvailableLocationsnull401         fun getListOfAvailableLocations(): List<String> {
402             return entries.map { it -> it.name }
403         }
404     }
405 }
406