Skip to content

Android Scoped Storage — Complete Guide

DodaTech Updated 2026-06-24 2 min read

In this tutorial, you'll learn about Android Scoped Storage. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

The Problem

Your app targeting Android 10+ can't read files that it didn't create, or MediaStore queries return no results.

Wrong Approach ❌

// Direct file path access — fails on scoped storage
val file = File("/storage/emulated/0/DCIM/Camera/photo.jpg")
val bitmap = BitmapFactory.decodeFile(file.absolutePath) // AccessDeniedException!

Output: FileNotFoundException or AccessDeniedException on Android 10+.

Right Approach ✅

class ScopedStorageManager(private val context: Context) {

    // Query MediaStore for images
    fun getRecentPhotos(limit: Int = 10): List<MediaFile> {
        val projection = arrayOf(
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media.DATE_ADDED,
            MediaStore.Images.Media.SIZE
        )

        val cursor = context.contentResolver.query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            projection,
            null, // selection
            null, // selectionArgs
            "${MediaStore.Images.Media.DATE_ADDED} DESC LIMIT $limit"
        )

        val files = mutableListOf<MediaFile>()
        cursor?.use {
            while (it.moveToNext()) {
                val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
                val name = it.getString(it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
                val uri = ContentUris.withAppendedId(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id
                )
                files.add(MediaFile(uri, name))
            }
        }
        return files
    }

    // Read file from MediaStore URI
    fun readPhoto(uri: Uri): Bitmap? {
        return try {
            context.contentResolver.openInputStream(uri)?.use { input ->
                BitmapFactory.decodeStream(input)
            }
        } catch (e: SecurityException) {
            null // No permission for this URI
        }
    }

    // Create a new file in MediaStore
    fun savePhoto(bitmap: Bitmap, name: String): Uri? {
        val contentValues = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, "$name.jpg")
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }

        val uri = context.contentResolver.insert(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        )

        uri?.let {
            context.contentResolver.openOutputStream(it)?.use { output ->
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, output)
            }

            contentValues.clear()
            contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
            context.contentResolver.update(it, contentValues, null, null)
        }

        return uri
    }

    // Request user to grant access to a file
    fun requestFileAccess(activity: Activity, uri: Uri, requestCode: Int) {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = "*/*"
            putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
        }
        activity.startActivityForResult(intent, requestCode)
    }

    data class MediaFile(val uri: Uri, val displayName: String)
}

Output: Proper file access using MediaStore on scoped storage.

Prevention

  • Use MediaStore to query and access shared media files.
  • Use IS_PENDING flag for atomic writes.
  • Use ACTION_OPEN_DOCUMENT / ACTION_CREATE_DOCUMENT for file picker access.
  • Never use hardcoded file paths on Android 10+.
  • Add Android:requestLegacyExternalStorage="true" (temporary, only for Migration).

Common Mistakes with storage scoped

  1. Mixing let bindings with <- bindings in do notation, producing type errors
  2. Overlapping type class instances that cause GHC to reject the program with ambiguous dispatch errors
  3. Non-exhaustive pattern matches that compile with warnings then crash at runtime

These mistakes appear frequently in real-world Android code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.

Practice Exercise

Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.

This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.

FAQ

### What is IS_PENDING in MediaStore?

When creating a file, set IS_PENDING = 1 to hide it from other apps during write. After writing, set it to 0 to make the file visible.

### Can I still use file paths on Android 10?

Only for app-specific directories (getExternalFilesDir()). For public directories, you must use MediaStore or SAF.

### What is the MANAGE_EXTERNAL_STORAGE permission?

A special permission for apps that need "all files access" (file managers, antivirus). It requires Google Play approval and is not available for most apps.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro