Managing Android scoped storage file I/O used to be simple. You requested WRITE_EXTERNAL_STORAGE and saved files wherever you wanted. However, the introduction of Scoped Storage in Android 10 (API 29) changed everything, instantly breaking file management in thousands of apps.
Today, trying to write a basic text file to a public folder often throws a frustrating SecurityException. Developers are now forced to navigate complex URIs instead of using standard Java file paths.
Fortunately, the MediaStore API offers a clear, secure path forward. This guide will show you exactly how to implement an Android MediaStore save file operation and read data directly from the public Downloads directory using Kotlin and Jetpack Compose.
Why Developers Need the MediaStore API
Direct file path manipulation is largely deprecated for public folders. If you want to save media or documents to directories like Downloads, Pictures, or Music, MediaStore is the modern standard.
Here is why you should adopt this approach:
- Privacy & Security: The system follows the principle of least privilege. Your application only accesses the specific data it needs.
- Shared Storage Access: Files saved via MediaStore remain accessible to other apps. They also persist on the device even if the user uninstalls your application.
- Zero Permission Requirements: On Android 10 and above, you do not need to request storage permissions to save files to common collections, provided your app is the one that created them.
How to Save a File Using MediaStore Android
To write a file, we utilize ContentValues to structure the file’s metadata. We then insert this data into the ContentResolver to generate a secure URI.
Writing the Save Logic in Kotlin
private fun saveFile(context: Context, name: String, content: String) {
val contentResolver = context.contentResolver
// MediaStore.Downloads requires API 29+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
uri?.let {
contentResolver.openOutputStream(it)?.use { stream ->
stream.write(content.toByteArray())
}
Toast.makeText(context, "Saved to Downloads!", Toast.LENGTH_SHORT).show()
}
}
Key Breakdown:
ContentValues: We define theDISPLAY_NAME(filename),MIME_TYPE(format), andRELATIVE_PATH(the specific sub-folder).EXTERNAL_CONTENT_URI: This represents the destination table. For documents and generic text,MediaStore.Downloadsis the correct collection.openOutputStream(uri): The system creates a placeholder and returns a URI. We use this URI to stream our byte content safely.
How to Read a File from the Downloads Folder Android
Reading a file requires two specific steps. First, we query the system to locate the file’s unique ID. Next, we open an input stream using that specific identifier.
Implementing the Read Logic
private fun readFile(context: Context, name: String): String? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null
val contentResolver = context.contentResolver
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
val args = arrayOf(name)
val uri = contentResolver.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.MediaColumns._ID),
selection, args, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
} else null
}
return uri?.let {
contentResolver.openInputStream(it)?.bufferedReader()?.readText()
}
}
Key Breakdown:
query(): We search the shared storage collection for a record matching our specificDISPLAY_NAME.Cursor: The code iterates through the database results to extract the exact_IDof our target file.ContentUris.withAppendedId(): This utility function combines the base collection URI with the file ID. This creates a direct, resolvable link to the file.
Integrating MediaStore with Jetpack Compose
To see this in action, let’s wrap our logic inside a modern Jetpack Compose save file UI. This screen allows users to input text and execute the I/O operations seamlessly.
@Composable
fun ScopedStorageUI() {
val context = LocalContext.current
var input by remember { mutableStateOf("") }
var output by remember { mutableStateOf("No content yet") }
val fileName = "My_New_File.txt"
Column(Modifier.padding(20.dp).fillMaxSize()) {
TextField(
value = input,
onValueChange = { input = it },
placeholder = { Text("Enter text to save...") },
modifier = Modifier.fillMaxWidth()
)
Button(onClick = { saveFile(context, fileName, input) }) {
Text("Save to Downloads")
}
Button(onClick = { output = readFile(context, fileName) ?: "File not found" }) {
Text("Read from Downloads")
}
Text(text = "File Content: $output")
}
}
By utilizing the ContentResolver alongside Jetpack Compose, you easily bypass legacy file path limitations. Your app remains fully compliant with modern Android security standards.
You can also adapt this exact pattern for MediaStore.Images or MediaStore.Audio to handle dynamic media assets.
Frequently Asked Questions (FAQ)
Do I need storage permissions for Android 10+?
No. If your application targets API 29 or higher, you do not need WRITE_EXTERNAL_STORAGE to save files to public directories like Downloads or Pictures, as long as your app is the one creating those files.
What is the difference between MediaStore and FileProvider?
The MediaStore API is used to interact with shared, public media and documents (like saving a photo to the gallery). FileProvider is used to securely share your app’s private, internal files with other applications (like sharing a generated PDF via email).
How do I save images using the Android MediaStore API?
The logic is nearly identical to saving text. Instead of MediaStore.Downloads, you target MediaStore.Images.Media.EXTERNAL_CONTENT_URI. You will also need to update the MIME_TYPE to an image format like image/jpeg or image/png.
📌 Full Course Playlist https://www.youtube.com/playlist?list=PLO1OrQEU0vHNmD9Xqzs-qXwzzwrDvdhVu
~ ~ THANK YOU FOR READING ~ ~