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

Android 10以下无需WRITE_STORAGE权限保存文件至下载文件夹

解决方案:Android全版本适配下载文件夹文件保存

首先明确:没有官方或第三方支持库能让Android 9及以下系统的MediaStore直接提供下载文件夹的URI,但可以通过以下几种方案实现适配,尽量减少权限弹窗对用户的打扰:

方案1:利用DownloadManager服务(推荐)

DownloadManager是系统内置服务,本身拥有写入公共下载文件夹的权限,因此在所有Android版本中,应用调用它保存文件到下载文件夹时,无需申请WRITE_EXTERNAL_STORAGE权限,也不会弹出授权弹窗。

实现思路

如果你的文件是本地已有的字节流或文件:

  1. 先将文件写入应用内部存储(无需权限);
  2. 通过FileProvider生成系统可访问的Content URI;
  3. 调用DownloadManager将文件复制到公共下载文件夹。

示例代码:

// 1. 将文件写入应用内部存储
val inputStream: InputStream = ... // 你的文件输入流
val internalTempFile = File(filesDir, "temp_file.ext")
FileOutputStream(internalTempFile).use { output ->
    inputStream.copyTo(output)
}

// 2. 通过FileProvider生成Content URI(需提前配置)
val contentUri = FileProvider.getUriForFile(
    context,
    "${packageName}.fileprovider",
    internalTempFile
)

// 3. 调用DownloadManager完成保存
val downloadManager = getSystemService(DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(contentUri)
    .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "final_file.ext")
    .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) // 可选:显示下载完成通知

downloadManager.enqueue(request)

// 可选:清理内部存储的临时文件
internalTempFile.delete()

需要在AndroidManifest.xml中配置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">
    <files-path name="internal_files" path="." />
</paths>

方案2:版本区分的权限适配

如果不想依赖DownloadManager,可以针对系统版本做逻辑拆分:

  • Android 10及以上:直接使用MediaStore插入下载文件夹条目,无需权限;
  • Android 9及以下:申请WRITE_EXTERNAL_STORAGE权限,通过旧API获取下载文件夹路径并写入文件。

示例代码:

private const val PERMISSION_REQUEST_CODE = 1001

fun saveFileToDownloads(context: Context, fileName: String, content: ByteArray) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Android 10+:用MediaStore直接写入
        val contentValues = ContentValues().apply {
            put(MediaStore.Downloads.DISPLAY_NAME, fileName)
            put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
            put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
        }

        val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
        uri?.let {
            context.contentResolver.openOutputStream(it).use { outputStream ->
                outputStream?.write(content)
            }
        }
    } else {
        // Android <10:先申请权限再写入
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                context as Activity,
                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                PERMISSION_REQUEST_CODE
            )
            return
        }

        val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
        val targetFile = File(downloadsDir, fileName)
        FileOutputStream(targetFile).use { it.write(content) }
    }
}

方案3:引导用户使用SAF(存储访问框架)

对于Android <10的设备,也可以引导用户通过SAF选择下载文件夹,获取写入权限,这种方式无需申请WRITE_EXTERNAL_STORAGE,但需要用户手动操作一次。

示例代码:

private const val SAF_REQUEST_CODE = 1002

fun openSAFForDownloads(context: Context) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
    // 默认定位到下载文件夹
    val downloadsUri = DocumentsContract.buildDocumentUriUsingTree(
        MediaStore.Downloads.EXTERNAL_CONTENT_URI,
        DocumentsContract.getTreeDocumentId(MediaStore.Downloads.EXTERNAL_CONTENT_URI)
    )
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, downloadsUri)
    (context as Activity).startActivityForResult(intent, SAF_REQUEST_CODE)
}

// 在Activity的onActivityResult中处理返回结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == SAF_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        data?.data?.let { treeUri ->
            // 持久化权限,避免重复请求
            contentResolver.takePersistableUriPermission(
                treeUri,
                Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )
            // 写入文件
            val downloadsDir = DocumentFile.fromTreeUri(this, treeUri)
            downloadsDir?.createFile("application/octet-stream", "final_file.ext")?.let { fileUri ->
                contentResolver.openOutputStream(fileUri.uri).use { it?.write(content) }
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 06:06:45