All Projects β†’ anggrayudi β†’ SimpleStorage

anggrayudi / SimpleStorage

Licence: Apache-2.0 license
πŸ’Ύ Simplify Android Storage Access Framework for file management across API levels.

Programming Languages

kotlin
9241 projects
shell
77523 projects

Projects that are alternatives of or similar to SimpleStorage

ScopedStorageDemo
medium.com/better-programming/all-you-need-to-know-about-scoped-storage-in-android-10-e621f40bc8b9
Stars: ✭ 44 (-91.16%)
Mutual labels:  storage, android-storage, android-10, scoped-storage
FireFiles
Powerful Android File Manager for everything that runs on Android OS (Android TV, Android Watch, Mobile, etc)
Stars: ✭ 37 (-92.57%)
Mutual labels:  storage, file
modernstorage
ModernStorage is a group of libraries that provide an abstraction layer over storage on Android to simplify its interactions
Stars: ✭ 982 (+97.19%)
Mutual labels:  storage, storage-access-framework
Rustbreak
A simple, fast and easy to use self-contained single file storage for Rust
Stars: ✭ 315 (-36.75%)
Mutual labels:  storage, file
Filegator
Powerful Multi-User File Manager
Stars: ✭ 587 (+17.87%)
Mutual labels:  storage, file
WhatsApp Status Saver
WhatsApp Status Downloader, preview, share and save files. Android 10+ support
Stars: ✭ 78 (-84.34%)
Mutual labels:  storage-access-framework, scoped-storage
Node Scalable Blob Store
A file system blob store that is designed to prevent conflicts when used with a distributed file system or storage area network
Stars: ✭ 31 (-93.78%)
Mutual labels:  storage, file
File Storage
File storage abstraction for Yii2
Stars: ✭ 116 (-76.71%)
Mutual labels:  storage, file
Nodestream
Storage-agnostic streaming library for binary data transfers
Stars: ✭ 70 (-85.94%)
Mutual labels:  storage, file
React Native Fetch Blob
This project was started in the cause of solving issue facebook/react-native#854, React Native's lacks of Blob implementation which results into problems when transferring binary data.
Stars: ✭ 2,593 (+420.68%)
Mutual labels:  storage, file
Fuck-Storage-Access-Framework
Fuck Storage Access Framework (or just FSAF) is a handy library that hides away from you all the annoying parts of the Storage Access Framework (like DocumentTrees / DocumentIds / DocumentFiles / DocumentContracts and other bullshit) leaving only an API that is similar to good-old Java File API
Stars: ✭ 211 (-57.63%)
Mutual labels:  saf, storage-access-framework
beegfs-csi-driver
The BeeGFS Container Storage Interface (CSI) driver provides high performing and scalable storage for workloads running in Kubernetes.
Stars: ✭ 32 (-93.57%)
Mutual labels:  storage
transfer-sh
Node.js CLI tool for easy file sharing using Transfer.sh
Stars: ✭ 24 (-95.18%)
Mutual labels:  file
Floppy
Fast object key value storage for Java with much support for Android
Stars: ✭ 82 (-83.53%)
Mutual labels:  storage
inplace
In-place file processing in Python
Stars: ✭ 21 (-95.78%)
Mutual labels:  file
azure-storage
Azure Storage module for Nest framework (node.js) ☁️
Stars: ✭ 71 (-85.74%)
Mutual labels:  storage
storage-box
Intuitive and easy-to-use storage box.
Stars: ✭ 26 (-94.78%)
Mutual labels:  storage
eks-nvme-ssd-provisioner
EKS NVMe SSD provisioner for Amazon EC2 Instance Stores
Stars: ✭ 50 (-89.96%)
Mutual labels:  storage
file-handling
File Handling Helper
Stars: ✭ 14 (-97.19%)
Mutual labels:  storage
StorageDsc
DSC resource module is used to manage storage on Windows Servers.
Stars: ✭ 58 (-88.35%)
Mutual labels:  storage

SimpleStorage

Maven Central Build Status

Table of Contents

Overview

The more higher API level, the more Google restricted file access on Android storage. Although Storage Access Framework (SAF) is designed to secure user's storage from malicious apps, but this makes us even more difficult in accessing files. Let's take an example where java.io.File has been deprecated in Android 10.

Simple Storage ease you in accessing and managing files across API levels. If you want to know more about the background of this library, please read this article: Easy Storage Access Framework in Android with SimpleStorage

Adding Simple Storage into your project is pretty simple:

implementation "com.anggrayudi:storage:X.Y.Z"

Where X.Y.Z is the library version: Maven Central

All versions can be found here. To use SNAPSHOT version, you need to add this URL to the root Gradle:

