Skip to content
intermediate Phase 7 · Data & Persistence

File Storage

Read and write files to internal and external storage with proper permission handling.

35m
2 problems
Topic Progress 0%

Internal vs External Storage

Storage Types

Android offers three primary file storage locations:

Internal Storage — Private to your app, automatically deleted when the app is uninstalled.

// Writing to internal storage
val file = File(context.filesDir, "config.json")
file.writeText("{\"theme\": \"dark\"}")

// Reading from internal storage
val content = file.readText()

No permissions needed. Files are in data/data/com.example.app/files/.

External Storage — Shared with other apps, requires permissions on older Android versions.

// Scoped storage (Android 10+)
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "report.pdf")
file.outputStream().use { os ->
    pdfBytes.inputStream().use { it.copyTo(os) }
}

External files dir is in Android/data/com.example.app/files/ — still app-private on modern Android.

Cache Storage — Temporary files the system may delete.

val cacheFile = File(context.cacheDir, "temp_image.jpg")
// System deletes cache files when storage is low

Choosing the Right Storage

Use Case Storage Type
App settings, configs Internal
User documents, exports External files
Temporary downloads Cache
Shared media (photos) MediaStore / SAF
Large datasets Internal or external

File Paths

// Internal
context.filesDir          // /data/data/pkg/files
context.cacheDir          // /data/data/pkg/cache
context.getDir("data", 0) // /data/data/pkg/app_data

// External (app-private)
context.getExternalFilesDir(null)              // Android/data/pkg/files
context.getExternalFilesDir(DIRECTORY_PICTURES) // Android/data/pkg/files/Pictures
context.externalCacheDir                        // Android/data/pkg/cache

File Safety

Always check if a file exists before reading, and wrap operations in try-catch:

fun readFileSafely(context: Context, filename: String): String? {
    return try {
        File(context.filesDir, filename).takeIf { it.exists() }?.readText()
    } catch (e: IOException) {
        Log.e("FileStorage", "Failed to read $filename", e)
        null
    }
}

Permissions and Scoped Storage

The Permission Landscape

Storage permissions have evolved significantly across Android versions:

Android 9 and below: READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE give broad access to all files.

Android 10 (Scoped Storage): Apps can only access their own files and media they created. requestLegacyExternalStorage = false by default.

Android 11+: MANAGE_EXTERNAL_STORAGE for file managers and backup apps only. MediaStore for shared media.

Android 13+: Granular media permissions replace READ_EXTERNAL_STORAGE.

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

Requesting Permissions at Runtime

class StorageActivity : AppCompatActivity() {
    private val permissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val allGranted = permissions.values.all { it }
        if (allGranted) loadMedia()
        else showPermissionDeniedMessage()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // Android 13+: request granular media permissions
            permissionLauncher.launch(arrayOf(
                Manifest.permission.READ_MEDIA_IMAGES,
                Manifest.permission.READ_MEDIA_VIDEO
            ))
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            // Android 6-12: request READ_EXTERNAL_STORAGE
            permissionLauncher.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE))
        } else {
            loadMedia()
        }
    }
}

Writing to Shared Storage with MediaStore

suspend fun saveImageToGallery(context: Context, bitmap: Bitmap, filename: String) {
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, filename)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }
    }
    
    val uri = context.contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
    ) ?: return
    
    context.contentResolver.openOutputStream(uri)?.use { os ->
        bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os)
    }
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        contentValues.clear()
        contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
        context.contentResolver.update(uri, contentValues, null, null)
    }
}

IS_PENDING prevents other apps from seeing the file until it's fully written.

Safe File Operations and Best Practices

Kotlin Extension Functions

Wrap file operations in extension functions for consistency:

fun Context.safeWrite(filename: String, content: String): Boolean {
    return try {
        File(filesDir, filename).writeText(content)
        true
    } catch (e: IOException) {
        Log.e("FileStorage", "Write failed: $filename", e)
        false
    }
}

