Managing files on Android has changed significantly over the years. With the introduction of Scoped Storage in Android 10 and further restrictions in Android 13, understanding which API to use — and when — is essential for building modern, privacy-respecting Android apps.
In this guide you'll learn the key concepts of Android Shared Storage with Kotlin, covering storage architecture, permissions history, and practical code examples for the most common file operations.
Storage Architecture Overview
Android provides two main storage areas for your app:
- App-specific storage (Internal/External) — files only accessible to your app. Deleted when the app is uninstalled. Use
filesDirfor internal andgetExternalFilesDir()for external app-specific storage. No permissions required. - Shared storage — files accessible to other apps and the user. Persists after uninstall. Divided into Media (photos, videos, audio) and Documents (PDF, DOCX, ZIP).
Storage Permissions - Android Version History
Storage permissions have changed significantly across Android versions. Here's a quick reference:
| Android Version | Read Media | Write Media | Documents |
|---|---|---|---|
| ≤ Android 9 (API 28) | READ_EXTERNAL_STORAGE |
WRITE_EXTERNAL_STORAGE |
SAF or permission |
| Android 10 (API 29) | READ_EXTERNAL_STORAGE |
No permission needed ✅ | SAF only |
| Android 11 (API 30) | READ_EXTERNAL_STORAGE |
No permission needed ✅ | SAF only |
| Android 13+ (API 33+) | READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIO |
No permission needed ✅ | SAF only |
WRITE_EXTERNAL_STORAGE is deprecated from Android 10 onwards. Remove it from your manifest if you're targeting API 29+. Adding files to shared storage via MediaStore no longer requires any write permission.
Common Storage Use Cases
1. Download a file to app-specific internal storage
Use context.filesDir to store files only your app can access. No permissions needed. Files are deleted when the app is uninstalled.
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
fun downloadToInternalStorage(context: Context, url: String) {
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
// .use() automatically closes the network socket — prevents memory leaks
client.newCall(request).execute().use { response ->
response.body?.byteStream()?.use { input ->
// Stored in app's internal storage — not accessible to other apps
val target = File(context.filesDir, "user-config.json")
target.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
2. Choose storage based on available space
When downloading large files, check available space first and fall back to external app-specific storage if internal storage is insufficient:
import java.io.File
import java.io.IOException
fun getStorageTarget(context: Context, fileSize: Long): File {
return when {
context.filesDir.usableSpace > fileSize -> context.filesDir
else -> {
context.getExternalFilesDirs(null)
.firstOrNull { it?.usableSpace ?: 0L > fileSize }
?: throw IOException("Not enough storage space available")
}
}
}
// Usage
val fileSize = 500_000_000L // 500MB
val targetDir = getStorageTarget(context, fileSize)
val file = File(targetDir, "large-asset.bin")
3. Save an image to shared storage (MediaStore)
To save an image so it appears in the user's gallery and is accessible to other apps, use MediaStore. No write permission needed from Android 10+:
import android.content.ContentValues
import android.graphics.Bitmap
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
fun saveImageToGallery(context: Context, bitmap: Bitmap, fileName: String) {
val filename = "${fileName}_${System.currentTimeMillis()}.jpg"
var outputStream: OutputStream? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10+ — use MediaStore, no permission required
context.contentResolver?.also { resolver ->
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val imageUri = resolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
outputStream = imageUri?.let { resolver.openOutputStream(it) }
}
} else {
// Android 9 and below — requires WRITE_EXTERNAL_STORAGE permission
val imagesDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
)
if (!imagesDir.exists()) imagesDir.mkdirs()
val image = File(imagesDir, filename)
outputStream = FileOutputStream(image)
// Notify media scanner so the image appears in gallery
MediaScannerConnection.scanFile(
context,
arrayOf(image.toString()),
arrayOf("image/jpeg")
) { path, uri ->
Log.d("MediaScanner", "Saved: $path")
}
}
outputStream?.use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, it)
}
}
4. Read an image from shared storage
To read media files, request READ_MEDIA_IMAGES (Android 13+) or READ_EXTERNAL_STORAGE (Android 12 and below), then query MediaStore:
import android.provider.MediaStore
import android.graphics.Bitmap
import android.graphics.BitmapFactory
fun loadImagesFromGallery(context: Context): List<Bitmap> {
val bitmaps = mutableListOf<Bitmap>()
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME
)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val contentUri = android.net.Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id.toString()
)
context.contentResolver.openInputStream(contentUri)?.use { stream ->
BitmapFactory.decodeStream(stream)?.let { bitmaps.add(it) }
}
}
}
return bitmaps
}
5. Select a document with the document picker (View-based)
Use the Storage Access Framework (SAF) via ActivityResultLauncher to let users pick documents. No permission needed — user explicitly grants access:
import androidx.activity.result.contract.ActivityResultContracts
import java.io.File
class MainActivity : AppCompatActivity() {
// Register document picker — handles the Intent result automatically
private val documentPicker = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri ?: return@registerForActivityResult
// Read the selected document
contentResolver.openInputStream(uri)?.use { inputStream ->
val target = File(filesDir, "selected-document.pdf")
target.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
// Launch the picker — pass MIME types you want to accept
private fun openDocumentPicker() {
documentPicker.launch(arrayOf("application/pdf", "application/msword"))
}
}
6. Photo Picker (Android 13+ recommended)
The Photo Picker is the modern, privacy-friendly way to select images and videos. It requires no permissions and gives users granular control over which media they share with the app:
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
class MainActivity : AppCompatActivity() {
// Single image picker
private val pickImage = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
uri ?: return@registerForActivityResult
// Use the URI — it's a content:// URI with temporary read access
binding.imageView.setImageURI(uri)
}
// Multiple images picker (up to 5)
private val pickMultipleImages = registerForActivityResult(
ActivityResultContracts.PickMultipleVisualMedia(maxItems = 5)
) { uris ->
uris.forEach { uri ->
// Process each selected image URI
}
}
private fun launchPhotoPicker() {
pickImage.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
}
Best Practices
- Use app-specific storage by default — only use shared storage if the files genuinely need to be accessible to other apps or persist after uninstall. App-specific storage requires zero permissions.
- Use the Photo Picker over READ_MEDIA_IMAGES — the Photo Picker requires no permissions and is available back to Android 4.4 via the Jetpack backport. It gives users better privacy controls and is Google's recommended approach.
- Remove WRITE_EXTERNAL_STORAGE from your manifest — it's deprecated, ignored on Android 10+, and its presence can confuse users seeing it in the permissions list.
- Handle storage operations off the main thread — always use coroutines or a background thread for file I/O. Blocking the main thread causes ANR (Application Not Responding) errors.
- Use
.use()for streams — always wrapInputStreamandOutputStreamin.use{}to ensure they're closed automatically and prevent memory leaks. - Check available space before writing large files — use
File.usableSpaceto verify enough space exists before starting a download.
Frequently Asked Questions
What is Scoped Storage in Android?
Scoped Storage, introduced in Android 10, restricts apps to accessing only their own files and specific media collections. Apps can no longer freely read or write to the entire external storage. This improves user privacy by preventing apps from accessing files they don't own.
Do I need permission to save a photo to the gallery on Android 10+?
No — from Android 10 onwards, you can save images to shared storage using MediaStore without any write permission. WRITE_EXTERNAL_STORAGE is deprecated and no longer required.
What is the difference between MediaStore and SAF?
MediaStore is used to access media files (photos, videos, audio) in shared media collections. SAF (Storage Access Framework) is used to access document files (PDF, DOCX, ZIP) through a system file picker. Use MediaStore for media, SAF for documents.
Should I use Photo Picker or READ_MEDIA_IMAGES permission?
Use the Photo Picker whenever possible. It requires zero permissions, gives users better privacy controls, and is available via Jetpack back to Android 4.4. Only use READ_MEDIA_IMAGES if you need direct access to the full media library without a picker UI.
- Use app-specific storage (
filesDir) by default — no permissions needed - Use MediaStore for saving/reading photos, videos and audio to shared storage
- Use SAF (document picker) for PDF, DOCX, ZIP and other document files
- Use Photo Picker for selecting images — no permissions, best user privacy
- WRITE_EXTERNAL_STORAGE is deprecated from Android 10 — remove it
- On Android 13+, use READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIO instead of READ_EXTERNAL_STORAGE
- Always run file I/O on a background thread using coroutines
- Always use .use() to auto-close streams and prevent memory leaks
thanks for sharing this with us
ReplyDelete