You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android API 29及以上图片/视频添加到图库实现与旧兼容问题处理

问题原因
  • Android 10(API 29)开始MediaStore.MediaColumns.DATA字段正式废弃,系统禁止应用直接通过文件路径操作公共目录媒体文件,原有直接写入路径的逻辑在API 29及以上版本会失效。
  • 视频时长显示为00:00的核心原因是插入媒体库时未填写MediaStore.Video.Media.DURATION字段,系统无法读取到视频时长信息,默认显示为0。
适配方案

前置权限配置

在AndroidManifest.xml中添加对应权限:

<!-- Android 13及以上 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Android 10-12,仅保存自身应用生成的媒体文件时可省略 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

修复后完整代码

import android.content.ContentValues
import android.content.Context
import android.media.MediaMetadataRetriever
import android.os.Build
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import java.io.File
import java.io.FileInputStream
import java.io.OutputStream

fun Context.addToGallery(file: File) {
    if (!file.exists()) return

    // 区分媒体类型
    val isVideo = file.extension.equals("mp4", ignoreCase = true)
    val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension) ?: if (isVideo) "video/mp4" else "image/jpeg"

    // 构建ContentValues
    val values = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, file.name)
        put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
        put(MediaStore.MediaColumns.SIZE, file.length())
        // API 29及以上指定存储路径,默认存到公共DCIM目录
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.MediaColumns.RELATIVE_PATH, if (isVideo) "DCIM/Video" else "DCIM/Image")
            put(MediaStore.MediaColumns.IS_PENDING, 1)
        }

        // 视频补充时长字段
        if (isVideo) {
            val duration = getVideoDuration(file)
            if (duration > 0) {
                put(MediaStore.Video.Media.DURATION, duration)
            }
        }
    }

    val contentUri = if (isVideo) {
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }

    contentResolver.insert(contentUri, values)?.let { uri ->
        var outputStream: OutputStream? = null
        var inputStream: FileInputStream? = null
        try {
            outputStream = contentResolver.openOutputStream(uri)
            inputStream = FileInputStream(file)
            inputStream.copyTo(outputStream!!)

            // 写入完成后更新状态,通知系统扫描
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                values.clear()
                values.put(MediaStore.MediaColumns.IS_PENDING, 0)
                contentResolver.update(uri, values, null, null)
            }
        } catch (e: Exception) {
            // 写入失败删除无效uri
            contentResolver.delete(uri, null, null)
            e.printStackTrace()
        } finally {
            outputStream?.close()
            inputStream?.close()
        }
    }
}

// 读取本地视频文件时长
private fun getVideoDuration(videoFile: File): Long {
    val retriever = MediaMetadataRetriever()
    return try {
        retriever.setDataSource(videoFile.absolutePath)
        val durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
        durationStr?.toLongOrNull() ?: 0L
    } catch (e: Exception) {
        e.printStackTrace()
        0L
    } finally {
        retriever.release()
    }
}

关键逻辑说明

  • 放弃直接写入DATA字段的逻辑,改为通过contentResolver.insert拿到媒体uri后,通过流拷贝的方式写入文件内容,兼容所有Android版本。
  • 新增视频时长读取逻辑,将时长写入MediaStore.Video.Media.DURATION字段,修复图库显示视频时长为0的问题。
  • API 29及以上通过RELATIVE_PATH指定媒体文件存储到公共DCIM目录,设置IS_PENDING标记避免系统在文件写入完成前扫描媒体。
  • 增加异常处理逻辑,写入失败时自动删除无效的媒体库记录,避免图库出现空白无效文件。

内容的提问来源于stack exchange,提问作者mahmoud mahmoudi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 22:27:05