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.docsui
17
18 import android.app.Activity
19 import android.net.Uri
20 import android.os.Build
21 import android.widget.VideoView
22 import androidx.activity.compose.rememberLauncherForActivityResult
23 import androidx.activity.result.contract.ActivityResultContracts
24 import androidx.annotation.RequiresApi
25 import androidx.compose.foundation.layout.Arrangement
26 import androidx.compose.foundation.layout.Column
27 import androidx.compose.foundation.layout.Row
28 import androidx.compose.foundation.layout.Spacer
29 import androidx.compose.foundation.layout.fillMaxSize
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.verticalScroll
36 import androidx.compose.material3.ButtonDefaults
37 import androidx.compose.material3.HorizontalDivider
38 import androidx.compose.material3.Text
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.collectAsState
41 import androidx.compose.runtime.getValue
42 import androidx.compose.runtime.mutableStateOf
43 import androidx.compose.runtime.remember
44 import androidx.compose.runtime.setValue
45 import androidx.compose.ui.Alignment
46 import androidx.compose.ui.Modifier
47 import androidx.compose.ui.graphics.Color
48 import androidx.compose.ui.platform.LocalContext
49 import androidx.compose.ui.res.stringResource
50 import androidx.compose.ui.text.font.FontWeight
51 import androidx.compose.ui.unit.dp
52 import androidx.compose.ui.unit.sp
53 import androidx.compose.ui.viewinterop.AndroidView
54 import androidx.lifecycle.viewmodel.compose.viewModel
55 import com.android.providers.media.tools.photopickerv2.R
56 import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent
57 import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails
58 import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent
59 import com.android.providers.media.tools.photopickerv2.utils.TextFieldComponent
60 import com.android.providers.media.tools.photopickerv2.utils.isImage
61 import com.android.providers.media.tools.photopickerv2.utils.resetMedia
62 import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
63 import com.bumptech.glide.integration.compose.GlideImage
64
65 /**
66 * This is the screen for the DocsUI tab.
67 */
68 @RequiresApi(Build.VERSION_CODES.O)
69 @OptIn(ExperimentalGlideComposeApi::class)
70 @Composable
71 fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) {
72 val context = LocalContext.current
73
74 // The ACTION_GET_CONTENT intent is selected by default
75 var selectedButton by remember { mutableStateOf<Int?>(R.string.action_get_content) }
76
77 var allowMultiple by remember { mutableStateOf(false) }
78
79 var isActionGetContentSelected by remember { mutableStateOf(true) }
80 var isOpenDocumentSelected by remember { mutableStateOf(false) }
81 var isCreateDocumentSelected by remember { mutableStateOf(false) }
82
83 var allowCustomMimeType by remember { mutableStateOf(false) }
84 var selectedMimeType by remember { mutableStateOf("") }
85 var customMimeTypeInput by remember { mutableStateOf("") }
86
87 var showImagesOnly by remember { mutableStateOf(false) }
88 var showVideosOnly by remember { mutableStateOf(false) }
89
90 // Meta Data Details
91 var showMetaData by remember { mutableStateOf(false) }
92
93 // Color of ACTION_GET_CONTENT and OPEN_DOCUMENT button
94 val getContentColor = if (isActionGetContentSelected){
95 ButtonDefaults.buttonColors()
96 } else ButtonDefaults.buttonColors(Color.Gray)
97
98 val openDocumentColor = if (isOpenDocumentSelected) {
99 ButtonDefaults.buttonColors()
100 } else ButtonDefaults.buttonColors(Color.Gray)
101
102 val createDocumentColor = if (isCreateDocumentSelected) {
103 ButtonDefaults.buttonColors()
104 } else ButtonDefaults.buttonColors(Color.Gray)
105
106 val openDocumentTreeColor = if (!isActionGetContentSelected &&
107 !isOpenDocumentSelected &&
108 !isCreateDocumentSelected) {
109 ButtonDefaults.buttonColors()
110 } else ButtonDefaults.buttonColors(Color.Gray)
111
112 // For handling the result of the photo picking activity
113 val launcher = rememberLauncherForActivityResult(
114 contract = ActivityResultContracts.StartActivityForResult()
115 ) { result ->
116 if (result.resultCode == Activity.RESULT_OK) {
117 // Get the clipData containing multiple selected items.
118 val clipData = result.data?.clipData
119 val uris = mutableListOf<Uri>() // An empty list to store the selected URIs
120
121 // If multiple items are selected (clipData is not null), iterate through the items.
122 if (clipData != null) {
123 // Add each selected item to the URIs list,
124 // up to the maxMediaItemsDisplayed limit if multiple selection is allowed
125 for (i in 0 until clipData.itemCount) {
126 uris.add(clipData.getItemAt(i).uri)
127 }
128 } else {
129 // If only a single item is selected, add its URI to the list
130 result.data?.data?.let { uris.add(it) }
131 }
132
133 // Update the ViewModel with the list of selected URIs
134 docsUIViewModel.updateSelectedMediaList(uris)
135 }
136 }
137
138 val resultMedia by docsUIViewModel.selectedMedia.collectAsState()
139
140 fun resetFeatureComponents(
141 isGetContentSelected: Boolean,
142 isOpenDocumentIntentSelected: Boolean,
143 isCreateDocumentIntentSelected: Boolean,
144 selectedButtonType: Int
145 ) {
146 isActionGetContentSelected = isGetContentSelected
147 isOpenDocumentSelected = isOpenDocumentIntentSelected
148 isCreateDocumentSelected = isCreateDocumentIntentSelected
149 selectedButton = selectedButtonType
150 allowMultiple = false
151 showImagesOnly = false
152 showVideosOnly = false
153 selectedMimeType = ""
154 resetMedia(docsUIViewModel)
155 allowCustomMimeType = false
156 customMimeTypeInput = ""
157 }
158
159 Column(
160 modifier = Modifier
161 .verticalScroll(rememberScrollState())
162 .padding(16.dp)
163 .fillMaxWidth()
164 ){
165 Text(
166 text = stringResource(id = R.string.tab_docsui),
167 fontWeight = FontWeight.Bold,
168 fontSize = 25.sp,
169 modifier = Modifier.padding(16.dp)
170 )
171
172 Row (
173 modifier = Modifier
174 .fillMaxWidth()
175 .padding(vertical = 5.dp),
176 verticalAlignment = Alignment.CenterVertically,
177 horizontalArrangement = Arrangement.spacedBy(8.dp)
178 ){
179 ButtonComponent(
180 label = stringResource(id = R.string.action_get_content),
181 onClick = {
182 resetFeatureComponents(
183 isGetContentSelected = true,
184 isOpenDocumentIntentSelected = false,
185 isCreateDocumentIntentSelected = false,
186 selectedButtonType = R.string.action_get_content
187 )
188 },
189 modifier = Modifier.weight(1f),
190 colors = getContentColor
191 )
192
193 ButtonComponent(
194 label = stringResource(R.string.open_document),
195 onClick = {
196 resetFeatureComponents(
197 isGetContentSelected = false,
198 isOpenDocumentIntentSelected = true,
199 isCreateDocumentIntentSelected = false,
200 selectedButtonType = R.string.open_document
201 )
202 },
203 modifier = Modifier.weight(1f),
204 colors = openDocumentColor
205 )
206 }
207
208 Row (
209 modifier = Modifier
210 .fillMaxWidth()
211 .padding(vertical = 5.dp),
212 verticalAlignment = Alignment.CenterVertically,
213 horizontalArrangement = Arrangement.spacedBy(8.dp)
214 ){
215 ButtonComponent(
216 label = stringResource(id = R.string.open_document_tree),
217 onClick = {
218 resetFeatureComponents(
219 isGetContentSelected = false,
220 isOpenDocumentIntentSelected = false,
221 isCreateDocumentIntentSelected = false,
222 selectedButtonType = R.string.open_document_tree
223 )
224 },
225 modifier = Modifier.weight(1f),
226 colors = openDocumentTreeColor
227 )
228
229 ButtonComponent(
230 label = stringResource(R.string.create_document),
231 onClick = {
232 resetFeatureComponents(
233 isGetContentSelected = false,
234 isOpenDocumentIntentSelected = false,
235 isCreateDocumentIntentSelected = true,
236 selectedButtonType = R.string.create_document
237 )
238 },
239 modifier = Modifier.weight(1f),
240 colors = createDocumentColor
241 )
242 }
243
244 if (isActionGetContentSelected || isOpenDocumentSelected){
245 // SHOW ONLY IMAGES OR VIDEOS
246 if (!allowCustomMimeType) {
247 Row(
248 modifier = Modifier
249 .fillMaxWidth()
250 .padding(vertical = 5.dp),
251 verticalAlignment = Alignment.CenterVertically
252 ) {
253 Column (modifier = Modifier.weight(1f)){
254 SwitchComponent(
255 label = stringResource(R.string.show_images_only),
256 checked = showImagesOnly,
257 onCheckedChange = {
258 showImagesOnly = it
259 if (it) {
260 showVideosOnly = false
261 selectedMimeType = "image/*"
262 } else if (!showImagesOnly && !showVideosOnly) {
263 selectedMimeType = ""
264 }
265 }
266 )
267 }
268
269 Spacer(modifier = Modifier.width(6.dp))
270
271 Column (modifier = Modifier.weight(1f)){
272 SwitchComponent(
273 label = stringResource(R.string.show_videos_only),
274 checked = showVideosOnly,
275 onCheckedChange = {
276 showVideosOnly = it
277 if (it) {
278 showImagesOnly = false
279 selectedMimeType = "video/*"
280 } else if (!showImagesOnly && !showVideosOnly) {
281 selectedMimeType = ""
282 }
283 }
284 )
285 }
286 }
287 }
288
289 // Allow Custom Mime Type
290 SwitchComponent(
291 label = stringResource(id = R.string.allow_custom_mime_type),
292 checked = allowCustomMimeType,
293 onCheckedChange = {
294 allowCustomMimeType = it
295 }
296 )
297
298 if (allowCustomMimeType){
299 TextFieldComponent(
300 // Custom Mime Type Input
301 value = customMimeTypeInput,
302 onValueChange = { customMimeType ->
303 customMimeTypeInput = customMimeType
304 },
305 label = stringResource(id = R.string.enter_mime_type)
306 )
307 }
308
309 // Multiple Selection
310 SwitchComponent(
311 label = stringResource(id = R.string.allow_multiple_selection),
312 checked = allowMultiple,
313 onCheckedChange = {
314 allowMultiple = it
315 }
316 )
317 }
318
319 // Pick Media Button
320 ButtonComponent(
321 label = if (!isCreateDocumentSelected) {
322 stringResource(R.string.pick_media)
323 } else {
324 stringResource(R.string.create_file)
325 },
326 onClick = {
327
328
329 // Resetting the custom Mime Type Box when allowCustomMimeType is unselected
330 if (!allowCustomMimeType){
331 customMimeTypeInput = ""
332 }
333
334 /* TODO: (@adityasngh) please check the URI below and fix this intent.
335 // For CREATE_DOCUMENT intent
336 val initialUri = Uri.parse("content://some/initial/uri")
337
338 val errorMessage = docsUIViewModel.validateAndLaunchPicker(
339 isActionGetContentSelected = isActionGetContentSelected,
340 isOpenDocumentSelected = isOpenDocumentSelected,
341 isCreateDocumentSelected = isCreateDocumentSelected,
342 allowMultiple = allowMultiple,
343 selectedMimeType = selectedMimeType,
344 allowCustomMimeType = allowCustomMimeType,
345 customMimeTypeInput = customMimeTypeInput,
346 pickerInitialUri = initialUri,
347 launcher = launcher::launch
348 )
349 if (errorMessage != null) {
350 Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
351 }
352 */
353 }
354 )
355
356 Spacer(modifier = Modifier.height(16.dp))
357
358 Column {
359 if (isActionGetContentSelected || isOpenDocumentSelected){
360 // Switch for showing meta data
361 SwitchComponent(
362 label = stringResource(R.string.show_metadata),
363 checked = showMetaData,
364 onCheckedChange = { showMetaData = it }
365 )
366 }
367
368 if (!isCreateDocumentSelected){
369 resultMedia.forEach { uri ->
370 if (showMetaData) {
371 MetaDataDetails(
372 uri = uri,
373 contentResolver = context.contentResolver,
374 showMetaData = showMetaData,
375 inDocsUITab = true
376 )
377 }
378 if (isImage(context, uri)) {
379 // To display image
380 GlideImage(
381 model = uri,
382 contentDescription = null,
383 modifier = Modifier
384 .fillMaxWidth()
385 .fillMaxSize()
386 .padding(top = 8.dp)
387 )
388 } else {
389 AndroidView(
390 // To display video
391 factory = { ctx ->
392 VideoView(ctx).apply {
393 setVideoURI(uri)
394 start()
395 }
396 },
397 modifier = Modifier
398 .fillMaxWidth()
399 .height(600.dp)
400 .padding(top = 8.dp)
401 )
402 }
403 Spacer(modifier = Modifier.height(20.dp))
404 HorizontalDivider(thickness = 6.dp)
405 Spacer(modifier = Modifier.height(17.dp))
406 }
407 }
408 }
409 }
410 }