Saving Images to Android Gallery Using the MediaStore API
This article explains how to save images to the Android gallery using the MediaStore API, covering permission changes across Android versions, the insertion and output stream workflow, code examples in Kotlin, and considerations for sharing images without unnecessary FileProvider usage.
Google has tightened privacy and system security on Android, making direct file access increasingly difficult. Starting with Android 6.0, runtime storage permissions are required; Android 10 introduced scoped storage (which can be disabled with android:requestLegacyExternalStorage="true"), and Android 11 enforces scoped storage, requiring all apps targeting API 30 to use MediaStore for shared media access.
The recommended approach is to use the MediaStore framework, which behaves like a database for media files. The general workflow is:
Insert a new image record into MediaStore to obtain a content Uri.
Open an output stream for that Uri and write the image data.
For Android 10+ set the IS_PENDING flag to 0 after writing so other apps can see the file.
Below are the essential code snippets.
<!--Android Q之后不需要存储权限,完全使用MediaStore API来实现-->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> private fun saveImageInternal() {
val uri = assets.open("wallhaven_rdyyjm.jpg").use {
it.saveToAlbum(this, fileName = "save_wallhaven_rdyyjm.jpg", null)
} ?: return
Toast.makeText(this, uri.toString(), Toast.LENGTH_SHORT).show()
} /**
* 保存 Bitmap 到相册的 Pictures 文件夹
*
* @param context 上下文
* @param fileName 文件名,需要带后缀
* @param relativePath 相对于 Pictures 的路径
* @param quality 图片质量
*/
fun Bitmap.saveToAlbum(
context: Context,
fileName: String,
relativePath: String? = null,
quality: Int = 75
): Uri? {
val resolver = context.contentResolver
val outputFile = OutputFileTaker()
// 插入图片信息
val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile)
if (imageUri == null) {
Log.w(TAG, "insert: error: uri == null")
return null
}
// 通过 Uri 打开输出流并写入图片
(imageUri.outputStream(resolver) ?: return null).use {
val format = fileName.getBitmapFormat()
[email protected](format, quality, it)
// 更新 IS_PENDING 状态
imageUri.finishPending(context, resolver, outputFile.file)
}
return imageUri
} private fun ContentResolver.insertMediaImage(
fileName: String,
relativePath: String?,
outputFileTaker: OutputFileTaker? = null
): Uri? {
val imageValues = ContentValues().apply {
val mimeType = fileName.getMimeType()
if (mimeType != null) {
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
}
val date = System.currentTimeMillis() / 1000
put(MediaStore.Images.Media.DATE_ADDED, date)
put(MediaStore.Images.Media.DATE_MODIFIED, date)
}
val collection: Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val path = if (relativePath != null) "${ALBUM_DIR}/${relativePath}" else ALBUM_DIR
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.RELATIVE_PATH, path)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
val pictures = Environment.getExternalStoragePublicDirectory(ALBUM_DIR)
val saveDir = if (relativePath != null) File(pictures, relativePath) else pictures
if (!saveDir.exists() && !saveDir.mkdirs()) {
Log.e(TAG, "save: error: can't create Pictures directory")
return null
}
var imageFile = File(saveDir, fileName)
val fileNameWithoutExtension = imageFile.nameWithoutExtension
val fileExtension = imageFile.extension
var queryUri = this.queryMediaImage28(imageFile.absolutePath)
var suffix = 1
while (queryUri != null) {
val newName = "${fileNameWithoutExtension}(${suffix++}).${fileExtension}"
imageFile = File(saveDir, newName)
queryUri = this.queryMediaImage28(imageFile.absolutePath)
}
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
val imagePath = imageFile.absolutePath
Log.v(TAG, "save file: $imagePath")
put(MediaStore.Images.Media.DATA, imagePath)
}
outputFileTaker?.file = imageFile
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
return this.insert(collection, imageValues)
} private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null
val imageFile = File(imagePath)
if (imageFile.canRead() && imageFile.exists()) {
Log.v(TAG, "query: path: $imagePath exists")
return Uri.fromFile(imageFile)
}
val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val query = this.query(
collection,
arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA),
"${MediaStore.Images.Media.DATA} = ?",
arrayOf(imagePath), null
)
query?.use {
while (it.moveToNext()) {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val id = it.getLong(idColumn)
val existsUri = ContentUris.withAppendedId(collection, id)
Log.v(TAG, "query: path: $imagePath exists uri: $existsUri")
return existsUri
}
}
return null
} private fun Uri.finishPending(
context: Context,
resolver: ContentResolver,
outputFile: File?
) {
val imageValues = ContentValues()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (outputFile != null) {
imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
}
resolver.update(this, imageValues, null, null)
val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
context.sendBroadcast(intent)
} else {
imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(this, imageValues, null, null)
}
} private fun shareImageInternal() {
val uri = assets.open("wallhaven_rdyyjm.jpg").use {
it.saveToAlbum(this, fileName = "save_wallhaven_rdyyjm.jpg", null)
} ?: return
val intent = Intent(Intent.ACTION_SEND)
.putExtra(Intent.EXTRA_STREAM, uri)
.setType("image/*")
startActivity(Intent.createChooser(intent, null))
}The article also clarifies when a FileProvider is actually needed: only when sharing files that reside in the app’s private sandbox (e.g., sharing an APK or a private image to WeChat). Images saved to the system gallery are already in a public location, so a FileProvider is unnecessary and can be omitted to avoid extra initialization overhead.
References are provided for the MediaStore demo repository and official Android documentation.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
