{"id":13608377,"url":"https://github.com/anggrayudi/SimpleStorage","last_synced_at":"2025-04-12T17:31:23.579Z","repository":{"id":38537032,"uuid":"288174740","full_name":"anggrayudi/SimpleStorage","owner":"anggrayudi","description":"💾 Simplify Android Storage Access Framework for file management across API levels.","archived":false,"fork":false,"pushed_at":"2025-01-17T11:19:27.000Z","size":1055,"stargazers_count":833,"open_issues_count":10,"forks_count":99,"subscribers_count":21,"default_branch":"master","last_synced_at":"2025-04-06T00:04:25.884Z","etag":null,"topics":["android","android-10","android-storage","file","mediastore","saf","scoped-storage","storage","storage-access-framework","storage-manager"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anggrayudi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":null,"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":["paypal.me/hardiannicko","saweria.co/hardiannicko"]}},"created_at":"2020-08-17T12:29:50.000Z","updated_at":"2025-04-03T01:48:44.000Z","dependencies_parsed_at":"2023-10-20T17:17:22.362Z","dependency_job_id":"8a2ed534-3c31-420a-af63-ec46ac4aa12b","html_url":"https://github.com/anggrayudi/SimpleStorage","commit_stats":{"total_commits":218,"total_committers":10,"mean_commits":21.8,"dds":0.1009174311926605,"last_synced_commit":"cdab9945ccaeb6deae3906db3af98a87bc450e5f"},"previous_names":[],"tags_count":41,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anggrayudi%2FSimpleStorage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anggrayudi%2FSimpleStorage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anggrayudi%2FSimpleStorage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anggrayudi%2FSimpleStorage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anggrayudi","download_url":"https://codeload.github.com/anggrayudi/SimpleStorage/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248605055,"owners_count":21132101,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-10","android-storage","file","mediastore","saf","scoped-storage","storage","storage-access-framework","storage-manager"],"created_at":"2024-08-01T19:01:26.816Z","updated_at":"2025-04-12T17:31:20.306Z","avatar_url":"https://github.com/anggrayudi.png","language":"Kotlin","funding_links":["paypal.me/hardiannicko","saweria.co/hardiannicko"],"categories":["Kotlin"],"sub_categories":[],"readme":"# SimpleStorage\n![Maven Central](https://img.shields.io/maven-central/v/com.anggrayudi/storage.svg)\n[![Build Status](https://github.com/anggrayudi/SimpleStorage/workflows/Android%20CI/badge.svg)](https://github.com/anggrayudi/SimpleStorage/actions?query=workflow%3A%22Android+CI%22)\n\n### Table of Contents\n* [Overview](#overview)\n  + [Java Compatibility](#java-compatibility)\n* [Terminology](#terminology)\n* [Check Accessible Paths](#check-accessible-paths)\n* [Read Files](#read-files)\n  + [`DocumentFileCompat`](#documentfilecompat)\n    - [Example](#example)\n  + [`MediaStoreCompat`](#mediastorecompat)\n    - [Example](#example-1)\n* [Manage Files](#manage-files)\n  + [`DocumentFile`](#documentfile)\n  + [`MediaFile`](#mediafile)\n* [Request Storage Access, Pick Folder \u0026 Files, Request Create File, etc.](#request-storage-access-pick-folder--files-request-create-file-etc)\n* [Move \u0026 Copy: Files \u0026 Folders](#move--copy-files--folders)\n* [Search: Files \u0026 Folders](#search-files--folders)\n* [Compress \u0026 Unzip: Files \u0026 Folders](#compress--unzip-files--folders)\n  + [Compression](#compression)\n  + [Decompression](#decompression)\n* [FAQ](#faq)\n* [Other SimpleStorage Usage Examples](#other-simpleStorage-usage-examples)\n* [License](#license)\n\n## Overview\n\nThe more higher API level, the more Google restricted file access on Android storage.\nAlthough Storage Access Framework (SAF) is designed to secure user's storage from malicious apps,\nbut this makes us even more difficult in accessing files as a developer. Let's take an example where\n[`java.io.File` has been deprecated in Android 10](https://commonsware.com/blog/2019/06/07/death-external-storage-end-saga.html).\n\nSimple Storage ease you in accessing and managing files across API levels.\nIf you want to know more about the background of this library, please read this article:\n[Easy Storage Access Framework in Android with SimpleStorage](https://medium.com/@hardiannicko/easy-storage-access-framework-in-android-with-simplestorage-ec0a566f472c)\n\nAdding Simple Storage into your project is pretty simple:\n\n```groovy\nimplementation \"com.anggrayudi:storage:X.Y.Z\"\n```\n\nWhere `X.Y.Z` is the library version: ![Maven Central](https://img.shields.io/maven-central/v/com.anggrayudi/storage.svg)\n\nAll versions can be found [here](https://oss.sonatype.org/#nexus-search;gav~com.anggrayudi~storage~~~~kw,versionexpand).\nTo use `SNAPSHOT` version, you need to add this URL to the root Gradle:\n\n```groovy\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        // add this line\n        maven { url \"https://oss.sonatype.org/content/repositories/snapshots\" }\n    }\n}\n```\n\n### Java Compatibility\n\nSimple Storage is built in Kotlin. Follow this [documentation](JAVA_COMPATIBILITY.md) to use it in your Java project.\n\nNote that some long-running functions like copy, move, search, compress, and unzip are now only available in Kotlin.\nThey are powered by Kotlin Coroutines \u0026 Flow, which are easy to use.\nYou can still use these Java features in your project, but you will need [v1.5.6](https://github.com/anggrayudi/SimpleStorage/releases/tag/1.5.6) which is the latest version that\nsupports Java.\n\n## Terminology\n\n![Alt text](art/terminology.png?raw=true \"Simple Storage Terms\")\n\n### Other Terminology\n* Storage Permission – related to [runtime permissions](https://developer.android.com/training/permissions/requesting)\n* Storage Access – related to [URI permissions](https://developer.android.com/reference/android/content/ContentResolver#takePersistableUriPermission(android.net.Uri,%20int))\n\n## Check Accessible Paths\n\nTo check whether you have access to particular paths, call `DocumentFileCompat.getAccessibleAbsolutePaths()`. The results will look like this in breakpoint:\n\n![Alt text](art/getAccessibleAbsolutePaths.png?raw=true \"DocumentFileCompat.getAccessibleAbsolutePaths()\")\n\nAll paths in those locations are accessible via functions `DocumentFileCompat.from*()`, otherwise your action will be denied by the system if you want to\naccess paths other than those, then functions `DocumentFileCompat.from*()` (next section) will return null as well. On API 28-, you can obtain it by requesting\nthe runtime permission. For API 29+, it is obtained automatically by calling `SimpleStorageHelper#requestStorageAccess()` or\n`SimpleStorageHelper#openFolderPicker()`. The granted paths are persisted by this library via `ContentResolver#takePersistableUriPermission()`,\nso you don't need to remember them in preferences:\n```kotlin\nbuttonSelectFolder.setOnClickListener {\n    storageHelper.openFolderPicker()\n}\n\nstorageHelper.onFolderSelected = { requestCode, folder -\u003e\n    // tell user the selected path\n}\n```\n\nIn the future, if you want to write files into the granted path, use `DocumentFileCompat.fromFullPath()`:\n```kotlin\nval grantedPaths = DocumentFileCompat.getAccessibleAbsolutePaths(this)\nval path = grantedPaths.values.firstOrNull()?.firstOrNull() ?: return\nval folder = DocumentFileCompat.fromFullPath(this, path, requiresWriteAccess = true)\nval file = folder?.makeFile(this, \"notes\", \"text/plain\")\n```\n\n## Read Files\n\nIn Simple Storage, `DocumentFile` is used to access files when your app has been granted full storage access,\nincluded URI permissions for read and write. Whereas `MediaFile` is used to access media files from `MediaStore`\nwithout URI permissions to the storage.\n\nYou can read file with helper functions in `DocumentFileCompat` and `MediaStoreCompat`:\n\n### `DocumentFileCompat`\n\n* `DocumentFileCompat.fromFullPath()`\n* `DocumentFileCompat.fromSimplePath()`\n* `DocumentFileCompat.fromFile()`\n* `DocumentFileCompat.fromPublicFolder()`\n\n#### Example\n```kotlin\nval fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = \"Download/MyMovie.mp4\")\n\nval fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = \"9016-4EF8\", basePath = \"Download/MyMovie.mp4\")\n```\n\n### `MediaStoreCompat`\n\n* `MediaStoreCompat.fromMediaId()`\n* `MediaStoreCompat.fromFileName()`\n* `MediaStoreCompat.fromRelativePath()`\n* `MediaStoreCompat.fromFileNameContains()`\n* `MediaStoreCompat.fromMimeType()`\n* `MediaStoreCompat.fromMediaType()`\n\n#### Example\n```kotlin\nval myVideo = MediaStoreCompat.fromFileName(context, MediaType.DOWNLOADS, \"MyMovie.mp4\")\n\nval imageList = MediaStoreCompat.fromMediaType(context, MediaType.IMAGE)\n```\n\n## Manage Files\n\n### `DocumentFile`\n\nSince `java.io.File` has been deprecated in Android 10, thus you have to use `DocumentFile` for file management.\n\nSimple Storage adds Kotlin extension functions to `DocumentFile`, so you can manage files like this:\n* `DocumentFile.getStorageId()`\n* `DocumentFile.getStorageType()`\n* `DocumentFile.getBasePath()`\n* `DocumentFile.copyFileTo()`\n* `List\u003cDocumentFile\u003e.moveTo()`\n* `DocumentFile.search()`\n* `DocumentFile.deleteRecursively()`\n* `DocumentFile.getProperties()`\n* `DocumentFile.openOutputStream()`, and many more…\n\n### `MediaFile`\n\nFor media files, you can have similar capabilities to `DocumentFile`, i.e.:\n* `MediaFile.absolutePath`\n* `MediaFile.isPending`\n* `MediaFile.delete()`\n* `MediaFile.renameTo()`\n* `MediaFile.copyFileTo()`\n* `MediaFile.moveFileTo()`\n* `MediaFile.openInputStream()`\n* `MediaFile.openOutputStream()`, etc.\n\n## Request Storage Access, Pick Folder \u0026 Files, Request Create File, etc.\n\nAlthough user has granted read and write permissions during runtime, your app may still does not have full access to the storage,\nthus you cannot search, move and copy files. You can check whether you have the storage access via `SimpleStorage.hasStorageAccess()` or\n`DocumentFileCompat.getAccessibleAbsolutePaths()`.\n\nTo enable full storage access, you need to open SAF and let user grant URI permissions for read and write access.\nThis library provides you an helper class named `SimpleStorageHelper` to ease the request process:\n\n```kotlin\nclass MainActivity : AppCompatActivity() {\n\n    private val storageHelper = SimpleStorageHelper(this)\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        // Only setup required callbacks, based on your need:\n        storageHelper.onStorageAccessGranted = { requestCode, root -\u003e\n            // do stuff\n        }\n        storageHelper.onFolderSelected = { requestCode, folder -\u003e\n            // do stuff\n        }\n        storageHelper.onFileSelected = { requestCode, files -\u003e\n            // do stuff\n        }\n        storageHelper.onFileCreated = { requestCode, file -\u003e\n            // do stuff\n        }\n\n        // Depends on your actions:\n        btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }\n        btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }\n        btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }\n        btnCreateFile.setOnClickListener { storageHelper.createFile(\"text/plain\", \"Test create file\") }\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        storageHelper.onSaveInstanceState(outState)\n        super.onSaveInstanceState(outState)\n    }\n\n    override fun onRestoreInstanceState(savedInstanceState: Bundle) {\n        super.onRestoreInstanceState(savedInstanceState)\n        storageHelper.onRestoreInstanceState(savedInstanceState)\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        // Mandatory for direct subclasses of android.app.Activity,\n        // but not for subclasses of androidx.fragment.app.Fragment, androidx.activity.ComponentActivity, androidx.appcompat.app.AppCompatActivity\n        storageHelper.storage.onActivityResult(requestCode, resultCode, data)\n    }\n\n    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array\u003cString\u003e, grantResults: IntArray) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        // Mandatory for direct subclasses of android.app.Activity,\n        // but not for subclasses of androidx.fragment.app.Fragment, androidx.activity.ComponentActivity, androidx.appcompat.app.AppCompatActivity\n        storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)\n    }\n}\n```\n\nSimple, right?\n\nThis helper class contains default styles for managing storage access.\nIf you want to use custom dialogs for `SimpleStorageHelper`, just copy the logic from this class.\n\n## Move \u0026 Copy: Files \u0026 Folders\n\nSimple Storage helps you in copying/moving files \u0026 folders via:\n* `DocumentFile.copyFileTo()`\n* `DocumentFile.moveFileTo()`\n* `DocumentFile.copyFolderTo()`\n* `DocumentFile.moveFolderTo()`\n\nFor example, you can move a folder with few lines of code:\n\n```kotlin\nval folder: DocumentFile = ...\nval targetFolder: DocumentFile = ...\n\nval job = ioScope.launch {\n  folder.moveFolderTo(applicationContext, targetFolder, skipEmptyFiles = false, updateInterval = 1000, onConflict = object : FolderConflictCallback(uiScope) {\n    override fun onParentConflict(destinationFolder: DocumentFile, action: ParentFolderConflictAction, canMerge: Boolean) {\n      handleParentFolderConflict(destinationFolder, action, canMerge)\n    }\n\n    override fun onContentConflict(\n      destinationFolder: DocumentFile,\n      conflictedFiles: MutableList\u003cFileConflict\u003e,\n      action: FolderContentConflictAction\n    ) {\n      handleFolderContentConflict(action, conflictedFiles)\n    }\n  }).onCompletion {\n    if (it is CancellationException) {\n      Timber.d(\"Folder move is aborted\")\n    }\n  }.collect { result -\u003e\n    when (result) {\n      is FolderResult.Validating -\u003e Timber.d(\"Validating...\")\n      is FolderResult.Preparing -\u003e Timber.d(\"Preparing...\")\n      is FolderResult.CountingFiles -\u003e Timber.d(\"Counting files...\")\n      is FolderResult.DeletingConflictedFiles -\u003e Timber.d(\"Deleting conflicted files...\")\n      is FolderResult.Starting -\u003e Timber.d(\"Starting...\")\n      is FolderResult.InProgress -\u003e Timber.d(\"Progress: ${result.progress.toInt()}% | ${result.fileCount} files\")\n      is FolderResult.Completed -\u003e uiScope.launch {\n        Timber.d(\"Completed: ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files\")\n        Toast.makeText(baseContext, \"Moved ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files\", Toast.LENGTH_SHORT).show()\n      }\n\n      is FolderResult.Error -\u003e uiScope.launch {\n        Timber.e(result.errorCode.name)\n        Toast.makeText(baseContext, \"An error has occurred: ${result.errorCode.name}\", Toast.LENGTH_SHORT).show()\n      }\n    }\n  }\n}\n\n// call this function somewhere, for example in a dialog with a cancel button:\njob.cancel() // it will abort the process\n```\n\nThe coolest thing of this library is you can ask users to choose Merge, Replace, Create New, or Skip Duplicate folders \u0026 files\nwhenever a conflict is found via `onConflict()`. Here're screenshots of the sample code when dealing with conflicts:\n\n![Alt text](art/parent-folder-conflict.png?raw=true \"Parent Folder Conflict\")\n![Alt text](art/folder-content-conflict.png?raw=true \"Folder Content Conflict\")\n\nRead [`MainActivity`](sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt)\nfrom the sample code if you want to mimic above dialogs.\n\n## Search: Files \u0026 Folders\n\nYou can search files and folders by using `DocumentFile.search()` extension function:\n\n```kotlin\nioScope.launch {\n  val nameToFind = \"nicko\" // search files with name containing \"nicko\"\n  folder.search(recursive = true, regex = Regex(\"^.*$nameToFind.*\\$\"), updateInterval = 1000).collect {\n    // update results every 1 second\n    Timber.d(\"Found ${it.size} files, last: ${it.lastOrNull()?.fullName}\")\n  }\n}\n```\n\n## Compress \u0026 Unzip: Files \u0026 Folders\n\n### Compression\n\nTo compress files and folders, use `List\u003cDocumentFile\u003e.compressToZip()` extension function:\n\n```kotlin\nioScope.launch {\n  // make sure you have an URI access to /storage/emulated/0/Documents, otherwise it will return null\n  val targetZipFile = DocumentFileCompat.createFile(baseContext, basePath = \"Documents/compress test.zip\", mimeType = \"application/zip\")\n  if (targetZipFile != null) {\n    listOf(folder).compressToZip(baseContext, targetZipFile, deleteSourceWhenComplete = false, updateInterval = 500).collect {\n      when (it) {\n        is ZipCompressionResult.CountingFiles -\u003e Timber.d(\"Calculating...\")\n        is ZipCompressionResult.Compressing -\u003e Timber.d(\"Compressing... ${it.progress.toInt()}%\")\n        is ZipCompressionResult.Completed -\u003e Timber.d(\"Completed: ${it.zipFile.fullName}\")\n        is ZipCompressionResult.Error -\u003e Timber.e(it.errorCode.name)\n        is ZipCompressionResult.DeletingEntryFiles -\u003e Timber.d(\"Deleting ...\") // will be emitted if `deleteSourceWhenComplete` is true\n      }\n    }\n  }\n}\n```\n\nIf you don't have any URI access, then you can request the user to create a ZIP file in the desired location:\n\n```kotlin\nstorageHelper.onFileCreated = { requestCode, file -\u003e\n  ioScope.launch {\n    listOf(folder).compressToZip(baseContext, file).collect {\n      // do stuff\n    }\n  }\n}\nstorageHelper.createFile(mimeType = \"application/zip\", fileName = \"compress test\", initialPath = FileFullPath(baseContext, StorageId.PRIMARY, \"Documents\"))\n```\n\n### Decompression\n\nFYI, decompressing ZIP files is also easy:\n\n```kotlin\nioScope.launch {\n  file.decompressZip(baseContext, targetFolder)\n    .onCompletion {\n      if (it is CancellationException) {\n        Timber.d(\"Decompression is aborted\")\n      }\n    }.collect {\n      when (it) {\n        is ZipDecompressionResult.Validating -\u003e Timber.d(\"Validating...\")\n        is ZipDecompressionResult.Decompressing -\u003e Timber.d(\"Decompressing... ${it.bytesDecompressed}\")\n        is ZipDecompressionResult.Completed -\u003e uiScope.launch {\n          Toast.makeText(baseContext, \"Decompressed successfully\", Toast.LENGTH_SHORT).show()\n        }\n\n        is ZipDecompressionResult.Error -\u003e uiScope.launch {\n          Toast.makeText(baseContext, \"An error has occurred: ${it.errorCode.name}\", Toast.LENGTH_SHORT).show()\n        }\n      }\n    }\n}\n```\n\n## FAQ\n\nHaving trouble? Read the [Frequently Asked Questions](FAQ.md) or join the [Discussions](https://github.com/anggrayudi/SimpleStorage/discussions).\n\n## Other SimpleStorage Usage Examples\n\nSimpleStorage is used in these open source projects.\nCheck how these repositories use it:\n\n* [Snapdrop](https://github.com/fm-sys/snapdrop-android)\n* [MaterialPreference](https://github.com/anggrayudi/MaterialPreference)\n* [Super Productivity](https://github.com/johannesjo/super-productivity-android)\n* [Shared Storage for Flutter](https://pub.dev/packages/shared_storage)\n* [Nextcloud Cookbook](https://codeberg.org/MicMun/nextcloud-cookbook)\n* [Audiobookshelf](https://github.com/advplyr/audiobookshelf-app)\n\n## License\n\n    Copyright © 2020-2024 Anggrayudi Hardiannico A.\n \n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n \n        http://www.apache.org/licenses/LICENSE-2.0\n \n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanggrayudi%2FSimpleStorage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanggrayudi%2FSimpleStorage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanggrayudi%2FSimpleStorage/lists"}