SimpleStorage

repository·master·Indexed 21 days ago

https://github.com/anggrayudi/simplestorage

An Android library designed to simplify file access and management across different API levels, specifically addressing the Storage Access Framework (SAF) and Scoped Storage. It provides unified abstractions like StorageFile, DocumentFileCompat, and MediaStoreCompat, along with a Jetpack Compose extension for handling storage permissions and file picking. Version 3.0.0-beta03 introduces a redesigned API with one-shot suspend operations and the StorageAccessManager.

Tokens
11.1K
Snippets
27
Records
36
Agent score
73%

What's inside SimpleStorage

  1. Overview of SimpleStorage v3 (beta)

    master

    Version 3.0.0-beta03 introduces a redesigned API focused on modern Android development:

    • StorageFile: A unified abstraction over DocumentFile, MediaFile, and java.io.File.
    • One-shot suspend operations: Methods like copyTo, moveTo, zipTo, and unzipTo return a unified TransferResult.
    • StorageAccessManager: Replaces SimpleStorageHelper for managing storage access.
    • Targeting: Targets Android 17 (API 37) with a minimum SDK of 26.

    Note: The 2.x API remains compatible during the 3.x lifecycle.

  2. How StorageFile works

    master

    A StorageFile is the unified interface for all file types in SimpleStorage v3. It abstracts away the differences between SAF (DocumentFile), MediaStore (MediaFile), and standard java.io.File.

    Key Properties:

    • name, mimeType, length, isDirectory, exists, lastModified, canRead, canWrite
    • absolutePath and path: Return null if the file has no resolvable physical path.

    Key Methods:

    • list(): Lists contents.
    • child(String): Accesses a sub-path.
    • openInputStream() / openOutputStream(): Standard I/O access.
    • asDocumentFile(), asMediaFile(), asRawFile(): Escape hatches to access the underlying implementation.
    // Creating StorageFiles from various sources:
    val a = StorageFile.from(context, uri)                  // SAF, file://, or MediaStore URI
    val b = StorageFile.from(context, File("/storage/emulated/0/Download/movie.mp4"))
    val c = StorageFile.fromPath(context, "/storage/emulated/0/Download/movie.mp4")
    val d = StorageFile.fromPath(context, StoragePath(storageId = "AAAA-BBBB", basePath = "Download/movie.mp4"))
    val e = StorageFile.fromPublicDirectory(context, PublicDirectory.DOWNLOADS, "movie.mp4")
    
    // Conversions from 2.x:
    val f = documentFile.toStorageFile(context)
    val g = mediaFile.toStorageFile(context)
  3. Manage files with DocumentFile and MediaFile

    master

    SimpleStorage provides extension functions to manage files across different storage abstractions.

    DocumentFile

    Used for general file management when full storage access is granted. Key functions include:

    • getStorageId(), getStorageType(), getBasePath()
    • copyFileTo(), moveTo()
    • search(), deleteRecursively()
    • getProperties(), openOutputStream()

    MediaFile

    Used for media files via MediaStore. Key functions include:

    • absolutePath, isPending
    • delete(), renameTo()
    • copyFileTo(), moveFileTo()
    • openInputStream(), openOutputStream()
  4. Handle empty file paths for SingleDocumentFile

    master

    Methods like getAbsolutePath() or getBasePath() may return an empty string if the DocumentFile is an instance of androidx.documentfile.provider.SingleDocumentFile (common with content:// URIs from Downloads or Media providers).

    To handle this:

    1. Check for direct path availability: Use the DocumentFile.isTreeDocumentFile extension function. If it returns true, the file has a guaranteed direct file path.
    2. Conversion: You can attempt to convert a SingleDocumentFile to a MediaFile and use MediaFile.absolutePath.
    3. Recommended Approach: Avoid using direct file paths for file management (reading, uploading, or importing). Instead, use the Uri.openInputStream() extension function for both DocumentFile and MediaFile to work with URIs as intended by Android OS.
  5. Migrate from SimpleStorage 2.x to 3.0

    master

    Version 3.0 introduces a unified abstraction (StorageFile) over Android's different file systems and a consistent vocabulary for long-running operations. While the 2.x API remains available (some parts marked @Deprecated) to allow for incremental migration, 3.0 simplifies threading and scope management.

    Platform Requirements for 3.0

    • minSdk: 26
    • compileSdk / targetSdk: 37 (Android 17)

    Key Improvements

    • Unified Abstraction: Use StorageFile instead of managing DocumentFile or MediaFile separately.
    • Simplified Threading: Operations in 3.0 can be called from any thread without manual scope juggling (e.g., no need to pass uiScope to conflict callbacks).
    • Typed Results: Operations return TransferResult, which provides typed success (TransferResult.Success<StorageFile>) and failure (TransferResult.Failure) states.
    // 2.x approach (requires scope juggling and complex callbacks)
    ioScope.launch {
      file.copyFileTo(context, targetFolder, 
        onConflict = object : SingleFileConflictCallback<DocumentFile>(uiScope) {
          override fun onFileConflict(destFile: DocumentFile, action: FileConflictAction) {
            action.confirmResolution(ConflictResolution.REPLACE)
          }
        }
      ).collect { /* ... */ }
    }
    
    // 3.0 approach (cleaner, works from any thread)
    val result = file.copyTo(targetFolder) {
      onConflict { ConflictResolution.REPLACE }
      onProgress { progressBar.progress = it.percent.toInt() }
    }
    when (result) {
      is TransferResult.Success -> toast("Copied ${result.result.name}")
      is TransferResult.Failure -> log(result.errorCode, result.cause)
    }
  6. Install SimpleStorage v3 (beta)

    master

    To use SimpleStorage 3.0.0-beta03, add the following dependencies to your project. Note that all operations require Kotlin coroutines.

    Requirements:

    • minSdk: 26
    • Compiled against: API 37 (Android 17)
    • Kotlin Coroutines: Required for all operations.
    // Core library
    implementation "com.anggrayudi:storage:3.0.0-beta03"
    
    // For Jetpack Compose support
    implementation "com.anggrayudi:storage-compose:3.0.0-beta03"
  7. Use SimpleStorage in Java projects

    master
    SimpleStorage is built in Kotlin, but it is fully compatible with Java. You can call Kotlin functions as standard Java methods. Refer to the JAVA_COMPATIBILITY.md file in the repository for specific details on interoperability.
  8. Use SimpleStorage in Jetpack Compose

    master

    For Jetpack Compose projects, use SimpleStorageCompose.kt to handle storage permissions and file picking via composable functions. This avoids manual handling of storage access and permission dialogs.

    Available composable functions:

    • rememberLauncherForStoragePermission()
    • rememberLauncherForStorageAccess()
    • rememberLauncherForFolderPicker()
    • rememberLauncherForFilePicker()
    • rememberLauncherForFileCreation()

    If the default UI behavior does not suit your requirements, you can implement custom logic by referencing SimpleStorageResultContracts.kt.

  9. Use Activity Result Contracts for storage operations

    master

    If you prefer the modern ActivityResultLauncher pattern over SimpleStorageHelper, use the contracts provided in SimpleStorageResultContracts.kt. This approach avoids manual lifecycle management of a helper instance.

    Available Contracts:

    • RequestStorageAccessContract
    • StoragePermissionContract
    • FileCreationContract
    • OpenFilePickerContract
    • OpenFolderPickerContract

    Example: Requesting Storage Access When using RequestStorageAccessContract, the result can be one of:

    • RequestStorageAccessResult.RootPathNotSelected
    • RequestStorageAccessResult.ExpectedStorageNotSelected
    • RequestStorageAccessResult.RootPathPermissionGranted
    val contract = RequestStorageAccessContract(
        expectedStorageId = StorageId.PRIMARY,
        expectedBasePath = "Documents"
    )
    
    requestStorageAccessLauncher = registerForActivityResult(contract) { result -> 
      when (result) {
        is RequestStorageAccessResult.RootPathNotSelected -> { /* ... */ }
        is RequestStorageAccessResult.ExpectedStorageNotSelected -> { /* ... */ }
        is RequestStorageAccessResult.RootPathPermissionGranted -> { /* ... */ }
      }
    }
    
    // To launch:
    val options = RequestStorageAccessContract.Options(
      initialPath = FileFullPath(
        baseContext,
        storageId = StorageId.PRIMARY,
        basePath = "Documents"
      )
    )
    requestStorageAccessLauncher.launch(options)
  10. Request storage access using SimpleStorageHelper

    master

    Even with runtime permissions, your app may lack full storage access (needed for searching, moving, or copying). Use SimpleStorageHelper to facilitate the Storage Access Framework (SAF) flow.

    Steps to implement:

    1. Initialize SimpleStorageHelper(context).
    2. Set up required callbacks: onStorageAccessGranted, onFolderSelected, onFileSelected, and onFileCreated.
    3. Call the request methods (e.g., requestStorageAccess(), openFolderPicker(), openFilePicker(), or createFile()).
    4. Mandatory Lifecycle Handling:
      • For AppCompatActivity or ComponentActivity, you must call storageHelper.storage.onActivityResult(...) and storageHelper.onRequestPermissionsResult(...) in their respective overrides.
      • You must also call storageHelper.onSaveInstanceState(outState) and storageHelper.onRestoreInstanceState(savedInstanceState) to maintain state.
    class MainActivity : AppCompatActivity() {
        private val storageHelper = SimpleStorageHelper(this)
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            
            storageHelper.onStorageAccessGranted = { requestCode, root -> /* handle access */ }
            storageHelper.onFolderSelected = { requestCode, folder -> /* handle folder */ }
            storageHelper.onFileSelected = { requestCode, files -> /* handle files */ }
            storageHelper.onFileCreated = { requestCode, file -> /* handle file */ }
    
            btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }
            btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }
            btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }
            btnCreateFile.setOnClickListener { storageHelper.createFile("text/plain", "Test create file") }
        }
    
        override fun onSaveInstanceState(outState: Bundle) {
            storageHelper.onSaveInstanceState(outState)
            super.onSaveInstanceState(outState)
        }
    
        override fun onRestoreInstanceState(savedInstanceState: Bundle) {
            storageHelper.onRestoreInstanceState(savedInstanceState)
            super.onRestoreInstanceState(savedInstanceState)
        }
    
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            storageHelper.storage.onActivityResult(requestCode, resultCode, data)
        }
    
        override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
        }
    }
  11. Use SimpleStorage in Java projects

    master

    SimpleStorage is written in Kotlin but is compatible with Java. To use it in a Java project, you must interact with Kotlin's object classes and extension functions using specific Java syntax.

    Accessing Utility Functions

    Utility functions are stored in Kotlin object classes like DocumentFileCompat and MediaStoreCompat.

    In Java, you must append .INSTANCE to the class name to access these functions, unless the function is annotated with @JvmStatic.

    Accessing Extension Functions

    Kotlin extension functions (e.g., those in DocumentFileExtKt or FileExtKt) are available in Java as static methods. Since version 0.4.2, these are typically accessed via classes renamed with the Utils suffix (e.g., DocumentFileUtils).

    Version Compatibility Warning

    Important: Long-running functions such as copy, move, search, compress, and unzip are only available in Kotlin. If you require these features in a Java project, you must use version 1.5.6 or earlier, as it is the last version providing Java support for these specific operations.

    // Accessing a utility function from a Kotlin object
    DocumentFile file = DocumentFileCompat.INSTANCE.fromSimplePath(context, "AAAA-BBBB", "Music/My Love.mp3");
    
    // Accessing an extension function (renamed to Utils in Java)
    String storageId = DocumentFileUtils.getStorageId(file, context);
  12. Use SimpleStorage with Jetpack Compose

    master

    SimpleStorage provides specialized launchers for Jetpack Compose to handle pickers and permissions using the rememberLauncher... pattern.

    Available Launchers:

    • rememberLauncherForMediaPicker(maxItems): For the System Photo Picker.
    • rememberLauncherForStoragePermission()
    • rememberLauncherForStorageAccess()
    • rememberLauncherForFolderPicker()
    • rememberLauncherForFilePicker()
    • rememberLauncherForFileCreation()
    val mediaPicker = rememberLauncherForMediaPicker(maxItems = 5) { files: List<StorageFile> ->
      viewModel.onMediaPicked(files)
    }
    
    Button(onClick = { mediaPicker.launch() }) {
      Text("Pick photos")
    }