How to use Android Storage Access Framework with example

How to use Android Storage Access Framework with example

Tags
Android
Published
December 14, 2020
Author
Mateusz Teteruk
Before I go into details of Storage Access Framework, let me tell you little backstory. Google is forcing Android developers to make apps target specific Android version. Current requirement is SDK 29 (Android 10). With this come some changes regarding apps and permissions. One of the most crucial change is Scoped Storage which changes how permissions regarding storage work. More about those changes can be found here https://developer.android.com/training/data-storage.
In DaysCounter I used storage permissions in data backup feature. Exporting data was working in this way:
  1. if user wanted to export data locally, file was generated in specific destination using default IO methods.
  1. if user wanted to export data to Google Drive, he had to log in via Google button. Later, file was generated in root folder and sent directly to root folder of Google Drive. Next, temporary file was deleted.
Importing data worked very similar: choosing specific file from device or just fetching last file with given prefix from Google Drive root folder.
Even though it was working, because of permissions changes in Android 10 and 11, I had to change it so I was checking out possible ways how to handle that. First of all, I didn’t want to fight against Android and I wanted to make this feature great again as smooth as possible. My pick was Storage Access Framework.

What’s Storage Access Framework

Storage Access Framework was introduced in API 19 and doesn’t require any permissions (great!). DocumentProvider is needed. More info about SAF is here: https://developer.android.com/training/data-storage/shared/documents-files and https://developer.android.com/guide/topics/providers/document-provider.

How does Storage Access Framework work

First of all, we have to prepare Intent with specific action which is used to startActivityForResult. Please remember about restrictions introduced in Android 11 (https://developer.android.com/training/data-storage/shared/documents-files#document-tree-access-restrictions). Later on, in onActivityResult we listen for result of SAF activity in which we receive URI. This URI is all we need. We can use it to make proper stream and do operations on file (read from it or write to it).

Example with code snippets

All code snippets presented below are available on GitHub repository https://github.com/mateuszteteruk/blogposts-samples/tree/master/storage-access-framework.
First, let’s start with entry points to SAF which are Intents. Really simple. Two Intents, first one for opening file, second one for saving file. I also specified type of files and initial filename when saving new one.
fun createFilePickerIntent(): Intent = Intent(Intent.ACTION_OPEN_DOCUMENT) .apply { addCategory(Intent.CATEGORY_OPENABLE) type = "text/*" } fun createFileSaverIntent(fileName: String): Intent = Intent(Intent.ACTION_CREATE_DOCUMENT) .apply { addCategory(Intent.CATEGORY_OPENABLE) type = "text/*" putExtra(Intent.EXTRA_TITLE, fileName) }
In case we want to use it in Activity:
// .... Activity val safDelegate: StorageAccessFrameworkDelegate const val REQ_CODE_EXPORT = 45123 const val REQ_CODE_IMPORT = 45124 // ... override fun onCreate(savedInstanceState: Bundle?) { // ..... // example for opening file startActivityForResult(safDelegate.createFilePickerIntent(), REQ_CODE_IMPORT) // .... } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { REQ_CODE_EXPORT -> { if (resultCode == Activity.RESULT_OK) { val uri = data?.data ?: return // handle uri } } REQ_CODE_IMPORT -> { if (resultCode == Activity.RESULT_OK) { val uri = data?.data ?: return // handle uri } } } super.onActivityResult(requestCode, resultCode, data) }
Finally, it’s time for the last part which is using URI to create proper stream (I’m using in here Single from RxJava2 but could be replaced with Coroutines or whatever you want). I’m using here use extension function from Kotlin Closeable.kt which takes care of closing resource when not needed.
My use case is export/import data in some kind of line by line format, like CSV.
fun write(uri: Uri, items: List<String>): Single<Boolean> { val outputStream = context.contentResolver.openOutputStream(uri) ?: return Single.just(false) return Single.fromCallable { outputStream } .map { os -> os.use { stream -> stream.bufferedWriter().use { writer -> items.asSequence() .forEach { writer.append(it).appendLine() } } } true } .onErrorReturn { false } .subscribeOn(Schedulers.io()) } fun read(uri: Uri): Single<List<String>> { val inputStream = context.contentResolver.openInputStream(uri) ?: return Single.just(emptyList()) return Single.fromCallable { inputStream } .map { input -> input.use { stream -> stream.bufferedReader().use { reader -> reader.lineSequence().toList() } } } .onErrorReturn { emptyList() } .subscribeOn(Schedulers.io()) }

Time to sum up

In short, Storage Access Framework let me achieve same result as I had previously (local and Google Drive import/export) with less lines of code. Flow for both cases is the same because right now user has to pick proper location for new file using SAF screen – within app we can only receive URI of file we interacted with. From this URI we can make proper stream in order to read or write depending on our needs. Thanks to SAF, I could greatly simplify logic in DaysCounter’s backup screen.

Do you let your users to export/import data they have in app? If you do, do you use SAF like me? What’s your take on this? And if you don’t, you should! Users are really happy and find it useful (at least users of DaysCounter 🥰🥰). Let me know what do think about that in comments below or just send me an email.