Android Shared Storage with Kotlin — Scoped Storage, MediaStore and File Access Guide

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.

Android shared storage in Kotlin with scoped storage, MediaStore and file access

Android shared storage in Kotlin including scoped storage, MediaStore and file access.

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 filesDir for internal and getExternalFilesDir() 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
⚠️ Important: 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 wrap InputStream and OutputStream in .use{} to ensure they're closed automatically and prevent memory leaks.
  • Check available space before writing large files — use File.usableSpace to 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.

📚 Continue Learning
📝 Summary
  • 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

Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

1 Comments

Please let us know about any concerns or query.

Previous Post Next Post

Contact Form