Managing files in your app requires understanding internal and external storage types. This topic will explain each storage type and help you choose the best option for different scenarios. You'll learn about Android's newest feature, scoped storage, which drastically changes how apps interact with storage. We'll guide you through the key CRUD operations for both internal and external storage. Let's get started.
Internal vs. external storage
Before discussing internal and external storage, it's important to know that any Android device has two physical storage options:
A built-in storage, usually a flash memory, houses the operating system and all the files needed for the device to operate.
A removable storage medium, like an SD card or USB drive. This storage provides more space to store large files or files that aren't critical to the app's functionality.
When an app is installed on an Android device, the system automatically allocates two distinct storage locations for that app:
Internal storage: This space, created on the device's built-in storage, contains important files necessary for the app's operation. This storage isn't accessible by the user, except via installed apps. Files stored here by an app are exclusive to that app, meaning that other apps can't access them, even with the right permissions. You can get the path to these directories with
context.getFilesDir().External storage: Besides directories created in internal storage, the system also creates directories for the app in another space known as external storage. It could be either a dedicated partition of the device's built-in storage or a removable storage medium. Even if your phone doesn't have a mounted removable storage medium, it doesn't imply that it lacks external storage. In many cases, external storage is created as part of the device's built-in storage. It's available for storing shared files accessible to all apps (with the appropriate permissions) and to the user. External storage is more like a public storage. It's often larger than internal storage but may not always be accessible because some devices allow users to remove the physical device corresponding to external storage. You can get the path to these directories with
context.getExternalFilesDir().
Note that any files stored by the app in both locations are automatically deleted when the corresponding app is uninstalled.
Understanding external storage can be complex. So, let's delve deeper into this topic.
Shared and scoped storage
Shared storage is a place where all apps can store files accessible by other apps. These files aren't specific to the creating app, and these files won't be removed from the device if that app is uninstalled.
Before Android 10 (API level 29), apps could access other apps' external storage, if they had the correct permissions. Hence, there was little difference between shared storage and external storage, that's why those two terms were often used interchangeably. However, this changed with the introduction of scoped storage.
Starting with Android 10, Google introduced a new storage model known as "scoped storage". This model changes how apps can access a device's external storage. Under scoped storage, each app has its own isolated storage sandbox within the external storage, and by default, no other app can access this sandbox. This limitation applies even if the other app has the READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE permissions.
This change enhances user privacy by restricting an app's ability to access sensitive data stored by other apps. It provides a more secure environment and gives users more control over their data, marking a significant improvement in Android's storage system.
Note that starting from Android 11, apps targeting API 30 or higher must use scope storage. Apps that run on Android 11 but target Android 10 can still request the requestLegacyExternalStorage attribute, which allows apps to temporarily opt out of the changes associated with scoped storage. After you update your app to target Android 11, the system ignores the requestLegacyExternalStorage flag.
To summarize, we can illustrate the architecture of Android storage:
App's internal storage: save, load, and delete
In Android, each application has a dedicated space in the device's internal storage to save data. This data is private to the application, meaning other apps cannot access it. It is a secure place to store sensitive information or any data that your app needs to function but doesn't need to be exposed to the user or other apps. This data persists across user sessions, even if your app is killed and restarted. However, if the user uninstalls your app, the system removes all your app's files from internal storage.
Now, let's explore the operations you can perform on internal storage:
1) Save data in internal storage:
Here's how you can write data to a file in internal storage:
fun writeToFile(context: Context, filename: String, data: String) {
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
it.write(data.toByteArray())
}
}In this function, openFileOutput() is used to create a file in the internal storage. The MODE_PRIVATE argument means the file will be accessible by only your app. The use function is a Kotlin extension function on Closeable, which automatically closes the stream when finished.
2) Load data from internal storage:
Reading data from a file in internal storage can be done as follows:
fun readFromFile(context: Context, filename: String): String {
return context.openFileInput(filename).bufferedReader().use { it.readText() }
}Here, openFileInput() is used to open the file, and bufferedReader().use { it.readText() } is used to read the file content as a string.
3) Delete data from internal storage:
Deleting a file from internal storage is straightforward:
fun deleteFile(context: Context, filename: String) {
context.deleteFile(filename)
}The deleteFile() function is used to remove a file from the internal storage.
Remember, working with internal storage is limited by the available space on the device. Therefore, it's best used for small amounts of sensitive data that your app needs to function.
When performing I/O operations in a real app, always ensure robust exception handling to manage unexpected situations. Use try-catch blocks to catch specific exceptions like FileNotFoundException, IOException, or SecurityException. Ensure handling edge cases where the storage might not be available or is in a read-only state. This will help prevent your app from crashing, provide feedback to the user, or log the issue for further investigation.
App's external/scoped storage: save, load, and delete
In this section, we'll discuss the concept of an app's external storage, also known as scoped storage in Android 10 and later versions. This storage area can be used to save large files that are intended to remain private to your app, or to store other non-critical private files.
Note that external storage can be unmounted or removed, so your app should handle these situations gracefully. Furthermore, if a user uninstalls your app, the system will automatically delete all of your app's files from scoped storage, similar to the behaviour with internal storage. This ensures user data's privacy and security.
Now, let's look at the operations that can be performed on scoped storage:
1) Save data in external storage:
To write data to external storage, you must request the appropriate permissions from the user. From Android 6.0 (API level 23), you need to check for and request permissions at runtime. For external storage, typically you need to request WRITE_EXTERNAL_STORAGE permission. However, if you're targeting Android 10 (API level 29) or above and using getExternalFilesDir(), you don't need to request this permission because you're writing to your app's specific directory.
Also, check if the external storage is available and writable. Then, you can use FileOutputStream to write data. Here's an example:
fun writeToFile(context: Context, filename: String, data: String) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
return
}
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
val file = File(context.getExternalFilesDir(null), filename)
FileOutputStream(file).use {
it.write(data.toByteArray())
}
}
}In this function, before writing to the external storage, it checks if the app is running on Android 10 (API level 29) or higher. If it's running on a lower version, it checks if the WRITE_EXTERNAL_STORAGE permission has been granted. If the permission isn't granted, you should request it. The actual permission request is commented out because it usually involves UI interaction and should be handled in an Activity or Fragment where you can override onRequestPermissionsResult to handle the callback.
getExternalFilesDir(null) is used to get a File representing your app's private files directory in external storage. The null argument means you're not requesting a specific sub-directory, but you could request directories for specific types of files like Environment.DIRECTORY_PICTURES.
2) Load Data from External Storage:
Reading data from a file in external storage is similar to writing. You need to check for permissions and if the external storage is available to read. Then, you can use FileInputStream to read data:
fun readFromFile(context: Context, filename: String): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
return null
}
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED || Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED_READ_ONLY) {
val file = File(context.getExternalFilesDir(null), filename)
return FileInputStream(file).bufferedReader().use { it.readText() }
}
return null
}Here, FileInputStream(file).bufferedReader().use { it.readText() } is used to read the file content as a string. Also, note that when loading data, we need the READ_EXTERNAL_STORAGE permission if our app is running on Android 9 or lower.
3) Delete Data from External Storage:
To delete a file from external storage, check if the WRITE_EXTERNAL_STORAGE permission has been granted and if the storage is available, then call delete() on the File object:
fun deleteFile(context: Context, filename: String): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
return
}
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
val file = File(context.getExternalFilesDir(null), filename)
return file.delete()
}
return false
}Pay attention here that we are always dealing with the app-specific storage on the external storage by using getExternalFilesDir. This is due to scoped storage taking effect from Android 10 and above, where apps aren't allowed to access or modify files outside their specific storage.
Shared storage: saving, loading, and deleting
We've looked at how an app can interact with its own locked storage, either internal or external (also known as scoped storage from Android 10 onwards). This location is private to each app and cannot be accessed by other apps. But what if your app needs to store data that should be accessible by other apps or the user? For instance, a photo taken by your app that should appear in the user's gallery, or a PDF document that a PDF reader app should be able to access. In these scenarios, your app needs to use shared storage.
Shared storage is a common space all apps can access, given they have the needed permissions. Please note that from Android 10, direct access to shared storage is limited due to the introduction of scoped storage, and apps are encouraged to use specific APIs to interact with files they do not own. Also, remember that files stored in shared storage aren't deleted if the creating app gets uninstalled.
Interacting with shared storage can be complex due to the variety of data types and the need to handle permissions properly. So, a detailed discussion will be provided in separate topics. However, in this section, we'll provide an overview of the main APIs used for accessing shared storage:
1) MediaStore API:
The MediaStore API is primarily used when your app needs to access media files like pictures, audio, or video. This API provides a standardized way to access media files, manage their metadata, and organise them into collections based on media type.
Here's an example of how to save an image to the shared storage using the MediaStore API:
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "Image.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val imageUri = resolver.insert(MediaStoreConclusion
In conclusion, understanding the different types of storage in Android (internal, external/scoped, and shared) is crucial for managing files within your app effectively and securely. This topic provided a comprehensive overview of these storage types, their characteristics, and how to perform basic CRUD operations on them.
Remember, choosing the right type of storage for your app's needs is essential. Internal storage is best for small amounts of sensitive data, while external (scoped) storage is ideal for larger, non-critical files. Shared storage, on the other hand, is useful when your app needs to store data that should be accessible by other apps or the user. By understanding these concepts, you can create more secure and efficient apps that respect user privacy and provide a better user experience. Happy practicing.