fun Context.safeRead(filename: String): String? {
    return try {
        val file = File(filesDir, filename)
        if (file.exists()) file.readText() else null
    } catch (e: IOException) {
        Log.e("FileStorage", "Read failed: $filename", e)
        null
    }
}

Atomic Writes

Write to a temp file first, then rename. This prevents corruption if the app crashes mid-write:

suspend fun atomicWrite(context: Context, filename: String, content: String) {
    withContext(Dispatchers.IO) {
        val target = File(context.filesDir, filename)
        val temp = File(context.filesDir, "$filename.tmp")
        
        temp.writeText(content)
        temp.renameTo(target)
    }
}

Streaming Large Files

Don't load entire files into memory. Use streams:

fun copyFile(source: File, dest: File, onProgress: ((Long) -> Unit)? = null) {
    source.inputStream().use { input ->
        dest.outputStream().use { output ->
            val buffer = ByteArray(8192)
            var bytesRead: Int
            var totalBytes = 0L
            
            while (input.read(buffer).also { bytesRead = it } != -1) {
                output.write(buffer, 0, bytesRead)
                totalBytes += bytesRead
                onProgress?.invoke(totalBytes)
            }
        }
    }
}

Cleanup

Delete files you no longer need. Use cacheDir for temporary data the system can also reclaim:

fun cleanupOldFiles(dir: File, maxAgeMs: Long) {
    dir.listFiles()?.filter { file ->
        System.currentTimeMillis() - file.lastModified() > maxAgeMs
    }?.forEach { it.delete() }
}

Quiz

1. Which storage location requires no runtime permissions and is private to your app?

Question 1 options

2. What does IS_PENDING do in MediaStore ContentValues on Android 10+?

Question 2 options

3. Why use atomic writes (write-to-temp then rename) for critical data?

Question 3 options

4. Which permission model replaced READ_EXTERNAL_STORAGE on Android 13?

Question 4 options

Flashcards

Question

What is the difference between filesDir and getExternalFilesDir()?

Answer

filesDir is internal storage (always private, no permissions). getExternalFilesDir() is app-private external storage (deleted on uninstall, no permissions needed on Android 10+).

Question

When should you use cacheDir instead of filesDir?

Answer

For temporary files the system can reclaim when storage is low. Cache files may disappear without the app deleting them.

Question

How does Scoped Storage restrict file access on Android 10+?

Answer

Apps can only access their own app-private files and media they created. Accessing shared files requires MediaStore or Storage Access Framework.

Question

What permission is needed to save files to the device gallery on Android 13?

Answer

No permission needed to write via MediaStore. READ_MEDIA_IMAGES or READ_MEDIA_VIDEO are only needed to read other apps' media.

Revision Notes

Key Takeaways

  • 1. Internal storage is always private and requires no permissions
  • 2. Scoped Storage on Android 10+ restricts apps to their own files and media they created
  • 3. Atomic writes prevent corruption when the app crashes mid-write
  • 4. MediaStore is the correct way to share media files with other apps

Interview Tips

  • Know the storage permission model for each Android version — interviewers ask about Scoped Storage
  • Explain why you chose internal vs external storage for a given scenario
  • Discuss atomic writes as a pattern for data integrity
  • Be ready to describe the MediaStore insertion flow with IS_PENDING

Cheat Sheet

File Storage Cheat Sheet

Internal Storage:

  • context.filesDir — private, no permissions
  • context.cacheDir — system-reclaimable temp files

External App-Private:

  • context.getExternalFilesDir() — private, no permissions on Android 10+
  • Deleted when app is uninstalled

Shared Storage:

  • MediaStore API for photos, videos, audio
  • SAF for user-selected files

Permissions by Android Version:

  • Android 9-: READ/WRITE_EXTERNAL_STORAGE
  • Android 10: Scoped Storage (no broad access)
  • Android 11+: MANAGE_EXTERNAL_STORAGE for file managers only
  • Android 13+: READ_MEDIA_IMAGES/VIDEO/AUDIO

Best Practices:

  • Use atomic writes (temp file + rename)
  • Stream large files, don't load into memory
  • Clean up old files periodically