allprojects {
    repositories {
        google()
        mavenCentral()
        // add this line
        maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
    }
}

Java Compatibility

Simple Storage is built in Kotlin. Follow this documentation to use it in your Java project.

Terminology

Alt text

Other Terminology

Read Files

In Simple Storage, DocumentFile is used to access files when your app has been granted full storage access, included URI permissions for read and write. Whereas MediaFile is used to access media files from MediaStore without URI permissions to the storage.

You can read file with helper functions in DocumentFileCompat and MediaStoreCompat:

DocumentFileCompat

  • DocumentFileCompat.fromFullPath()
  • DocumentFileCompat.fromSimplePath()
  • DocumentFileCompat.fromFile()
  • DocumentFileCompat.fromPublicFolder()

Example

val fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = "Downloads/MyMovie.mp4")

val fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = "9016-4EF8", basePath = "Downloads/MyMovie.mp4")

MediaStoreCompat

  • MediaStoreCompat.fromMediaId()
  • MediaStoreCompat.fromFileName()
  • MediaStoreCompat.fromRelativePath()
  • MediaStoreCompat.fromFileNameContains()
  • MediaStoreCompat.fromMimeType()
  • MediaStoreCompat.fromMediaType()

Example

val myVideo = MediaStoreCompat.fromFileName(context, MediaType.DOWNLOADS, "MyMovie.mp4")

val imageList = MediaStoreCompat.fromMediaType(context, MediaType.IMAGE)

Manage Files

DocumentFile

Since java.io.File has been deprecated in Android 10, thus you have to use DocumentFile for file management.

Simple Storage adds Kotlin extension functions to DocumentFile, so you can manage files like this:

  • DocumentFile.getStorageId()
  • DocumentFile.getStorageType()
  • DocumentFile.getBasePath()
  • DocumentFile.copyFileTo()
  • List<DocumentFile>.moveTo()
  • DocumentFile.search()
  • DocumentFile.deleteRecursively()
  • DocumentFile.getProperties()
  • DocumentFile.openOutputStream(), and many more…

MediaFile

For media files, you can have similar capabilities to DocumentFile, i.e.:

  • MediaFile.absolutePath
  • MediaFile.isPending
  • MediaFile.delete()
  • MediaFile.renameTo()
  • MediaFile.copyFileTo()
  • MediaFile.moveFileTo()
  • MediaFile.openInputStream()
  • MediaFile.openOutputStream(), etc.

Request Storage Access

Although user has granted read and write permissions during runtime, your app may still does not have full access to the storage, thus you cannot search, move and copy files. You can check whether you have the storage access via SimpleStorage.hasStorageAccess().

To enable full storage access, you need to open SAF and let user grant URI permissions for read and write access. This library provides you an helper class named SimpleStorage to ease the request process:

class MainActivity : AppCompatActivity() {

    private val storage = SimpleStorage(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setupSimpleStorage()
        btnRequestStorageAccess.setOnClickListener {
            storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS)
        }
    }

    private fun setupSimpleStorage() {
        storage.storageAccessCallback = object : StorageAccessCallback {
            override fun onRootPathNotSelected(
                requestCode: Int,
                rootPath: String,
                uri: Uri,
                selectedStorageType: StorageType,
                expectedStorageType: StorageType
            ) {
                MaterialDialog(this@MainActivity)
                    .message(text = "Please select $rootPath")
                    .negativeButton(android.R.string.cancel)
                    .positiveButton {
                        val initialRoot = if (expectedStorageType.isExpected(selectedStorageType)) selectedStorageType else expectedStorageType
                        storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS, initialRoot, expectedStorageType)
                    }.show()
            }

            override fun onCanceledByUser(requestCode: Int) {
                Toast.makeText(baseContext, "Canceled by user", Toast.LENGTH_SHORT).show()
            }

            override fun onStoragePermissionDenied(requestCode: Int) {
                /*
                Request runtime permissions for Manifest.permission.WRITE_EXTERNAL_STORAGE
                and Manifest.permission.READ_EXTERNAL_STORAGE
                */
            }

            override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
                Toast.makeText(baseContext, "Storage access has been granted for ${root.getStorageId(baseContext)}", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        storage.onSaveInstanceState(outState)
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        storage.onRestoreInstanceState(savedInstanceState)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storage.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storage.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}

Folder Picker

private fun requestStoragePermission() {
    /*
    Request runtime permissions for Manifest.permission.WRITE_EXTERNAL_STORAGE
    and Manifest.permission.READ_EXTERNAL_STORAGE
    */
}

private fun setupFolderPickerCallback() {
    storage.folderPickerCallback = object : FolderPickerCallback {
        override fun onStoragePermissionDenied(requestCode: Int) {
            requestStoragePermission()
        }

        override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
            if (storageType == StorageType.UNKNOWN) {
                requestStoragePermission()
                return
            }
            MaterialDialog(this@MainActivity)
                .message(
                    text = "You have no write access to this storage, thus selecting this folder is useless." +
                            "\nWould you like to grant access to this folder?"
                )
                .negativeButton(android.R.string.cancel)
                .positiveButton {
                    storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS, storageType)
                }.show()
        }

        override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
            Toast.makeText(baseContext, folder.getAbsolutePath(baseContext), Toast.LENGTH_SHORT).show()
        }

        override fun onCanceledByUser(requestCode: Int) {
            Toast.makeText(baseContext, "Folder picker canceled by user", Toast.LENGTH_SHORT).show()
        }
    }
}

