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