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