Taming File Storage on Android  —  Part 2
2020-04-03 | 8 min read

Taming File Storage on Android  —  Part 2

Luka Kordić

Android Developer


Oh, you've already read part 1 of this post?! Great, let's continue then. If you haven't, I strongly recommend reading it before you proceed. 😊

Working with media content

In the first part of this post, I talked about storing non-media files on both internal and external storage. Let me now show you how to work with media files from app-specific and shared storage.

App-specific media

If you have to store some media files, but the files are meant for use only inside of your app, it’s probably best to store them to app-specific directories within external storage.

You can obtain the pictures directory using the following code:

fun getPhotoDirFromAppStorage(dirName: String): File? {
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), dirName)
if (!file.mkdirs()) {
Log.e("TEST", "Directory not created")
}
return file
}

When you’re accessing these directories, you must use constants provided by the API, like DIRECTORY_PICTURES in the example above. If you can't find a directory name defined in the API that suits your needs, you can pass null into getExternalFilesDir(). Passing null returns the root app-specific directory within external storage.

Shared storage

Whenever you want your app’s data to be accessible to other apps, or you want the data to persist even after the app has been uninstalled, you should store it to a shared storage.

Android provides two APIs for storing and accessing shareable data. MediaStore API is a recommended way to go when working with media files (pictures, audio, video). If, on the other hand, you need to work with documents and other files, you should use the platform’s Storage Access Framework.

MediaStore API

What’s a MediaStore? According to documentation, MediaStore is an optimized index into media collections, that allows easier retrieving and updating media files. Interaction with the media store is done through ContentResolver object. You can obtain its instance from your app's context.

MediaStore works by defining collections for every media type. The system automatically scans a storage volume and adds media files to appropriate collection. Collections are represented as tables that you can access by calling MediaStore.<media-type>.

Querying collections

For example, to interact with the images table you would do something like the following:

val projection = arrayOf(
//you only want to retrieve _ID and DISPLAY_NAME columns
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME)
context.contentResolver.query(
uri, projection, null, null, null, null)?.use { cursor ->
//cache column indices
val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)

//iterating over all of the found images
while (cursor.moveToNext()) {
val imageId = cursor.getString(idColumn)
val imageName = cursor.getString(nameColumn)
}
}

Notice: When you are working with Cursor objects, don’t forget to close them. I’ve used Kotlin’s use() function to automatically close it after the code inside the block has been executed. Also, make sure to call query() method on a worker thread.

You can use the same code if you want to interact with video or audio files. All you have to do is change MediaStore.Images to MediaStore.Video or MediaStore.Audio, respectively.

The Media store also includes a collection called MediaStore.Files. What you will find in there depends on the version of Android you have. To be precise, if your app uses scoped storage (available on Android 10 and higher) this collection will show only the photos, videos, and audio files that your app has created. When scoped storage is not being used, the collection shows all types of media files.

On newer versions of Android (only Android 10 and higher), MediaStore API also provides you with MediaStore.Downloads table, where you can access downloaded files.

Creating a new file

If you want to create a new file and store it to one of the collections, you can easily do so using the MediaStore API. Here’s one example of creating a new image file:

fun createNewImageFile(): Uri? {
val resolver = context.contentResolver
// On API <= 28, use VOLUME_EXTERNAL instead.
val imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val newImageDetails = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "New image.jpg")
}
//return Uri of newly created file
return resolver.insert(imageCollection, newImageDetails)
}

First, you obtain an instance of ContentResolver class. After that, you get Uri of a collection where you want to store the new file. To put the new file within the collection you create ContentValues object and call put() method on it, passing key-value pairs as arguments.

The last step is to call insert() method on the previously obtained resolver instance.
insert() method returns newly created file's Uri which you can use to modify the file after creation.  

Deleting a file

I’m sure that you’ve noticed a pattern in the MediaStore API by now. It’s a very similar procedure with update or deletion as well. Let’s have a look!

To delete a file you would use code similar to this:

fun deleteMediaFile(fileName: String) {
val fileInfo = getFileInfoFromName(fileName) //this function returns Kotlin Pair<Long, Uri>
val id = fileInfo.first
val uri = fileInfo.second
val selection = "${MediaStore.Images.Media._ID} = ?"
val selectionArgs = arrayOf(id.toString())
val resolver = context.contentResolver
resolver.delete(uri, selection, selectionArgs)
}

First, you need to obtain an id and a uri of the file you want to delete. This is what getFileInfoFromName() function does. Just a hint, this is my helper function, it's not part of the official API. When you have the needed info, you can retrieve an instance of ContentResolver as before and call its delete() method.

This method accepts three arguments:

  • Uri of the file
  • WHERE clause without actual arguments, called selection - specify which rows are going to be deleted
  • Array of arguments for selection parameter - provide id for an item you want to delete

Updating a file

Updating a file is done the same way, except you need to call contentResolver.update() method instead. There's one thing to note here, however.


 If your app uses scoped storage you can't simply update or delete a file that your app didn't create. There are some additional steps you need to do before you are allowed to do those operations. Here's an example from the official documentation of how this can be done.