File Picker

private fun setupFilePickerCallback() {
    storage.filePickerCallback = object : FilePickerCallback {
        override fun onCanceledByUser(requestCode: Int) {
            Toast.makeText(baseContext, "File picker canceled by user", Toast.LENGTH_SHORT).show()
        }

        override fun onStoragePermissionDenied(requestCode: Int, file: DocumentFile?) {
            requestStoragePermission()
        }

        override fun onFileSelected(requestCode: Int, file: DocumentFile) {
            Toast.makeText(baseContext, "File selected: ${file.name}", Toast.LENGTH_SHORT).show()
        }
    }
}

SimpleStorageHelper

If you feel implementing folder & file picker and full disk request are complicated enough, you can use SimpleStorageHelper to simplify the process. This helper class contains default styles for managing storage access.

class MainActivity : AppCompatActivity() {

    private val storageHelper = SimpleStorageHelper(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        storageHelper.onFolderSelected = { requestCode, folder ->
            // do stuff
        }
        storageHelper.onFileSelected = { requestCode, files ->
            // do stuff
        }

        btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }
        btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }
        btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        storageHelper.onSaveInstanceState(outState)
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        storageHelper.onRestoreInstanceState(savedInstanceState)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storageHelper.storage.onActivityResult(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // Mandatory for Activity, but not for Fragment & ComponentActivity
        storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
}

Simpler, right?

Move & Copy: Files & Folders

Simple Storage helps you in copying/moving files & folders via:

  • DocumentFile.copyFileTo()
  • DocumentFile.moveFileTo()
  • DocumentFile.copyFolderTo()
  • DocumentFile.moveFolderTo()

For example, you can move a folder with few lines of code:

val folder: DocumentFile = ...
val targetFolder: DocumentFile = ...

// Since moveFolderTo() is annotated with @WorkerThread, you must execute it in background thread
folder.moveFolderTo(applicationContext, targetFolder, skipEmptyFiles = false, callback = object : FolderCallback() {
    override fun onPrepare() {
        // Show notification or progress bar dialog with indeterminate state
    }

    override fun onCountingFiles() {
        // Inform user that the app is counting & calculating files
    }

    override fun onStart(folder: DocumentFile, totalFilesToCopy: Int, workerThread: Thread): Long {
        return 1000 // update progress every 1 second
    }

    override fun onParentConflict(destinationFolder: DocumentFile, action: FolderCallback.ParentFolderConflictAction, canMerge: Boolean) {
        handleParentFolderConflict(destinationFolder, action, canMerge)
    }

    override fun onContentConflict(
        destinationFolder: DocumentFile,
        conflictedFiles: MutableList<FolderCallback.FileConflict>,
        action: FolderCallback.FolderContentConflictAction
    ) {
        handleFolderContentConflict(action, conflictedFiles)
    }

    override fun onReport(report: Report) {
        Timber.d("onReport() -> ${report.progress.toInt()}% | Copied ${report.fileCount} files")
    }

    override fun onCompleted(result: Result) {
        Toast.makeText(baseContext, "Copied ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files", Toast.LENGTH_SHORT).show()
    }

    override fun onFailed(errorCode: ErrorCode) {
        Toast.makeText(baseContext, "An error has occurred: $errorCode", Toast.LENGTH_SHORT).show()
    }
})

The coolest thing of this library is you can ask users to choose Merge, Replace, Create New, or Skip Duplicate folders & files whenever a conflict is found via onConflict(). Here're screenshots of the sample code when dealing with conflicts:

Alt text Alt text

Read MainActivity from the sample code if you want to mimic above dialogs.

FAQ

Having trouble? Read the Frequently Asked Questions.

Other SimpleStorage Usage Examples

SimpleStorage is used in these open source projects. Check how these repositories use it:

License

Copyright Β© 2020-2022 Anggrayudi Hardiannico A.

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

    http://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.
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].