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.photopicker
17 
18 import android.annotation.SuppressLint
19 import android.app.Activity
20 import android.net.Uri
21 import android.widget.Toast
22 import android.widget.VideoView
23 import androidx.compose.ui.res.stringResource
24 import androidx.activity.compose.rememberLauncherForActivityResult
25 import androidx.activity.result.contract.ActivityResultContracts
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Column
28 import androidx.compose.foundation.layout.Row
29 import androidx.compose.foundation.layout.Spacer
30 import androidx.compose.foundation.layout.fillMaxWidth
31 import androidx.compose.foundation.layout.height
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.layout.width
34 import androidx.compose.foundation.rememberScrollState
35 import androidx.compose.foundation.text.KeyboardOptions
36 import androidx.compose.foundation.verticalScroll
37 import androidx.compose.material3.ButtonDefaults
38 import androidx.compose.material3.HorizontalDivider
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.collectAsState
41 import androidx.compose.runtime.getValue
42 import androidx.compose.runtime.mutableIntStateOf
43 import androidx.compose.runtime.mutableStateOf
44 import androidx.compose.runtime.remember
45 import androidx.compose.runtime.setValue
46 import androidx.compose.ui.Alignment
47 import androidx.compose.ui.Modifier
48 import androidx.compose.ui.graphics.Color
49 import androidx.compose.ui.platform.LocalContext
50 import androidx.compose.ui.text.input.KeyboardType
51 import androidx.compose.ui.unit.dp
52 import androidx.compose.ui.viewinterop.AndroidView
53 import androidx.lifecycle.viewmodel.compose.viewModel
54 import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
55 import com.bumptech.glide.integration.compose.GlideImage
56 import com.android.providers.media.tools.photopickerv2.utils.isImage
57 import com.android.providers.media.tools.photopickerv2.R
58 import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent
59 import com.android.providers.media.tools.photopickerv2.utils.DropdownList
60 import com.android.providers.media.tools.photopickerv2.utils.ErrorMessage
61 import com.android.providers.media.tools.photopickerv2.utils.LaunchLocation
62 import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails
63 import com.android.providers.media.tools.photopickerv2.utils.PhotoPickerTitle
64 import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent
65 import com.android.providers.media.tools.photopickerv2.utils.TextFieldComponent
66 import com.android.providers.media.tools.photopickerv2.utils.resetMedia
67 
68 /**
69  * This is the screen for the PhotoPicker tab.
70  */
71 @SuppressLint("NewApi")
72 @OptIn(ExperimentalGlideComposeApi::class)
73 @Composable
74 fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) {
75     val context = LocalContext.current
76 
77     // initializing intent extras
78     var isOrderSelectionEnabled by remember { mutableStateOf(false) }
79     var allowMultiple by remember { mutableStateOf(false) }
80     var isActionGetContentSelected by remember { mutableStateOf(false) }
81     var selectedLaunchTab by remember { mutableStateOf(LaunchLocation.PHOTOS_TAB.name) }
82     var accentColor by remember { mutableStateOf("#FF6200EE") } // default
83 
84 
85     var allowCustomMimeType by remember { mutableStateOf(false) }
86     var selectedMimeType by remember { mutableStateOf("") }
87     var customMimeTypeInput by remember { mutableStateOf("") }
88 
89     var showImagesOnly by remember { mutableStateOf(false) }
90     var showVideosOnly by remember { mutableStateOf(false) }
91 
92     // We can only take string as an input, not an int using OutlinedTextField
93     var maxSelectionInput by remember { mutableStateOf("10") }
94     var maxMediaItemsDisplayed by remember { mutableIntStateOf(10) } // default items
95 
96     var selectionErrorMessage by remember { mutableStateOf("") }
97     var maxSelectionLimitError by remember { mutableStateOf("") }
98 
99     // The Pick Images intent is selected by default
100     var selectedButton by remember { mutableStateOf<Int?>(R.string.pick_images) }
101 
102     // Meta Data Details
103     var showMetaData by remember { mutableStateOf(false) }
104 
105     var isPreSelectionEnabled by remember { mutableStateOf(false) }
106 
107     // Color of PickImages and ACTION_GET_CONTENT button
108     val getContentColor = if (isActionGetContentSelected) {
109         ButtonDefaults.buttonColors()
110     } else ButtonDefaults.buttonColors(Color.Gray)
111 
112     val pickImagesColor = if (!isActionGetContentSelected) {
113         ButtonDefaults.buttonColors()
114     } else ButtonDefaults.buttonColors(Color.Gray)
115 
116     // For handling the result of the photo picking activity
117     val launcher = rememberLauncherForActivityResult(
118         contract = ActivityResultContracts.StartActivityForResult()
119     ) { result ->
120         if (result.resultCode == Activity.RESULT_OK) {
121             // Get the clipData containing multiple selected items.
122             val clipData = result.data?.clipData
123             val uris = mutableListOf<Uri>() // An empty list to store the selected URIs
124 
125             // If multiple items are selected (clipData is not null), iterate through the items.
126             if (clipData != null) {
127                 // Add each selected item to the URIs list,
128                 // up to the maxMediaItemsDisplayed limit if multiple selection is allowed
129                 for (i in 0 until clipData.itemCount) {
130                     uris.add(clipData.getItemAt(i).uri)
131                 }
132             } else {
133                 // If only a single item is selected, add its URI to the list
134                 result.data?.data?.let { uris.add(it) }
135             }
136 
137             // Update the ViewModel with the list of selected URIs
138             photoPickerViewModel.updateSelectedMediaList(uris)
139         }
140     }
141 
142     val resultMedia by photoPickerViewModel.selectedMedia.collectAsState()
143 
144     fun resetFeatureComponents(
145         isGetContentSelected: Boolean,
146         selectedButtonType: Int
147     ) {
148         isActionGetContentSelected = isGetContentSelected
149         selectedButton = selectedButtonType
150         allowMultiple = false
151         showImagesOnly = false
152         showVideosOnly = false
153         showMetaData = false
154         selectedMimeType = ""
155         accentColor = "#FF6200EE"
156         resetMedia(photoPickerViewModel)
157         isOrderSelectionEnabled = false
158         maxSelectionInput = "10" // resetting the max Selection limit to default
159         maxMediaItemsDisplayed = 10
160         allowCustomMimeType = false
161         customMimeTypeInput = ""
162         selectedLaunchTab = LaunchLocation.PHOTOS_TAB.toString()
163         isPreSelectionEnabled = false
164     }
165 
166     Column(
167         modifier = Modifier
168             .padding(16.dp)
169             .verticalScroll(rememberScrollState())
170             .fillMaxWidth()
171     ) {
172         // Title : PhotoPicker V2
173         PhotoPickerTitle()
174 
175         // ACTION_PICK_IMAGES or ACTION_GET_CONTENT
176         Row(
177             modifier = Modifier
178                 .fillMaxWidth()
179                 .padding(vertical = 5.dp),
180             verticalAlignment = Alignment.CenterVertically,
181             horizontalArrangement = Arrangement.spacedBy(8.dp)
182         ) {
183             ButtonComponent(
184                 label = stringResource(id = R.string.pick_images),
185                 onClick = {
186                     resetFeatureComponents(
187                         isGetContentSelected = false,
188                         selectedButtonType = R.string.pick_images
189                     )
190                 },
191                 modifier = Modifier.weight(1f),
192                 colors = pickImagesColor
193             )
194 
195             // ACTION_GET_CONTENT will only support "images/*" and "videos/*"
196             // in the PhotoPicker tab
197             ButtonComponent(
198                 label = stringResource(id = R.string.action_get_content),
199                 onClick = {
200                     resetFeatureComponents(
201                         isGetContentSelected = true,
202                         selectedButtonType = R.string.action_get_content
203                     )
204                 },
205                 modifier = Modifier.weight(1f),
206                 colors = getContentColor
207             )
208         }
209 
210         if (!isActionGetContentSelected) {
211             // Display Order of Selection
212             SwitchComponent(
213                 label = stringResource(id = R.string.display_order_of_selection),
214                 checked = isOrderSelectionEnabled,
215                 onCheckedChange = { isOrderSelectionEnabled = it }
216             )
217         }
218 
219         if (!allowCustomMimeType || isActionGetContentSelected) {
220             // SHOW ONLY IMAGES OR VIDEOS
221             Row(
222                 modifier = Modifier
223                     .fillMaxWidth()
224                     .padding(vertical = 5.dp),
225                 verticalAlignment = Alignment.CenterVertically
226             ) {
227                 Column(modifier = Modifier.weight(1f)) {
228                     SwitchComponent(
229                         label = stringResource(R.string.show_images_only),
230                         checked = showImagesOnly,
231                         onCheckedChange = {
232                             showImagesOnly = it
233                             if (it) {
234                                 showVideosOnly = false
235                                 selectedMimeType = "image/*"
236                             } else if (!showImagesOnly && !showVideosOnly) {
237                                 selectedMimeType = ""
238                             }
239                         }
240                     )
241                 }
242 
243                 Spacer(modifier = Modifier.width(6.dp))
244 
245                 Column(modifier = Modifier.weight(1f)) {
246                     SwitchComponent(
247                         label = stringResource(R.string.show_videos_only),
248                         checked = showVideosOnly,
249                         onCheckedChange = {
250                             showVideosOnly = it
251                             if (it) {
252                                 showImagesOnly = false
253                                 selectedMimeType = "video/*"
254                             } else if (!showImagesOnly && !showVideosOnly) {
255                                 selectedMimeType = ""
256                             }
257                         }
258                     )
259                 }
260             }
261         }
262 
263         if (!isActionGetContentSelected) {
264             // Allow Custom Mime Type
265             SwitchComponent(
266                 label = stringResource(id = R.string.allow_custom_mime_type),
267                 checked = allowCustomMimeType,
268                 onCheckedChange = {
269                     allowCustomMimeType = it
270                 }
271             )
272 
273             if (allowCustomMimeType) {
274                 TextFieldComponent(
275                     // Custom Mime Type Input
276                     value = customMimeTypeInput,
277                     onValueChange = { customMimeType ->
278                         customMimeTypeInput = customMimeType
279                     },
280                     label = stringResource(id = R.string.enter_mime_type)
281                 )
282             }
283 
284             Spacer(modifier = Modifier.height(16.dp))
285 
286             // Launch Tab
287             DropdownList(
288                 label = stringResource(id = R.string.select_launch_tab),
289                 options = LaunchLocation.entries.map { it.name },
290                 selectedOption = selectedLaunchTab,
291                 onOptionSelected = { selectedLaunchTab = it },
292                 enabled = true
293             )
294         }
295 
296         if (!isActionGetContentSelected){
297             // Accent Color
298             TextFieldComponent(
299                 value = accentColor,
300                 onValueChange = { color ->
301                     accentColor = color
302                 },
303                 label = "Accent Color"
304             )
305         }
306 
307         if (!isActionGetContentSelected) {
308             // Switch for enabling pre-selection
309             SwitchComponent(
310                 label = stringResource(R.string.enable_preselection),
311                 checked = isPreSelectionEnabled,
312                 onCheckedChange = { isPreSelectionEnabled = it }
313             )
314         }
315 
316         // Multiple Selection
317         SwitchComponent(
318             label = stringResource(id = R.string.allow_multiple_selection),
319             checked = allowMultiple,
320             onCheckedChange = {
321                 allowMultiple = it
322             }
323         )
324 
325         // Max Number of Media Items
326         // ACTION_GET_CONTENT does not support the intent EXTRA_PICK_IMAGES_MAX
327         // i.e., it doesn't allow user to set a limit on the media items
328         if (allowMultiple && !isActionGetContentSelected) {
329             TextFieldComponent(
330                 value = maxSelectionInput,
331                 onValueChange = {
332                     maxSelectionInput = it
333                     // Converting the input to int
334                     maxMediaItemsDisplayed = it.toIntOrNull() ?: 1
335                 },
336                 label = stringResource(id = R.string.max_number_of_media_items),
337                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
338             )
339         }
340 
341         // Error Message if invalid input is given to Max number of media items
342         if (allowMultiple && maxSelectionLimitError.isNotEmpty()) {
343             ErrorMessage(
344                 text = selectionErrorMessage
345             )
346         }
347 
348         Spacer(modifier = Modifier.height(8.dp))
349 
350         // Pick Media Button
351         ButtonComponent(
352             label = stringResource(R.string.pick_media),
353             onClick = {
354                 // Resetting the maxSelection input box when allowMultiple is unselected
355                 if (!allowMultiple) {
356                     maxSelectionLimitError = ""
357                     selectionErrorMessage = ""
358                     maxSelectionInput = "10"
359                     maxMediaItemsDisplayed = 10
360                 }
361 
362                 // Resetting the custom Mime Type Box when allowCustomMimeType is unselected
363                 if (!allowCustomMimeType) {
364                     customMimeTypeInput = ""
365                 }
366 
367                 val errorMessage = photoPickerViewModel.validateAndLaunchPicker(
368                     isActionGetContentSelected = isActionGetContentSelected,
369                     allowMultiple = allowMultiple,
370                     maxMediaItemsDisplayed = maxMediaItemsDisplayed,
371                     selectedMimeType = selectedMimeType,
372                     allowCustomMimeType = allowCustomMimeType,
373                     customMimeTypeInput = customMimeTypeInput,
374                     isOrderSelectionEnabled = isOrderSelectionEnabled,
375                     selectedLaunchTab = LaunchLocation.valueOf(selectedLaunchTab),
376                     accentColor = accentColor,
377                     isPreSelectionEnabled = isPreSelectionEnabled,
378                     launcher = launcher::launch
379                 )
380                 if (errorMessage != null) {
381                     maxSelectionLimitError = errorMessage
382                     Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
383                 } else {
384                     maxSelectionLimitError = ""
385                 }
386             }
387         )
388 
389         // Error Message if there is a wrong input in the max Selection text field
390         ErrorMessage(
391             text = selectionErrorMessage
392         )
393 
394         Spacer(modifier = Modifier.height(16.dp))
395 
396         Column {
397             // Switch for showing meta data
398             SwitchComponent(
399                 label = stringResource(R.string.show_metadata),
400                 checked = showMetaData,
401                 onCheckedChange = { showMetaData = it }
402             )
403 
404             resultMedia.forEach { uri ->
405                 if (showMetaData) {
406                     MetaDataDetails(
407                         uri = uri,
408                         contentResolver = context.contentResolver,
409                         showMetaData = showMetaData,
410                         inDocsUITab = false
411                     )
412                 }
413                 if (isImage(context, uri)) {
414                     // To display image
415                     GlideImage(
416                         model = uri,
417                         contentDescription = null,
418                         modifier = Modifier
419                             .fillMaxWidth()
420                             .height(600.dp)
421                             .padding(top = 8.dp)
422                     )
423                 } else {
424                     AndroidView(
425                         // To display video
426                         factory = { ctx ->
427                             VideoView(ctx).apply {
428                                 setVideoURI(uri)
429                                 start()
430                             }
431                         },
432                         modifier = Modifier
433                             .fillMaxWidth()
434                             .height(600.dp)
435                             .padding(top = 8.dp)
436                     )
437                 }
438                 Spacer(modifier = Modifier.height(20.dp))
439                 HorizontalDivider(thickness = 6.dp)
440                 Spacer(modifier = Modifier.height(17.dp))
441             }
442         }
443     }
444 }
445