Storage Access Framework

In previous chapters, I’ve mostly talked about working with media files by using MediaStore API. In this chapter, you will learn about the Storage Access Framework and how to utilize it to browse and modify documents and other files across all document storage providers.

When you’re working with files by using this framework, you don’t need to request any system permissions because the user is involved in selecting the files or directories that your app can access. After the user has selected the file, your app gains read and write access to a URI representing the chosen file.

Opening a file

Opening a file using this framework is pretty straightforward. Here’s one example to demonstrate it:

private fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
startActivityForResult(intent, OPEN_DOCUMENT)
}

This code allows users to select any file from the system’s file picker app. Let’s break it down line by line.
First, you create an intent with ACTION_OPEN_DOCUMENT action. Then, you set a MIME type that indicates which file types your app supports. In the code above I've used */*, which means I want to show all files. If for example, you want to show only images, you use images/*, or for pdfs application/pdf. Besides the type, you should add a category for files. In this case, I chose Intent.CATEGORY_OPENABLE. This will show only files that can be opened with ContentResolver.openFileDescriptor() method.
 When you have prepared your intent, call startActivityForResult() and pass in the intent with a unique request code.

Try to run the app and check what happens. You should see a screen similar to this one:

System Picker UI

Creating a new file

The process of creating a new file is very similar to the one for opening. Use ACTION_CREATE_DOCUMENT action to create a new intent.

private fun createFile(name: String) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_TITLE, name)
}
startActivityForResult(intent, CREATE_DOCUMENT)
}

As with opening, you need to add a category.


In this case, it’s Intent.CATEGORY_OPENABLE. Then set the MIME type for the file you want to create. If you would like to add a title for a file, you can do so by using Intent.EXTRA_TITLE intent extra. One thing to note here is that this action cannot overwrite an existing file. In case you put the same name, the system appends a number at the end of the file name.

Try running the code. You should see something like this:

Creating a new file

Granting access to a directory

If for some reason, your app needs to get access to the contents of a directory, you can use ACTION_OPEN_DOCUMENT_TREE intent action. By using this, the user can grant your app access to the entire directory tree. Your app can then access any file in the directory and its subdirectories.


 It's worth noting that your app doesn't have access to other apps' files outside of the user-selected directory.

Getting the result back

For each of these actions, you need to call startActivityForResult() passing in intent with the appropriate action. When the user has finished selecting the file or a directory, you will get the result in onActivityResult() callback. You get selected file's Uri within the intent's data property. You can then use this Uri to make modifications to the file.

I will explain this in more detail later, but before making any modifications to the file, you should check the value of DocumentsContract.Document.COLUMN_FLAGS. It indicates which operations on the given file are supported by the provider.

Here’s the code you can use to obtain the Uri:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == OPEN_DOCUMENT && resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
//use this uri to make supported modifications to the file
}
}
}

Under the hood

Storage Access Framework works with content providers under the hood. Main parts of the framework are:

  • Documents provider — A provider that allows other apps to reveal the files they manage. This provider can be implemented by both local and cloud storage services. Android provides you with Downloads, Images and Videos built-in documents providers. To learn more about documents provider check out this link
  • Client app — An app that invokes some of the intent actions we’ve mentioned above, and receives selected files
  • Picker — This is the UI that lets users select files from all providers that satisfy the search criteria defined in the client’s app

Every app that wants to contribute their files to the system picker UI has to implement its own documents provider.


Now, to go back to the statement from the last chapter where I said that you need to check DocumentsContract.Document.COLUMN_FLAGS before trying to do any modifications to a file. When apps implement the provider, they can set different capabilities for each document with the aforementioned flags. For example, the provider can set Document.FLAG_SUPPORTS_REMOVE to indicate that you can delete this document.

To conclude

File storage in Android is a broad topic for sure, and there are some things that couldn’t fit in these two posts. That's why I’ll put some additional resources below, so make sure to check them out for more details:

1. Privacy in Android 11

2. Performing operations on chosen locations

3. Google storage samples

It can be really hard to start working with file storage on Android. There are quite a few things to consider when doing any kind of file management. I encourage you to take these examples and write them yourself (don’t just copy-paste them). Also, run the app and see what’s happening. Once you try it out, it will become much more clear how this stuff works.

I hope this post was helpful. If you have any questions or suggestions for improvement, please let us know! 🤗

Like what you read?Go on, share it with friends!
ABOUT THE AUTHOR

Luka Kordić

Android Developer
Luka Kordić is an Android developer at COBE Tech. He mostly uses Kotlin in his day to day development, but Java is also an option. When he's not writing Android apps, he likes to learn new things from the computer science world. He really enjoys sports. In his spare time he plays football, but also likes running, climbing and basketball. When not working with software or playing sports, he likes to play video games.

Let's turn your idea into reality

Save money, time and energy and book the entire team today.