Files: Internal & External

Room stores TrailLog's structured trail records, but a trail's photoUri field points at an actual image file living somewhere on the device — Room only stores the reference, not the bytes. Android gives you a few genuinely different places to put that file, each with its own trade-offs around privacy, permissions, and what happens when the app is uninstalled. This lesson walks through the options TrailLog would actually choose between.

Internal Storage: Private, App-Only, Auto-Deleted

context.filesDir points at a directory that belongs entirely to your app — no other app can read it, no runtime permission is ever required to use it, and Android automatically deletes everything inside it the moment the user uninstalls TrailLog. That combination makes it the right default for data your app needs but nobody else ever should.

Saving a trail photo to internal storage

fun savePhotoInternally(context: Context, trailId: Int, photoBytes: ByteArray): String {
    val photoFile = File(context.filesDir, "trail_$trailId.jpg")
    photoFile.outputStream().use { output ->
        output.write(photoBytes)
    }
    return photoFile.absolutePath
}

The returned path is exactly what you'd store in a Trail's photoUri field. Because it's private, this is a solid choice for a photo the user only ever expects to see inside TrailLog itself — but it's the wrong choice the moment you want that same photo to show up in the device's Gallery app too.

Scoped Storage and MediaStore: Sharing a Photo with the Gallery

Older Android versions let apps write directly to shared storage using raw file paths — which also meant any app with broad storage permission could quietly read or tamper with any other app's files. Starting with Android 10 (API 29), scoped storage tightened this deliberately: apps can no longer freely browse shared storage by path. Saving something the user's Gallery should see now goes through the MediaStore API instead, which hands the system control over where the file physically lives while still making it visible to every app that reads MediaStore, including Gallery. This is a genuine privacy improvement, not just extra friction — it closes off a long-standing way for one app to snoop on another's files.

Saving a trail photo so it's visible in the Gallery

fun savePhotoToGallery(context: Context, trailName: String, photoBytes: ByteArray): Uri? {
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "$trailName.jpg")
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/TrailLog")
    }
    val uri = context.contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
    ) ?: return null

    context.contentResolver.openOutputStream(uri)?.use { output ->
        output.write(photoBytes)
    }
    return uri
}

Notice there's no raw file path anywhere in this function — MediaStore hands back a content:// Uri, and no special storage permission is needed for an app to write its own media this way on modern Android.

The Middle Ground: App-Specific External Storage

context.getExternalFilesDir(null) sits between the two options above. Like internal storage, it requires no special permission on modern Android and is automatically cleaned up when TrailLog is uninstalled. Unlike internal storage, it isn't fully private — it lives on shared storage and, on older Android versions, could be inspected by a user connecting the device to a computer. It's a solid fit for app-owned files that don't need Gallery visibility but also aren't sensitive enough to require internal storage's stricter privacy:

Saving a camera-captured trail photo to app-specific external storage

fun saveCapturedPhoto(context: Context, trailId: Int, photoBytes: ByteArray): String? {
    val dir = context.getExternalFilesDir("trail_photos") ?: return null
    val photoFile = File(dir, "trail_$trailId.jpg")
    photoFile.outputStream().use { output ->
        output.write(photoBytes)
    }
    return photoFile.absolutePath
}

fun readSavedPhoto(context: Context, trailId: Int): ByteArray? {
    val dir = context.getExternalFilesDir("trail_photos") ?: return null
    val photoFile = File(dir, "trail_$trailId.jpg")
    return if (photoFile.exists()) photoFile.readBytes() else null
}

This is a natural landing spot for a photo taken with the device camera — CameraX covers actually capturing that photo; this lesson only covers where to put the file once you have it.

Choosing the Right Option for TrailLog

TrailLog needsBest fit
A trail photo the user should also see in GalleryMediaStore (scoped storage)
A camera-captured trail photo, app use onlyApp-specific external storage (getExternalFilesDir)
Cached or downloaded data (e.g. offline map tiles)Internal storage (filesDir) or cacheDir
A user-exported trail log file (e.g. shared as a file)MediaStore / Storage Access Framework, since another app needs to open it
Note: All the Kotlin above is correct, real Android storage code, but this website's own project doesn't run a live Android Studio build, so no file is actually written on this page.
Next up: Saving a file privately is only half the story — sharing that same file with another app (like a camera or gallery picker) safely needs a FileProvider. FileProvider & Content Providers covers that next.