Taming File Storage on Android   —  Part 1
2020-04-02 | 8 min read

Taming File Storage on Android   —  Part 1

Luka Kordić

Android Developer

Welcome to the first part of a two-part series on working with file storage in Android!

A vast majority of apps are doing some form of data management. Whether it’s just loading a profile image, sharing images or video/audio files through messaging or simply storing data. Working with files on Android can be daunting. Especially if you’re new to Android, or just haven’t worked with it in a while.

When it comes to saving data on Android, we can choose from a few options:

  • SharedPreferences — most commonly used for storing app preferences, as key-value pairs
  • Databases (Room) — for storing structured data in a private database
  • File storage — for storing all kinds of media/documents to the file system

If you are wondering which of these options is for you, there are four simple questions in the official documentation that you can go through to figure out what type of storage you need.

In this article though, I am going to focus on storing data to a disk using some of the APIs provided by Android.

Topics we are going to cover include:

  • Categories of physical storage locations
  • Permissions and access to storage
  • Scoped storage
  • Working with media content
  • Working with documents and files

Internal storage vs external storage

Android system provides us with two types of physical storage locations: internal and external storage.
The most significant difference between them is that the internal storage is smaller than external storage on most devices. Also, the external storage might not always be available, in contrast to internal, which is always available on all devices. The system creates an internal storage directory and names it with the app’s package name.

Keeping in mind that the internal storage is always available, this makes it a reliable place to store data that your app depends on. Furthermore, if you need to save sensitive data or data that only your app can have access to, go for the internal storage.

Two things are important to note when working with internal storage:

  • A user cannot access these files through the file manager
  • Files in this folder will be deleted when an app is uninstalled

To access and store data to app’s files in the internal storage, you can use File API. I wrote a simple method just for an example:

fun writeToFile(fileName: String, contentToWrite: String) {
val file = File(context.filesDir, fileName)
file.writeText(contentToWrite)
}

Alternatively, you can call openFileOutput() method to obtain an instance of FileOutputStream that can write to a file in the filesDir directory.

fun writeToFile(fileName : String, contentToWrite : String) { context.openFileOutput(fileName, Context.MODE_PRIVATE).use { it.write(contentToWrite.toByteArray()) 
}
}

You would read from a file in this directory in almost the same way. You can create a File and call readText() method or you can obtain an instance of FileInputStream by calling openFileInput().

Here’s an example:

fun readFromFile(fileName: String): String {
context.openFileInput(fileName).bufferedReader().useLines {
it.fold("") { some, text ->
return "$some \n$text"
}
}
return "The file is empty!"
}

Okay, but when should I use external storage?

Well, as I mentioned earlier, external storage is usually larger than internal storage. So, the first thing that comes to mind is to use it for storing larger files or apps. And that exactly is the most common usage of the external storage.
Since internal storage has limited space for app-specific data, it’s generally a good idea to use external storage for all of the non-sensitive data your app works with. Even if they are not so large.

Generally, data you save to this storage is persistent through app uninstallation. There is a case though, where files are going to be removed when an app is deleted. If you store app-specific files to the external storage by using getExternalFilesDir(), you will lose the files when the app is uninstalled, so be aware of that. Additionally, data stored in this directory can be accessed by other applications if they have appropriate permission.

Caveats

When it comes to working with external storage, there are few things you should do before storing data there:

  • Verify that the storage is available — there are cases where are user can remove a physical volume where the external storage resides. You can check the volume’s state by invoking Environment.getExternalStorageState() method. It will return a string representing the state. For example, if the method returns MEDIA_MOUNTED you can safely read and write app-specific files withing external storage.
  • Select which storage to use in case more of them exist — sometimes devices can have multiple physical volumes that could contain external storage. For example, a device can allocate a partition of its internal memory as external storage, but can also provide external storage on an SD card. There’s a handy method in ContextCompat class, called getExternalFilesDirs(). It returns an array of File's whose first element is considered the primary external storage volume.

Storage permissions

Android defines two permissions related to storage: READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE. As you can see, permissions are only defined for accessing external storage. That means that every app, by default, has permissions to access its internal storage.

On the other hand, if your app has to access external storage, you are obliged to request permission for that. That means if you’re trying to access media on external storage by using MediaStore API you will need to request READ_EXTERNAL_STORAGE permission.

However, few exceptions to this rule exist:

  • When you are accessing app-specific files on external storage you don’t need to request any permission (on Android 4.4 and higher)
  • With scoped storage introduced in Android 10, you no longer need to request permission when working with media files that are created by your app

You also don’t need any permissions if you’re trying to obtain any documents or other types of content when using Storage Access Framework. That’s because a user is involved in the process of selecting the actual content to work with.

When defining permissions, you can set a condition to only apply it for some versions. For example:

<uses-permission 
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

In the example above I defined WRITE_EXTERNAL_STORAGE permission only for version 28 and below. This can come in handy in situations where apps running newer versions can work without permission, but apps on older versions will break without it.

Introducing Scoped storage

Android is focusing on privacy more and more with every release. Application Sandboxing is a core part of Android’s design, isolating apps from each other. Following that principle, the Android team introduced Scoped storage in Android 10. As the team said, “It’s a way to ensure transparency, give users control and secure personal data.”

From the official documentation:

More recent versions of Android rely more on a file’s purpose than its location for determining an app’s ability to access that file. This purpose-based storage model improves user privacy because apps are given access only to the areas of the device’s file system that they use.

Why Scoped storage?

Before Android 10, apps have private, app-specific storage. In addition to private storage, the system provides shared storage where all of the other files are stored. The problem is that apps can access all of these files when given storage permission. From a user’s perspective, this doesn’t make sense.

Let’s take an example with an app that needs to provide users with an ability to select a profile image. To allow the app to access images you need to ask for storage reading permission. Given the permission, your app can now access images but also all the other files within the shared storage. There is no need for your app to be able to see every file, only to allow the user to select an image. This is one of the main reasons scoped storage has been introduced.

The changes

This means that apps that target Android 10 and higher are given scoped access to external storage, or scoped storage, by default. With this change, apps have unrestricted access to app-specific storage or media that the app itself has created. But, to read other applications’ media files, you need to request reading permission. 
For accessing MediaStore.Downloads collection files that your app didn't create, you must use Storage Access Framework.

If your app is not ready to adopt scoped storage yet, you have two options to opt-out of using it:

  • You can set targetSdkVersion to target Android 9 or lower
  • If your app targets Android 10, you can set requestLegacyExternalStorage flag to true in AndroidManifest.xml. Keep in mind that this flag is removed in Android 11

You can read more about Android 11 storage updates here.

Make sure to check the second part of this post tomorrow! 😊

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.