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 needs | Best fit |
|---|---|
| A trail photo the user should also see in Gallery | MediaStore (scoped storage) |
| A camera-captured trail photo, app use only | App-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 |