Mobile Development 12 min read

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.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Saving Images to Android Gallery Using the MediaStore API

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Mobile DevelopmentAndroidKotlinFileProviderMediaStoreImage Saving
Sohu Tech Products
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.