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

Android如何兼容API21及以上版本正确保存文件至外部存储公共目录?

分版本处理的方式是正确的

因为MediaStore.Downloads.EXTERNAL_CONTENT_URI是Android 10(API29)才引入的常量,API29及以下版本无法直接使用该字段,同时API29之前官方允许通过Environment.getExternalStoragePublicDirectory操作公共存储目录,因此分版本适配是唯一可行的兼容方案。

以下是完整的API21+兼容实现:

1. 权限配置

首先在AndroidManifest.xml中声明权限,限制仅在API29及以下版本生效:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
  • API23+版本需要动态申请WRITE_EXTERNAL_STORAGE权限;
  • API30+版本无需申请该权限,MediaStore API默认支持写入公共下载目录。

2. 核心实现代码

import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.core.content.FileProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

override suspend fun saveFile(byteArray: ByteArray, fileName: String): Uri? {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        saveFileToMediaStore(byteArray, fileName)
    } else {
        saveFileToExternalStorage(byteArray, fileName)
    }
}

// Android 10+ 使用MediaStore API保存
private suspend fun saveFileToMediaStore(byteArray: ByteArray, fileName: String): Uri? {
    val contentValues = ContentValues().apply {
        put(MediaStore.Downloads.DISPLAY_NAME, fileName)
        put(MediaStore.Downloads.MIME_TYPE, fileName.getMimeType())
        put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
        put(MediaStore.Downloads.SIZE, byteArray.size.toLong())
    }

    return withContext(Dispatchers.IO) {
        context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)?.also { uri ->
            context.contentResolver.openOutputStream(uri)?.use { outputStream ->
                outputStream.write(byteArray)
            }
        }
    }
}

// Android 21-29 使用旧版公共存储API保存
private suspend fun saveFileToExternalStorage(byteArray: ByteArray, fileName: String): Uri? {
    return withContext(Dispatchers.IO) {
        val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
        if (!downloadsDir.exists()) {
            downloadsDir.mkdirs()
        }

        val file = File(downloadsDir, fileName)
        return@withContext try {
            FileOutputStream(file).use { outputStream ->
                outputStream.write(byteArray)
            }
            // 适配Android 7.0+ 避免FileUriExposedException
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                getFileProviderUri(file)
            } else {
                Uri.fromFile(file)
            }
        } catch (e: IOException) {
            e.printStackTrace()
            null
        }
    }
}

// 适配Android 7.0+的FileProvider Uri生成
private fun getFileProviderUri(file: File): Uri {
    return FileProvider.getUriForFile(
        context,
        "${context.packageName}.fileprovider", // 替换为你的App的FileProvider授权名
        file
    )
}

// 辅助方法:根据文件名获取MIME类型
private fun String.getMimeType(): String {
    val extension = this.substringAfterLast(".", "")
    return android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "application/octet-stream"
}

3. FileProvider配置(Android 7.0+ 必需)

在AndroidManifest.xml的<application>标签内添加FileProvider配置:

<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文件:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="public_downloads"
        path="Download" />
</paths>

关键注意事项

  • 所有文件操作必须在IO线程执行,代码中通过withContext(Dispatchers.IO)确保;
  • API21-29版本必须先获取WRITE_EXTERNAL_STORAGE权限才能写入文件;
  • MIME类型获取方法可根据实际需求调整,确保与文件类型匹配。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 15:13:10