Skip to content
This repository has been archived by the owner on Feb 22, 2024. It is now read-only.

Commit

Permalink
Add support for file paths (#87)
Browse files Browse the repository at this point in the history
* Deprecate SharedFileSystem in favor of AndroidFileSystem

Add support for Photo Picker metadata fetching

* Add appendingSink implementation

* Add support for file paths (makes interactions with Internal Storage possible)

* Implement delete for physical files

Add more internal storage tests

* Enable (temporarily) tests on GitHub Actions

* Apply spotless

* Update sample to use AndroidFileSystem

Disable tests (they're getting cancelled on GitHub Actions for no reason...)

* Improve greatly platform testing coverage

Deprecate Uri.toPath in favor of Uri.toOkioPath
Add scanFile method

* Upgrade version number

Update storage guide

* Fix type selection in photo picker

* Fix metadata fetchinf for photo picker URIs

* Updating storage guide

* Fix deprecation issue for the old createMediaStoreUri method

Update sample to use new createMediaStoreUri method

* Upgrade AGP to 7.1.1
  • Loading branch information
yrezgui authored Feb 7, 2022
1 parent 215e61a commit 5acfe8a
Show file tree
Hide file tree
Showing 16 changed files with 1,127 additions and 108 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.1.0")
classpath("com.android.tools.build:gradle:7.1.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")

// NOTE: Do not place your application dependencies here; they belong
Expand Down
31 changes: 22 additions & 9 deletions docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ artifact: "modernstorage-storage"

# Storage Interactions

`{{ artifact }}` is a library abstracting interactions on the Android shared storage using the
library [Okio][okio_website]. It relies on its [FileSystem][okio_filesystem_guide] API, which
provides a [set of methods][okio_filesystem_api] to read and write files.
`{{ artifact }}` is a library abstracting storage interactions on Android using the library
[Okio][okio_website]. It relies on its [FileSystem][okio_filesystem_guide] API, which provides a
[set of methods][okio_filesystem_api] to read and write files.

Instead of opening an `InputStream` or `OutputStream` and relies on different APIs to get file
metadata for MediaStore and Storage Access Framework `DocumentProvider`, this library takes
Expand All @@ -33,16 +33,29 @@ To interact with the [FileSystem][okio_filesystem_guide] API, you need to initia
first:

```kotlin
import com.google.modernstorage.storage.SharedFileSystem
import com.google.modernstorage.storage.AndroidFileSystem

val fileSystem = SharedFileSystem(context)
val fileSystem = AndroidFileSystem(context)
```

## Get Path from Uri
Call `toPath` to get a `Path` from a `Uri`:
Call `toOkioPath` to get a `Path` from a `Uri`:

```kotlin
val path = uri.toPath()
val path = uri.toOkioPath()
```

## Get Path from File
Call `toOkioPath` to get a `Path` from a `File`:

```kotlin
val path = File(context.filesDir, "myfile.jpg").toOkioPath()
```

## Copy a file
You can easily copy a file to another location by using the `copy` method:
```kotlin
fileSystem.copy(originPath, targetPath)
```

## Get file metadata
Expand All @@ -52,7 +65,7 @@ You can get the file size by using the method `metadataOrNull`:
import com.google.modernstorage.storage.MetadataExtras.DisplayName
import com.google.modernstorage.storage.MetadataExtras.MimeType

val fileMetadata = fileSystem.metadataOrNull(uri.toPath())
val fileMetadata = fileSystem.metadataOrNull(uri.toOkioPath())
Log.d("ModernStorage/uri", uri.toString())
Log.d("ModernStorage/isRegularFile", metadata.isRegularFile.toString())
Log.d("ModernStorage/isDirectory", metadata.isDirectory.toString())
Expand All @@ -71,7 +84,7 @@ Log.d("ModernStorage/mimeType", metadata.extra(MimeType::class).value)
val actionOpenTextFile = registerForActivityResult(OpenDocument()) { uri ->
if(uri != null) {
// textPath is an instance of okio.Path
val textPath = uri.toPath()
val textPath = uri.toOkioPath()
Log.d("ModernStorage/metadata", fileSystem.metadataOrNull(textPath).toString())
Log.d("ModernStorage/content", fileSystem.source(textPath).buffer().readUtf8())
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
GROUP=com.google.modernstorage
VERSION_NAME=1.0.0-alpha05
VERSION_NAME=1.0.0-alpha06
POM_DESCRIPTION=Utility libraries for storage interactions on Android
POM_URL=https://github.com/google/modernstorage/
POM_SCM_URL=https://github.com/google/modernstorage/
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ plugins:
- macros

extra:
lib_version: "1.0.0-alpha05"
lib_version: "1.0.0-alpha06"
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,10 @@ class PhotoPicker : ActivityResultContract<PhotoPicker.Args, List<Uri>>() {
putExtra(EXTRA_PICK_IMAGES_MAX, input.maxItems)
}

when (input.type) {
Type.IMAGES_ONLY ->
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
Type.VIDEO_ONLY ->
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*"))
Type.IMAGES_AND_VIDEO ->
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
if (input.type == Type.IMAGES_ONLY) {
type = "image/*"
} else if (input.type == Type.VIDEO_ONLY) {
type = "video/*"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,23 @@ package com.google.modernstorage.sample.mediastore

import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.modernstorage.sample.ui.shared.FileDetails
import com.google.modernstorage.storage.SharedFileSystem
import com.google.modernstorage.storage.toPath
import com.google.modernstorage.storage.AndroidFileSystem
import com.google.modernstorage.storage.toOkioPath
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import okio.buffer
import okio.source
import java.io.File

class MediaStoreViewModel(application: Application) : AndroidViewModel(application) {
private val context: Context get() = getApplication()
private val fileSystem = SharedFileSystem(context)
private val fileSystem = AndroidFileSystem(context)

private val _addedFile = MutableStateFlow<FileDetails?>(null)
val addedFile: StateFlow<FileDetails?> = _addedFile
Expand All @@ -45,32 +47,45 @@ class MediaStoreViewModel(application: Application) : AndroidViewModel(applicati

fun addMedia(type: MediaType) {
viewModelScope.launch {
val extension = when (type) {
MediaType.IMAGE -> "jpg"
MediaType.VIDEO -> "mp4"
MediaType.AUDIO -> "wav"
}

val mimeType = when (type) {
MediaType.IMAGE -> "image/jpeg"
MediaType.VIDEO -> "video/mp4"
MediaType.AUDIO -> "audio/wav"
}

val directory = when (type) {
MediaType.IMAGE -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
MediaType.VIDEO -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
MediaType.AUDIO -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
val extension: String
val mimeType: String
val collection: Uri
val directory: File

when (type) {
MediaType.IMAGE -> {
extension = "jpg"
mimeType = "image/jpeg"
collection = MediaStore.Images.Media.getContentUri("external")
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
}
MediaType.VIDEO -> {
extension = "mp4"
mimeType = "video/mp4"
collection = MediaStore.Images.Media.getContentUri("external")
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
}
MediaType.AUDIO -> {
extension = "wav"
mimeType = "audio/x-wav"
collection = MediaStore.Images.Media.getContentUri("external")
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
}
}

val uri = fileSystem.createMediaStoreUri(
filename = "added-${System.currentTimeMillis()}.$extension",
collection = collection,
directory = directory.absolutePath
) ?: return@launch clearAddedFile()

val path = uri.toPath()
val path = uri.toOkioPath()

fileSystem.sink(path).buffer().writeAll(context.assets.open("sample.$extension").source())
fileSystem.write(path, false) {
context.assets.open("sample.$extension").source().use { source ->
writeAll(source)
}
}
fileSystem.scanUri(uri, mimeType)

val metadata = fileSystem.metadataOrNull(path) ?: return@launch clearAddedFile()
Expand All @@ -84,26 +99,37 @@ class MediaStoreViewModel(application: Application) : AndroidViewModel(applicati

fun addDocument(type: DocumentType) {
viewModelScope.launch {
val extension = when (type) {
DocumentType.TEXT -> "txt"
DocumentType.PDF -> "pdf"
DocumentType.ZIP -> "zip"
}

val mimeType = when (type) {
DocumentType.TEXT -> "text/plain"
DocumentType.PDF -> "application/pdf"
DocumentType.ZIP -> "application/zip"
val extension: String
val mimeType: String

when (type) {
DocumentType.TEXT -> {
extension = "txt"
mimeType = "text/plain"
}
DocumentType.PDF -> {
extension = "pdf"
mimeType = "application/pdf"
}
DocumentType.ZIP -> {
extension = "zip"
mimeType = "application/zip"
}
}

val uri = fileSystem.createMediaStoreUri(
filename = "added-${System.currentTimeMillis()}.$extension",
collection = MediaStore.Files.getContentUri("external"),
directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
) ?: return@launch clearAddedFile()
)!!

val path = uri.toPath()
val path = uri.toOkioPath()

fileSystem.sink(path).buffer().writeAll(context.assets.open("sample.$extension").source())
fileSystem.write(path, false) {
context.assets.open("sample.$extension").source().use { source ->
writeAll(source)
}
}
fileSystem.scanUri(uri, mimeType)

val metadata = fileSystem.metadataOrNull(path) ?: return@launch clearAddedFile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ import com.google.modernstorage.sample.HomeRoute
import com.google.modernstorage.sample.R
import com.google.modernstorage.sample.ui.shared.FileDetails
import com.google.modernstorage.sample.ui.shared.MediaPreviewCard
import com.google.modernstorage.storage.SharedFileSystem
import com.google.modernstorage.storage.toPath
import com.google.modernstorage.storage.AndroidFileSystem
import com.google.modernstorage.storage.toOkioPath

private const val IMAGE_MIMETYPE = "image/*"
private const val VIDEO_MIMETYPE = "video/*"
Expand All @@ -59,12 +59,12 @@ private const val VIDEO_MIMETYPE = "video/*"
@ExperimentalFoundationApi
@Composable
fun PickVisualMediaScreen(navController: NavController) {
val fileSystem = SharedFileSystem(LocalContext.current)
val fileSystem = AndroidFileSystem(LocalContext.current)
var selectedFiles by remember { mutableStateOf<List<FileDetails>>(emptyList()) }

val selectFile = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let { uri ->
val path = uri.toPath()
val path = uri.toOkioPath()
fileSystem.metadataOrNull(path)?.let { metadata ->
selectedFiles = listOf(FileDetails(uri, path, metadata))
}
Expand All @@ -73,15 +73,15 @@ fun PickVisualMediaScreen(navController: NavController) {

val selectMultipleFiles = rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
selectedFiles = uris.map { uri ->
val path = uri.toPath()
val path = uri.toOkioPath()
val metadata = fileSystem.metadataOrNull(path) ?: return@map null
FileDetails(uri, path, metadata)
}.filterNotNull()
}

val photoPicker = rememberLauncherForActivityResult(PhotoPicker()) { uris ->
selectedFiles = uris.map { uri ->
val path = uri.toPath()
val path = uri.toOkioPath()
val metadata = fileSystem.metadataOrNull(path) ?: return@map null
FileDetails(uri, path, metadata)
}.filterNotNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ import com.google.modernstorage.sample.HomeRoute
import com.google.modernstorage.sample.R
import com.google.modernstorage.sample.ui.shared.FileDetails
import com.google.modernstorage.sample.ui.shared.MediaPreviewCard
import com.google.modernstorage.storage.SharedFileSystem
import com.google.modernstorage.storage.toPath
import com.google.modernstorage.storage.AndroidFileSystem
import com.google.modernstorage.storage.toOkioPath

const val GENERIC_MIMETYPE = "*/*"
const val PDF_MIMETYPE = "application/pdf"
Expand All @@ -56,13 +56,13 @@ const val VIDEO_MIMETYPE = "video/*"
@ExperimentalFoundationApi
@Composable
fun SelectDocumentFileScreen(navController: NavController) {
val fileSystem = SharedFileSystem(LocalContext.current)
val fileSystem = AndroidFileSystem(LocalContext.current)
var selectedFile by remember { mutableStateOf<FileDetails?>(null) }

val selectFile =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri?.let { uri ->
val path = uri.toPath()
val path = uri.toOkioPath()
fileSystem.metadataOrNull(path)?.let { metadata ->
selectedFile = FileDetails(uri, path, metadata)
}
Expand Down
59 changes: 41 additions & 18 deletions storage/api/current.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
// Signature format: 4.0
package com.google.modernstorage.storage {

public final class AndroidFileSystem extends okio.FileSystem {
ctor public AndroidFileSystem(android.content.Context context);
method public okio.Sink appendingSink(okio.Path file, boolean mustExist);
method public void atomicMove(okio.Path source, okio.Path target);
method public okio.Path canonicalize(okio.Path path);
method public void createDirectory(okio.Path dir, boolean mustCreate);
method @Deprecated public android.net.Uri? createMediaStoreUri(String filename, String directory);
method public android.net.Uri? createMediaStoreUri(String filename, optional android.net.Uri collection, String? directory);
method public void createSymlink(okio.Path source, okio.Path target);
method public void delete(okio.Path path, boolean mustExist);
method public java.util.List<okio.Path> list(okio.Path dir);
method public java.util.List<okio.Path>? listOrNull(okio.Path dir);
method public okio.FileMetadata? metadataOrNull(okio.Path path);
method public okio.FileHandle openReadOnly(okio.Path file);
method public okio.FileHandle openReadWrite(okio.Path file, boolean mustCreate, boolean mustExist);
method public suspend Object? scanFile(java.io.File file, String mimeType, kotlin.coroutines.Continuation<? super android.net.Uri> p);
method public suspend Object? scanUri(android.net.Uri uri, String mimeType, kotlin.coroutines.Continuation<? super android.net.Uri> p);
method public okio.Sink sink(okio.Path file, boolean mustCreate);
method public okio.Source source(okio.Path file);
}

public final class MetadataExtras {
field public static final com.google.modernstorage.storage.MetadataExtras INSTANCE;
}
Expand All @@ -24,27 +45,29 @@ package com.google.modernstorage.storage {
}

public final class PathUtilsKt {
method public static okio.Path toPath(android.net.Uri);
method public static okio.Path toOkioPath(android.net.Uri);
method @Deprecated public static okio.Path toPath(android.net.Uri);
method public static android.net.Uri toUri(okio.Path);
}

public final class SharedFileSystem extends okio.FileSystem {
ctor public SharedFileSystem(android.content.Context context);
method public okio.Sink appendingSink(okio.Path file, boolean mustExist);
method public void atomicMove(okio.Path source, okio.Path target);
method public okio.Path canonicalize(okio.Path path);
method public void createDirectory(okio.Path dir, boolean mustCreate);
method public android.net.Uri? createMediaStoreUri(String filename, String directory);
method public void createSymlink(okio.Path source, okio.Path target);
method public void delete(okio.Path path, boolean mustExist);
method public java.util.List<okio.Path> list(okio.Path dir);
method public java.util.List<okio.Path>? listOrNull(okio.Path dir);
method public okio.FileMetadata? metadataOrNull(okio.Path path);
method public okio.FileHandle openReadOnly(okio.Path file);
method public okio.FileHandle openReadWrite(okio.Path file, boolean mustCreate, boolean mustExist);
method public suspend Object? scanUri(android.net.Uri uri, String mimeType, kotlin.coroutines.Continuation<? super android.net.Uri> p);
method public okio.Sink sink(okio.Path file, boolean mustCreate);
method public okio.Source source(okio.Path file);
@Deprecated public final class SharedFileSystem extends okio.FileSystem {
ctor @Deprecated public SharedFileSystem(android.content.Context context);
method @Deprecated public okio.Sink appendingSink(okio.Path file, boolean mustExist);
method @Deprecated public void atomicMove(okio.Path source, okio.Path target);
method @Deprecated public okio.Path canonicalize(okio.Path path);
method @Deprecated public void createDirectory(okio.Path dir, boolean mustCreate);
method @Deprecated public android.net.Uri? createMediaStoreUri(String filename, String directory);
method @Deprecated public android.net.Uri? createMediaStoreUri(String filename, String directory, optional android.net.Uri collection);
method @Deprecated public void createSymlink(okio.Path source, okio.Path target);
method @Deprecated public void delete(okio.Path path, boolean mustExist);
method @Deprecated public java.util.List<okio.Path> list(okio.Path dir);
method @Deprecated public java.util.List<okio.Path>? listOrNull(okio.Path dir);
method @Deprecated public okio.FileMetadata? metadataOrNull(okio.Path path);
method @Deprecated public okio.FileHandle openReadOnly(okio.Path file);
method @Deprecated public okio.FileHandle openReadWrite(okio.Path file, boolean mustCreate, boolean mustExist);
method @Deprecated public suspend Object? scanUri(android.net.Uri uri, String mimeType, kotlin.coroutines.Continuation<? super android.net.Uri> p);
method @Deprecated public okio.Sink sink(okio.Path file, boolean mustCreate);
method @Deprecated public okio.Source source(okio.Path file);
}

}
Expand Down
Loading

0 comments on commit 5acfe8a

Please sign in to comment.