📱 Device Features · Lesson 36
Camera & Gallery (Pick Image)
Two ways to get a photo
Photo Picker
- User chooses an existing image
- No permission needed
- PickVisualMedia()
Camera
- User takes a new photo
- Camera app saves it to your file
- TakePicture() + FileProvider
Photo Picker (gallery)
Kotlin
// From Student Diary – AddEditNoteFragment
private val pickImage = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
if (uri != null) copyImage(uri) // null = user cancelled
}
binding.btnAddPhoto.setOnClickListener {
pickImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
// pick up to 5 images
private val pickMany = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(5)) { uris ->
adapter.submitList(uris)
}Keep the photo: copy to app storage
Kotlin – util/ImageStore.kt
package com.codingeasily.studentdiary.util
import android.content.Context
import android.net.Uri
import java.io.File
import java.util.UUID
/**
* The photo picker gives a temporary content:// Uri.
* We copy the picture into the app's private folder so it stays available.
*/
object ImageStore {
fun copyToAppStorage(context: Context, uri: Uri): String? = try {
val dir = File(context.filesDir, "images").apply { mkdirs() }
val file = File(dir, "${UUID.randomUUID()}.jpg")
context.contentResolver.openInputStream(uri)?.use { input ->
file.outputStream().use { output -> input.copyTo(output) }
}
file.absolutePath
} catch (e: Exception) {
null
}
fun delete(path: String?) {
if (path != null) File(path).delete()
}
}Kotlin – using it
private fun copyImage(uri: Uri) {
viewLifecycleOwner.lifecycleScope.launch {
val path = withContext(Dispatchers.IO) { ImageStore.copyToAppStorage(requireContext(), uri) }
if (path == null) {
Toast.makeText(requireContext(), R.string.image_error, Toast.LENGTH_SHORT).show()
} else {
imagePath = path // saved with the note in Room
Glide.with(this@AddEditNoteFragment).load(File(path)).centerCrop().into(binding.ivPhoto)
}
}
}Take a photo with the camera
Kotlin
class ProfileFragment : Fragment() {
private var photoUri: Uri? = null
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) photoUri?.let { Glide.with(this).load(it).circleCrop().into(binding.ivAvatar) }
}
private fun openCamera() {
val file = File(requireContext().filesDir, "images/avatar_${System.currentTimeMillis()}.jpg")
file.parentFile?.mkdirs()
val uri = FileProvider.getUriForFile(
requireContext(), "${requireContext().packageName}.fileprovider", file
)
photoUri = uri
takePicture.launch(uri) // the camera app writes the photo into our file
}
}FileProvider setup
Other apps (the camera) cannot write to your private folder directly. A FileProvider creates a safe content:// Uri for one file.
XML
<!-- AndroidManifest.xml – inside <application> -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- res/xml/file_paths.xml -->
<paths>
<files-path name="images" path="images/" />
</paths>Let the user choose: camera or gallery
Kotlin
private fun choosePhotoSource() {
val options = arrayOf("📷 Take photo", "🖼 Choose from gallery")
MaterialAlertDialogBuilder(requireContext())
.setTitle("Add photo")
.setItems(options) { _, which ->
if (which == 0) openCamera()
else pickImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
.show()
}Common mistakes
- Requesting READ_EXTERNAL_STORAGE / READ_MEDIA_IMAGES just to pick a photo → not needed with Photo Picker (and Play may reject the app).
- Passing
Uri.fromFile(file)to the camera →FileUriExposedException; use FileProvider. - Authority in code not matching the manifest → "Failed to find configured root".
- Saving full-size photos in Room as bytes → huge database; store the file path.
Summary
- Gallery:
PickVisualMedia(no permission); copy the result intofilesDir. - Camera:
TakePicture+ a FileProvider Uri. - Show with Glide; store only the path.
✅ Practice
Add a profile screen with a round avatar. Tapping it offers "Take photo" or "Choose from gallery"; the chosen image is saved and still visible after restarting the app.
Frequently Asked Questions
Does the Photo Picker work on old Android versions?
Yes. On devices without the system picker, AndroidX falls back to the document picker (ACTION_OPEN_DOCUMENT), so the same code works on Android 5+.
Do I need CAMERA permission for TakePicture?
No, the camera app takes the photo. But if your manifest declares the CAMERA permission for another feature, you must request it before launching TakePicture.
Why copy the picked image?
The Uri from the picker may stop working after the app restarts. Copying the file into filesDir gives you a permanent path you can store in Room.
