From e485e62ef346232a2aedcf6affe9a6b6bc79771a Mon Sep 17 00:00:00 2001 From: Yacine Rezgui Date: Fri, 23 Aug 2024 17:44:15 +0200 Subject: [PATCH 1/4] Add GetDocument draft demo --- samples/README.md | 2 + .../storageaccessframework/FileCard.kt | 221 ++++++++++++++++++ .../storageaccessframework/GetDocument.kt | 179 ++++++++++++++ 3 files changed, 402 insertions(+) create mode 100644 samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt create mode 100644 samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt diff --git a/samples/README.md b/samples/README.md index 3c26a020..64fe1cd8 100644 --- a/samples/README.md +++ b/samples/README.md @@ -48,6 +48,8 @@ Drag and Drop using the views This sample demonstrates editing an UltraHDR image and the resulting gainmap as well. Spatial edit operations like crop, rotate, scale are supported - [Find devices sample](connectivity/bluetooth/ble/src/main/java/com/example/platform/connectivity/bluetooth/ble/FindBLEDevicesSample.kt): This example will demonstrate how to scanning for Low Energy Devices +- [GetDocument](storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt): +Open a document using the Storage Access Framework - [Haptics - 1. Vibration effects](user-interface/haptics/src/main/java/com/example/platform/ui/haptics/Haptics.kt): Shows various vibration effects. - [Hyphenation](user-interface/text/src/main/java/com/example/platform/ui/text/Hyphenation.kt): diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt new file mode 100644 index 00000000..41635ef3 --- /dev/null +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.storage.storageaccessframework + +import android.text.format.Formatter +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile +import androidx.compose.material.icons.filled.AudioFile +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.PictureAsPdf +import androidx.compose.material.icons.filled.VideoFile +import androidx.compose.material3.Button +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +internal enum class FileType(val mimeType: String) { + Image("image/*"), + Video("video/*"), + Audio("audio/*"), + Text("text/*"), + Pdf("application/pdf"), + Any("*/*"); +} + +data class FileRecord(val filename: String, val size: Long, val mimeType: String) + +@Composable +fun FileCard( + file: FileRecord, + icon: ImageVector, + contentPreview: @Composable (() -> Unit)? = null, +) { + val sizeLabel = Formatter.formatShortFileSize(LocalContext.current, file.size) + + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text(file.filename, style = MaterialTheme.typography.headlineSmall, maxLines = 2) + Spacer(modifier = Modifier.height(4.dp)) + Text("$sizeLabel · ${file.mimeType}", style = MaterialTheme.typography.bodyMedium) + + if (contentPreview != null) { + contentPreview() + } + } + } + } +} + +@Composable +fun ImageFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.Image) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* Handle button click */ }) { + Text("Read first 10 bytes") + } + } + } +} + +@Composable +fun VideoFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.VideoFile) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* Handle button click */ }) { + Text("Read first 10 bytes") + } + } + } +} + +@Composable +fun AudioFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.AudioFile) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* Handle button click */ }) { + Text("Read first 10 bytes") + } + } + } +} + +@Composable +fun TextFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.Description) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* Handle button click */ }) { + Text("Read first 10 bytes") + } + } + } +} + +@Composable +fun PdfFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.PictureAsPdf) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* Handle button click */ }) { + Text("Read first 10 bytes") + } + } + } +} + +@Composable +fun BinaryFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.AutoMirrored.Filled.InsertDriveFile) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* Handle button click */ }) { + Text("Read first 10 bytes") + } + } + } +} + +@Preview +@Composable +fun ImageFileCard_Preview() { + ImageFileCard(FileRecord("AmazingPhoto.png", 345_000, "image/png")) +} + +@Preview +@Composable +fun VideoFileCard_Preview() { + VideoFileCard(FileRecord("All hands - meeting recording.mp4", 1_234_567_890, "image/png")) +} + +@Preview +@Composable +fun AudioFileCard_Preview() { + AudioFileCard(FileRecord("Queen - We will rock you.mp3", 5_432_100, "audio/mp3")) +} + +@Preview +@Composable +fun TextFileCard_Preview() { + TextFileCard(FileRecord("Android Jetpack Compose.txt", 5_678, "text/plain")) +} + +@Preview +@Composable +fun PdfFileCard_Preview() { + PdfFileCard(FileRecord("Android Jetpack Compose.pdf", 1_234_567, "application/pdf")) +} + +@Preview +@Composable +fun BinaryFileCard_Preview() { + BinaryFileCard(FileRecord("binary.bin", 78_420_968, "application/octet-stream")) +} \ No newline at end of file diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt new file mode 100644 index 00000000..a46652ec --- /dev/null +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.storage.storageaccessframework + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import com.google.android.catalog.framework.annotations.Sample + +@Sample( + name = "GetDocument", + description = "Open a document using the Storage Access Framework", + documentation = "https://developer.android.com/training/data-storage/shared/documents-files#open-file", +) +@Composable +fun GetDocument() { + var fileTypes by remember { mutableStateOf(emptySet()) } + var selectMultiple by remember { mutableStateOf(false) } + var expanded by remember { mutableStateOf(false) } + var selectedFiles by remember { mutableStateOf(emptyList()) } + + val getSingleDocument = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + selectedFiles = uri?.let { listOf(it) } ?: emptyList() + } + + val getMultipleDocuments = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + selectedFiles = uris + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + floatingActionButton = { + ExtendedFloatingActionButton( + onClick = { + if (selectMultiple) { + getMultipleDocuments.launch("*/*") + } else { + getSingleDocument.launch("*/*") + } + }, + ) { + Text(if (selectMultiple) "Select Files" else "Select File") + } + }, + ) { paddingValues -> + LazyColumn(Modifier.padding(paddingValues)) { + item { + ListItem( + headlineContent = { Text("File type filter") }, + supportingContent = { + Text( + if (fileTypes.isEmpty()) "Any" else fileTypes.joinToString { it.name }, + ) + }, + trailingContent = { + val scrollState = rememberScrollState() + Box( + modifier = Modifier + .wrapContentSize(Alignment.TopStart), + ) { + IconButton(onClick = { expanded = true }) { + Icon( + Icons.Default.FilterAlt, + contentDescription = "Localized description", + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + scrollState = scrollState, + ) { + FileType.entries.forEach { fileType -> + DropdownMenuItem( + text = { Text(fileType.name) }, + onClick = { fileTypes = fileTypes.toggle(fileType) }, + leadingIcon = { + if (fileTypes.contains(fileType)) { + Icon( + Icons.Outlined.Check, + contentDescription = "Selected", + ) + } + }, + ) + } + } + LaunchedEffect(expanded) { + if (expanded) { + // Scroll to show the bottom menu items. + scrollState.scrollTo(scrollState.maxValue) + } + } + } + }, + ) + HorizontalDivider() + } + item { + ListItem( + headlineContent = { Text("File type filter") }, + trailingContent = { + Switch( + modifier = Modifier.semantics { + contentDescription = "Select multiple files" + }, + checked = selectMultiple, + onCheckedChange = { selectMultiple = it }, + thumbContent = { + if (selectMultiple) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + }, + ) + }, + ) + HorizontalDivider() + } + } + } +} + + +private fun Set.toggle(item: T): Set { + return if (contains(item)) { + this - item + } else { + this + item + } +} \ No newline at end of file From a5746e720396cba1c2d3be76f22ba21a42bd2400 Mon Sep 17 00:00:00 2001 From: Yacine Rezgui Date: Mon, 26 Aug 2024 17:19:09 +0200 Subject: [PATCH 2/4] Update UI & mime type filtering logic --- .../storageaccessframework/GetDocument.kt | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt index a46652ec..5e13f63e 100644 --- a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt @@ -60,7 +60,7 @@ import com.google.android.catalog.framework.annotations.Sample ) @Composable fun GetDocument() { - var fileTypes by remember { mutableStateOf(emptySet()) } + var selectedFilter by remember { mutableStateOf(FileType.Any) } var selectMultiple by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) } var selectedFiles by remember { mutableStateOf(emptyList()) } @@ -79,9 +79,9 @@ fun GetDocument() { ExtendedFloatingActionButton( onClick = { if (selectMultiple) { - getMultipleDocuments.launch("*/*") + getMultipleDocuments.launch(selectedFilter.mimeType) } else { - getSingleDocument.launch("*/*") + getSingleDocument.launch(selectedFilter.mimeType) } }, ) { @@ -94,9 +94,7 @@ fun GetDocument() { ListItem( headlineContent = { Text("File type filter") }, supportingContent = { - Text( - if (fileTypes.isEmpty()) "Any" else fileTypes.joinToString { it.name }, - ) + Text(selectedFilter.name) }, trailingContent = { val scrollState = rememberScrollState() @@ -118,9 +116,9 @@ fun GetDocument() { FileType.entries.forEach { fileType -> DropdownMenuItem( text = { Text(fileType.name) }, - onClick = { fileTypes = fileTypes.toggle(fileType) }, + onClick = { selectedFilter = fileType }, leadingIcon = { - if (fileTypes.contains(fileType)) { + if (selectedFilter == fileType) { Icon( Icons.Outlined.Check, contentDescription = "Selected", @@ -143,7 +141,7 @@ fun GetDocument() { } item { ListItem( - headlineContent = { Text("File type filter") }, + headlineContent = { Text("Select multiple files?") }, trailingContent = { Switch( modifier = Modifier.semantics { @@ -167,13 +165,4 @@ fun GetDocument() { } } } -} - - -private fun Set.toggle(item: T): Set { - return if (contains(item)) { - this - item - } else { - this + item - } } \ No newline at end of file From 732a2f0652a139c67f97a9db4a29987cae479dac Mon Sep 17 00:00:00 2001 From: Yacine Rezgui Date: Fri, 30 Aug 2024 18:20:53 +0200 Subject: [PATCH 3/4] Add file details loading --- .../storageaccessframework/FileCard.kt | 58 +++++++++------ .../storageaccessframework/FileRecord.kt | 74 +++++++++++++++++++ .../storageaccessframework/GetDocument.kt | 39 ++++++++-- 3 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileRecord.kt diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt index 41635ef3..e298dba5 100644 --- a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt @@ -42,20 +42,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -internal enum class FileType(val mimeType: String) { - Image("image/*"), - Video("video/*"), - Audio("audio/*"), - Text("text/*"), - Pdf("application/pdf"), - Any("*/*"); -} - -data class FileRecord(val filename: String, val size: Long, val mimeType: String) - @Composable fun FileCard( file: FileRecord, @@ -82,7 +72,12 @@ fun FileCard( ) Spacer(modifier = Modifier.width(16.dp)) Column { - Text(file.filename, style = MaterialTheme.typography.headlineSmall, maxLines = 2) + Text( + file.name, + style = MaterialTheme.typography.headlineSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) Spacer(modifier = Modifier.height(4.dp)) Text("$sizeLabel · ${file.mimeType}", style = MaterialTheme.typography.bodyMedium) @@ -103,7 +98,7 @@ fun ImageFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = } else { Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { /* Handle button click */ }) { - Text("Read first 10 bytes") + Text("Load image") } } } @@ -118,7 +113,7 @@ fun VideoFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = } else { Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { /* Handle button click */ }) { - Text("Read first 10 bytes") + Text("Load thumbnail") } } } @@ -148,7 +143,7 @@ fun TextFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = n } else { Spacer(modifier = Modifier.height(16.dp)) Button(onClick = { /* Handle button click */ }) { - Text("Read first 10 bytes") + Text("Load content") } } } @@ -187,35 +182,56 @@ fun BinaryFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = @Preview @Composable fun ImageFileCard_Preview() { - ImageFileCard(FileRecord("AmazingPhoto.png", 345_000, "image/png")) + ImageFileCard(FileRecord("AmazingPhoto.png", 345_000, "image/png", FileType.Image)) } @Preview @Composable fun VideoFileCard_Preview() { - VideoFileCard(FileRecord("All hands - meeting recording.mp4", 1_234_567_890, "image/png")) + VideoFileCard( + FileRecord( + "All hands - meeting recording.mp4", + 1_234_567_890, + "video/mp4", + FileType.Video, + ), + ) } @Preview @Composable fun AudioFileCard_Preview() { - AudioFileCard(FileRecord("Queen - We will rock you.mp3", 5_432_100, "audio/mp3")) + AudioFileCard( + FileRecord( + "Queen - We will rock you.mp3", + 5_432_100, + "audio/mp3", + FileType.Audio, + ), + ) } @Preview @Composable fun TextFileCard_Preview() { - TextFileCard(FileRecord("Android Jetpack Compose.txt", 5_678, "text/plain")) + TextFileCard(FileRecord("Android Jetpack Compose.txt", 5_678, "text/plain", FileType.Text)) } @Preview @Composable fun PdfFileCard_Preview() { - PdfFileCard(FileRecord("Android Jetpack Compose.pdf", 1_234_567, "application/pdf")) + PdfFileCard( + FileRecord( + "Android Jetpack Compose.pdf", + 1_234_567, + "application/pdf", + FileType.Pdf, + ), + ) } @Preview @Composable fun BinaryFileCard_Preview() { - BinaryFileCard(FileRecord("binary.bin", 78_420_968, "application/octet-stream")) + BinaryFileCard(FileRecord("binary.bin", 78_420_968, "application/octet-stream", FileType.Any)) } \ No newline at end of file diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileRecord.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileRecord.kt new file mode 100644 index 00000000..5dcb474d --- /dev/null +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileRecord.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.storage.storageaccessframework + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +data class FileRecord(val name: String, val size: Long, val mimeType: String, val fileType: FileType) { + companion object { + suspend fun fromUri(uri: Uri, context: Context): FileRecord? = withContext(Dispatchers.IO) { + val mimeType = context.contentResolver.getType(uri) ?: return@withContext null + val fileType = when { + mimeType.startsWith("image/") -> FileType.Image + mimeType.startsWith("video/") -> FileType.Video + mimeType.startsWith("audio/") -> FileType.Audio + mimeType.startsWith("text/") -> FileType.Text + mimeType == "application/pdf" -> FileType.Pdf + else -> FileType.Any + } + + val projection = arrayOf( + OpenableColumns.DISPLAY_NAME, + OpenableColumns.SIZE, + ) + + val cursor = context.contentResolver.query( + uri, + projection, + null, + null, + null, + ) ?: return@withContext null + + cursor.use { + if (!cursor.moveToFirst()) { + return@withContext null + } + + return@use FileRecord( + name = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)), + size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)), + mimeType = mimeType, + fileType = fileType, + ) + } + } + } +} + +enum class FileType(val mimeType: String) { + Image("image/*"), + Video("video/*"), + Audio("audio/*"), + Text("text/*"), + Pdf("application/pdf"), + Any("*/*"); +} \ No newline at end of file diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt index 5e13f63e..264495f3 100644 --- a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt @@ -16,7 +16,6 @@ package com.example.platform.storage.storageaccessframework -import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box @@ -25,6 +24,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -46,12 +46,15 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import com.google.android.catalog.framework.annotations.Sample +import kotlinx.coroutines.launch @Sample( name = "GetDocument", @@ -60,18 +63,30 @@ import com.google.android.catalog.framework.annotations.Sample ) @Composable fun GetDocument() { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current var selectedFilter by remember { mutableStateOf(FileType.Any) } var selectMultiple by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) } - var selectedFiles by remember { mutableStateOf(emptyList()) } + var selectedFiles by remember { mutableStateOf(emptyList()) } - val getSingleDocument = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - selectedFiles = uri?.let { listOf(it) } ?: emptyList() - } + val getSingleDocument = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + coroutineScope.launch { + selectedFiles = uri?.let { uri -> + FileRecord.fromUri(uri, context)?.let { listOf(it) } + } ?: emptyList() + } + } - val getMultipleDocuments = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> - selectedFiles = uris - } + val getMultipleDocuments = + rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + coroutineScope.launch { + selectedFiles = uris.mapNotNull { uri -> + FileRecord.fromUri(uri, context) + } + } + } Scaffold( modifier = Modifier.fillMaxSize(), @@ -163,6 +178,14 @@ fun GetDocument() { ) HorizontalDivider() } + items(selectedFiles) { file -> + when (file.fileType) { + FileType.Image -> ImageFileCard(file) + FileType.Video -> VideoFileCard(file) + FileType.Audio -> AudioFileCard(file) + else -> BinaryFileCard(file) + } + } } } } \ No newline at end of file From 4c59d408564403d7eac2a83a116fa4177432dade Mon Sep 17 00:00:00 2001 From: Yacine Rezgui Date: Mon, 2 Sep 2024 18:30:28 +0200 Subject: [PATCH 4/4] Finish file preview logic --- .../storageaccessframework/FileCard.kt | 237 --------- .../storageaccessframework/GetDocument.kt | 12 +- .../storageaccessframework/shared/FileCard.kt | 460 ++++++++++++++++++ .../{ => shared}/FileRecord.kt | 11 +- 4 files changed, 480 insertions(+), 240 deletions(-) delete mode 100644 samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt create mode 100644 samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/shared/FileCard.kt rename samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/{ => shared}/FileRecord.kt (91%) diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt deleted file mode 100644 index e298dba5..00000000 --- a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileCard.kt +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.platform.storage.storageaccessframework - -import android.text.format.Formatter -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.InsertDriveFile -import androidx.compose.material.icons.filled.AudioFile -import androidx.compose.material.icons.filled.Description -import androidx.compose.material.icons.filled.Image -import androidx.compose.material.icons.filled.PictureAsPdf -import androidx.compose.material.icons.filled.VideoFile -import androidx.compose.material3.Button -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp - -@Composable -fun FileCard( - file: FileRecord, - icon: ImageVector, - contentPreview: @Composable (() -> Unit)? = null, -) { - val sizeLabel = Formatter.formatShortFileSize(LocalContext.current, file.size) - - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - Row( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.Top, - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - ) - Spacer(modifier = Modifier.width(16.dp)) - Column { - Text( - file.name, - style = MaterialTheme.typography.headlineSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Spacer(modifier = Modifier.height(4.dp)) - Text("$sizeLabel · ${file.mimeType}", style = MaterialTheme.typography.bodyMedium) - - if (contentPreview != null) { - contentPreview() - } - } - } - } -} - -@Composable -fun ImageFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { - FileCard(file, Icons.Default.Image) { - if (contentPreview != null) { - Spacer(modifier = Modifier.height(16.dp)) - contentPreview() - } else { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { /* Handle button click */ }) { - Text("Load image") - } - } - } -} - -@Composable -fun VideoFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { - FileCard(file, Icons.Default.VideoFile) { - if (contentPreview != null) { - Spacer(modifier = Modifier.height(16.dp)) - contentPreview() - } else { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { /* Handle button click */ }) { - Text("Load thumbnail") - } - } - } -} - -@Composable -fun AudioFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { - FileCard(file, Icons.Default.AudioFile) { - if (contentPreview != null) { - Spacer(modifier = Modifier.height(16.dp)) - contentPreview() - } else { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { /* Handle button click */ }) { - Text("Read first 10 bytes") - } - } - } -} - -@Composable -fun TextFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { - FileCard(file, Icons.Default.Description) { - if (contentPreview != null) { - Spacer(modifier = Modifier.height(16.dp)) - contentPreview() - } else { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { /* Handle button click */ }) { - Text("Load content") - } - } - } -} - -@Composable -fun PdfFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { - FileCard(file, Icons.Default.PictureAsPdf) { - if (contentPreview != null) { - Spacer(modifier = Modifier.height(16.dp)) - contentPreview() - } else { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { /* Handle button click */ }) { - Text("Read first 10 bytes") - } - } - } -} - -@Composable -fun BinaryFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { - FileCard(file, Icons.AutoMirrored.Filled.InsertDriveFile) { - if (contentPreview != null) { - Spacer(modifier = Modifier.height(16.dp)) - contentPreview() - } else { - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { /* Handle button click */ }) { - Text("Read first 10 bytes") - } - } - } -} - -@Preview -@Composable -fun ImageFileCard_Preview() { - ImageFileCard(FileRecord("AmazingPhoto.png", 345_000, "image/png", FileType.Image)) -} - -@Preview -@Composable -fun VideoFileCard_Preview() { - VideoFileCard( - FileRecord( - "All hands - meeting recording.mp4", - 1_234_567_890, - "video/mp4", - FileType.Video, - ), - ) -} - -@Preview -@Composable -fun AudioFileCard_Preview() { - AudioFileCard( - FileRecord( - "Queen - We will rock you.mp3", - 5_432_100, - "audio/mp3", - FileType.Audio, - ), - ) -} - -@Preview -@Composable -fun TextFileCard_Preview() { - TextFileCard(FileRecord("Android Jetpack Compose.txt", 5_678, "text/plain", FileType.Text)) -} - -@Preview -@Composable -fun PdfFileCard_Preview() { - PdfFileCard( - FileRecord( - "Android Jetpack Compose.pdf", - 1_234_567, - "application/pdf", - FileType.Pdf, - ), - ) -} - -@Preview -@Composable -fun BinaryFileCard_Preview() { - BinaryFileCard(FileRecord("binary.bin", 78_420_968, "application/octet-stream", FileType.Any)) -} \ No newline at end of file diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt index 264495f3..f23d9623 100644 --- a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/GetDocument.kt @@ -53,6 +53,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import com.example.platform.storage.storageaccessframework.shared.AudioFileCard +import com.example.platform.storage.storageaccessframework.shared.BinaryFileCard +import com.example.platform.storage.storageaccessframework.shared.FileRecord +import com.example.platform.storage.storageaccessframework.shared.FileType +import com.example.platform.storage.storageaccessframework.shared.ImageFileCard +import com.example.platform.storage.storageaccessframework.shared.PdfFileCard +import com.example.platform.storage.storageaccessframework.shared.TextFileCard +import com.example.platform.storage.storageaccessframework.shared.VideoFileCard import com.google.android.catalog.framework.annotations.Sample import kotlinx.coroutines.launch @@ -183,7 +191,9 @@ fun GetDocument() { FileType.Image -> ImageFileCard(file) FileType.Video -> VideoFileCard(file) FileType.Audio -> AudioFileCard(file) - else -> BinaryFileCard(file) + FileType.Text -> TextFileCard(file) + FileType.Pdf -> PdfFileCard(file) + FileType.Any -> BinaryFileCard(file) } } } diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/shared/FileCard.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/shared/FileCard.kt new file mode 100644 index 00000000..950f1547 --- /dev/null +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/shared/FileCard.kt @@ -0,0 +1,460 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.platform.storage.storageaccessframework.shared + +import android.content.Context +import android.net.Uri +import android.text.format.Formatter +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile +import androidx.compose.material.icons.filled.AudioFile +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.PictureAsPdf +import androidx.compose.material.icons.filled.VideoFile +import androidx.compose.material3.Button +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.InputStreamReader + +@Composable +fun FileCard( + file: FileRecord, + icon: ImageVector, + contentPreview: @Composable (() -> Unit)? = null, +) { + val sizeLabel = Formatter.formatShortFileSize(LocalContext.current, file.size) + + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(42.dp), + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text( + file.name, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text("$sizeLabel · ${file.mimeType}", style = MaterialTheme.typography.bodyMedium) + + if (contentPreview != null) { + contentPreview() + } + } + } + } +} + +@Composable +fun ImageFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.Image) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + var loadThumbnail by remember { mutableStateOf(false) } + + Spacer(modifier = Modifier.height(16.dp)) + if (loadThumbnail) { + AsyncImage( + model = file.uri, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(1f), + ) + } else { + Button(onClick = { loadThumbnail = true }) { + Text("Load thumbnail") + } + } + } + } +} + +@Composable +fun VideoFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.VideoFile) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + var loadThumbnail by remember { mutableStateOf(false) } + + Spacer(modifier = Modifier.height(16.dp)) + if (loadThumbnail) { + AsyncImage( + model = file.uri, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(1f), + ) + } else { + Button(onClick = { loadThumbnail = true }) { + Text("Load thumbnail") + } + } + } + } +} + +@Composable +fun AudioFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.AudioFile) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + + var loadFilePreview by remember { mutableStateOf(false) } + val filePreview by loadRawFileContent(file.uri, LocalContext.current, loadFilePreview) + + Spacer(modifier = Modifier.height(16.dp)) + when (filePreview) { + FilePreview.NotLoadedYet -> { + Button(onClick = { loadFilePreview = true }) { + Text("Display first 10 bytes") + } + } + + FilePreview.Loading -> { + Text("Loading...") + } + + is FilePreview.Loaded -> { + Text("First 10 bytes: ${(filePreview as FilePreview.Loaded).content}") + } + + is FilePreview.Error -> { + Text("Error: ${(filePreview as FilePreview.Error).throwable.message}") + } + } + } + } +} + +@Composable +fun TextFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.Description) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + + var loadFilePreview by remember { mutableStateOf(false) } + val filePreview by loadTextFileContent(file.uri, LocalContext.current, loadFilePreview) + + Spacer(modifier = Modifier.height(16.dp)) + when (filePreview) { + FilePreview.NotLoadedYet -> { + Button(onClick = { loadFilePreview = true }) { + Text("Read first 300 characters") + } + } + + FilePreview.Loading -> { + Text("Loading...") + } + + is FilePreview.Loaded -> { + Text("First 300 chars: ${(filePreview as FilePreview.Loaded).content}") + } + + is FilePreview.Error -> { + Text("Error: ${(filePreview as FilePreview.Error).throwable.message}") + } + } + } + } +} + +@Composable +fun PdfFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.Default.PictureAsPdf) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + + var loadFilePreview by remember { mutableStateOf(false) } + val filePreview by loadRawFileContent(file.uri, LocalContext.current, loadFilePreview) + + Spacer(modifier = Modifier.height(16.dp)) + when (filePreview) { + FilePreview.NotLoadedYet -> { + Button(onClick = { loadFilePreview = true }) { + Text("Display first 10 bytes") + } + } + + FilePreview.Loading -> { + Text("Loading...") + } + + is FilePreview.Loaded -> { + Text("First 10 bytes: ${(filePreview as FilePreview.Loaded).content}") + } + + is FilePreview.Error -> { + Text("Error: ${(filePreview as FilePreview.Error).throwable.message}") + } + } + } + } +} + +@Composable +fun BinaryFileCard(file: FileRecord, contentPreview: @Composable (() -> Unit)? = null) { + FileCard(file, Icons.AutoMirrored.Filled.InsertDriveFile) { + if (contentPreview != null) { + Spacer(modifier = Modifier.height(16.dp)) + contentPreview() + } else { + Spacer(modifier = Modifier.height(16.dp)) + + var loadFilePreview by remember { mutableStateOf(false) } + val filePreview by loadRawFileContent(file.uri, LocalContext.current, loadFilePreview) + + Spacer(modifier = Modifier.height(16.dp)) + when (filePreview) { + FilePreview.NotLoadedYet -> { + Button(onClick = { loadFilePreview = true }) { + Text("Display first 10 bytes") + } + } + + FilePreview.Loading -> { + Text("Loading...") + } + + is FilePreview.Loaded -> { + Text("First 10 bytes: ${(filePreview as FilePreview.Loaded).content}") + } + + is FilePreview.Error -> { + Text("Error: ${(filePreview as FilePreview.Error).throwable.message}") + } + } + } + } +} + +@Preview +@Composable +fun ImageFileCard_Preview() { + ImageFileCard( + FileRecord( + Uri.EMPTY, + "AmazingPhoto.png", + 345_000, + "image/png", + FileType.Image, + ), + ) +} + +@Preview +@Composable +fun VideoFileCard_Preview() { + VideoFileCard( + FileRecord( + Uri.EMPTY, + "All hands - meeting recording.mp4", + 1_234_567_890, + "video/mp4", + FileType.Video, + ), + ) +} + +@Preview +@Composable +fun AudioFileCard_Preview() { + AudioFileCard( + FileRecord( + Uri.EMPTY, + "Queen - We will rock you.mp3", + 5_432_100, + "audio/mp3", + FileType.Audio, + ), + ) +} + +@Preview +@Composable +fun TextFileCard_Preview() { + TextFileCard( + FileRecord( + Uri.EMPTY, + "Android Jetpack Compose.txt", + 5_678, + "text/plain", + FileType.Text, + ), + ) +} + +@Preview +@Composable +fun PdfFileCard_Preview() { + PdfFileCard( + FileRecord( + Uri.EMPTY, + "Android Jetpack Compose.pdf", + 1_234_567, + "application/pdf", + FileType.Pdf, + ), + ) +} + +@Preview +@Composable +fun BinaryFileCard_Preview() { + BinaryFileCard( + FileRecord( + Uri.EMPTY, + "binary.bin", + 78_420_968, + "application/octet-stream", + FileType.Any, + ), + ) +} + +sealed interface FilePreview { + data object NotLoadedYet : FilePreview + data object Loading : FilePreview + + @JvmInline + value class Loaded(val content: String) : FilePreview + + @JvmInline + value class Error(val throwable: Throwable) : FilePreview +} + +@Composable +fun loadTextFileContent( + uri: Uri, + context: Context, + loadContent: Boolean = false, + numberOfChars: Int = 300, +): State { + return produceState(FilePreview.NotLoadedYet, uri, loadContent, numberOfChars) { + withContext(Dispatchers.IO) { + if (!loadContent) { + return@withContext + } + + value = FilePreview.Loading + + context.contentResolver.openInputStream(uri)?.use { inputStream -> + BufferedReader(InputStreamReader(inputStream)).use { reader -> + val buffer = CharArray(numberOfChars) + val charsRead = reader.read(buffer) + + value = if (charsRead > 0) { + FilePreview.Loaded(String(buffer, 0, charsRead)) + } else { + FilePreview.Error(Exception("End of file or no characters available.")) + } + } + } ?: run { + value = FilePreview.Error(Exception("Failed to open InputStream")) + } + } + } +} + +@Composable +fun loadRawFileContent( + uri: Uri, + context: Context, + loadContent: Boolean = false, + numberOfBytes: Int = 10, +): State { + return produceState(FilePreview.NotLoadedYet, uri, loadContent, numberOfBytes) { + withContext(Dispatchers.IO) { + if (!loadContent) { + return@withContext + } + + value = FilePreview.Loading + + context.contentResolver.openInputStream(uri)?.use { inputStream -> + val buffer = ByteArray(numberOfBytes) + val bytesRead = inputStream.read(buffer) + + value = if (bytesRead > 0) { + FilePreview.Loaded(buffer.joinToString(" | ") { byte -> byte.toString() }) + } else { + FilePreview.Error(Exception("End of InputStream or no bytes available")) + } + } ?: run { + value = FilePreview.Error(Exception("Failed to open InputStream")) + } + } + } +} \ No newline at end of file diff --git a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileRecord.kt b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/shared/FileRecord.kt similarity index 91% rename from samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileRecord.kt rename to samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/shared/FileRecord.kt index 5dcb474d..71cb83cc 100644 --- a/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/FileRecord.kt +++ b/samples/storage/src/main/java/com/example/platform/storage/storageaccessframework/shared/FileRecord.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.platform.storage.storageaccessframework +package com.example.platform.storage.storageaccessframework.shared import android.content.Context import android.net.Uri @@ -22,7 +22,13 @@ import android.provider.OpenableColumns import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -data class FileRecord(val name: String, val size: Long, val mimeType: String, val fileType: FileType) { +data class FileRecord( + val uri: Uri, + val name: String, + val size: Long, + val mimeType: String, + val fileType: FileType, +) { companion object { suspend fun fromUri(uri: Uri, context: Context): FileRecord? = withContext(Dispatchers.IO) { val mimeType = context.contentResolver.getType(uri) ?: return@withContext null @@ -54,6 +60,7 @@ data class FileRecord(val name: String, val size: Long, val mimeType: String, va } return@use FileRecord( + uri = uri, name = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)), size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)), mimeType = mimeType,