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

Android:如何将PDF下载到选定目录?文件未保存至目标路径问题

问题分析与解决方案

问题根源

你使用setDestinationInExternalPublicDir()的方式完全错误。这个方法的第一个参数是系统公共目录的类型常量(比如Environment.DIRECTORY_DOWNLOADS),而非通过SAF获取的目录Uri字符串。这就是文件没有保存到选定目录的核心原因——DownloadManager把你传入的Uri字符串当成了公共目录的子路径,最终文件被存到了默认的公共下载目录下。

解决方案

DownloadManager本身不支持直接将文件写入通过SAF(OpenDocumentTree)获取的自定义目录,你可以采用以下两种方案之一:

方案1:先下载到临时目录,再复制到选定目录

  1. 用DownloadManager将文件下载到应用临时目录或公共下载目录
  2. 监听下载完成事件,再通过SAF将文件复制到用户选定的目录

代码示例:

// 第一步:下载到应用临时缓存目录
fun downloadPdfToTemp(context: Context, pdfUrl: String, pdfTitle: String, directoryUri: Uri?) {
    val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
    val tempFile = File(context.externalCacheDir, pdfTitle)
    val request = DownloadManager.Request(Uri.parse(pdfUrl))
        .setTitle(pdfTitle)
        .setMimeType("application/pdf")
        .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        .setDestinationUri(Uri.fromFile(tempFile))

    val downloadId = downloadManager.enqueue(request)

    // 注册广播接收器监听下载完成
    val downloadCompleteReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val receivedId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
            if (receivedId == downloadId && directoryUri != null) {
                // 下载完成,执行文件复制
                copyToSelectedDirectory(context, tempFile, pdfTitle, directoryUri)
                context?.unregisterReceiver(this)
            }
        }
    }
    context.registerReceiver(downloadCompleteReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}

// 第二步:将临时文件复制到SAF选定目录
private fun copyToSelectedDirectory(context: Context?, sourceFile: File, fileName: String, targetDirUri: Uri) {
    context ?: return
    val targetDir = DocumentFile.fromTreeUri(context, targetDirUri) ?: return
    val targetFile = targetDir.createFile("application/pdf", fileName) ?: return

    // 执行文件复制
    context.contentResolver.openOutputStream(targetFile.uri)?.use { outputStream ->
        sourceFile.inputStream().use { inputStream ->
            inputStream.copyTo(outputStream)
        }
    }
    // 删除临时文件
    sourceFile.delete()
}

方案2:直接通过SAF写入自定义目录(不使用DownloadManager)

如果不需要DownloadManager的系统级通知和下载管理能力,可以直接用网络库(如OkHttp)下载文件并写入SAF目录:

import okhttp3.OkHttpClient
import okhttp3.Request
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

// 协程中执行下载,避免阻塞主线程
suspend fun downloadPdfDirectlyToSelectedDir(context: Context, pdfUrl: String, fileName: String, targetDirUri: Uri) = withContext(Dispatchers.IO) {
    val client = OkHttpClient()
    val request = Request.Builder().url(pdfUrl).build()
    val response = client.newCall(request).execute()

    if (!response.isSuccessful) throw IOException("下载失败: ${response.code}")

    val targetDir = DocumentFile.fromTreeUri(context, targetDirUri) ?: return@withContext
    val targetFile = targetDir.createFile("application/pdf", fileName) ?: return@withContext

    // 写入文件到选定目录
    context.contentResolver.openOutputStream(targetFile.uri)?.use { outputStream ->
        response.body?.byteStream()?.use { inputStream ->
            inputStream.copyTo(outputStream)
        }
    }
}

注意事项

  • 使用SAF时,OpenDocumentTree会自动请求目录写入权限,但需要用户确认授权
  • 方案1中,务必在下载完成后注销广播接收器,避免内存泄漏
  • 方案2需要添加OkHttp依赖到build.gradle(Module级别):
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 04:15:31