You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android Pie系统Download Manager外置SD卡下载失败(错误码403)求助

解决Android 9(Pie)中Download Manager写入可移除SD卡失败的问题

这个问题的核心原因是Android 9(API 28)对系统服务的存储访问权限做了收紧:DownloadManager作为系统级服务,不再允许直接通过file://类型的URI写入可移除SD卡上的应用私有目录——哪怕该目录是你的App专属的。这就是为什么在Android 6/8上正常运行,到Android 9就出现Permission denied(对应状态码403)的根本原因。

解决方案:改用Content URI替代File URI

我们需要通过ContentResolver创建目标文件的content://类型URI,让系统服务能合法访问并写入目标路径。以下是具体实现步骤:

步骤1:修改存储目录获取逻辑(返回File对象而非字符串)

先把原本返回路径字符串的逻辑改成返回File对象,方便后续处理存储卷信息:

private fun getExternalStorageDir(): File? { 
    val arrayOfFiles = getExternalFilesDirs(Environment.DIRECTORY_DOWNLOADS) 
    return when {
        // 优先使用可移除SD卡的应用私有下载目录
        arrayOfFiles.size > 1 && arrayOfFiles[1] != null -> arrayOfFiles[1]
        // 回退到主外部存储
        arrayOfFiles.size == 1 && arrayOfFiles[0] != null -> arrayOfFiles[0]
        // 最后回退到内部存储
        else -> File(filesDir, Environment.DIRECTORY_DOWNLOADS)
    }
}

步骤2:为Android 9+生成合法的Content URI

针对API 28及以上版本,通过MediaStore创建目标文件的Content URI;旧版本继续使用File URI即可:

private fun getDownloadTargetUri(context: Context, fileName: String): Uri? {
    val targetDir = getExternalStorageDir() ?: return null
    
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        // 构建文件信息,指定存储位置和类型
        val contentValues = ContentValues().apply {
            put(MediaStore.Downloads.DISPLAY_NAME, fileName)
            put(MediaStore.Downloads.MIME_TYPE, "image/png") // 根据你的文件类型调整
            // 指定相对路径:对应App在SD卡上的私有下载目录
            put(MediaStore.Downloads.RELATIVE_PATH, "Android/data/${context.packageName}/files/${Environment.DIRECTORY_DOWNLOADS}")
            // 指定存储卷为可移除SD卡
            put(MediaStore.Downloads.VOLUME_NAME, targetDir.volumeName)
        }
        // 插入到MediaStore获取合法的Content URI
        context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
    } else {
        // 旧版本直接使用File URI
        Uri.fromFile(File(targetDir, fileName))
    }
}

步骤3:更新DownloadManager请求

用生成的Content URI替代原来的File URI,构建下载请求:

// 获取目标URI,失败则处理异常
val targetUri = getDownloadTargetUri(this, "sampleImage.png") ?: run {
    Log.e("DOWNLOAD", "Failed to get valid target URI, fallback to internal storage")
    // 这里可以添加回退到内部存储的逻辑
    return@yourFunctionName
}

// 构建并执行下载请求
val request = DownloadManager.Request(downloadUri).apply {
    setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
    setAllowedOverRoaming(false)
    setTitle("Image Downloading")
    setNotificationVisibility(VISIBILITY_VISIBLE)
    setDestinationUri(targetUri) // 使用合法的Content URI
}

// 执行请求(原有逻辑不变)
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadId = downloadManager.enqueue(request)

额外说明

  • 无需额外权限:我们操作的是App专属的外部存储目录,Android 6+已经默认授予该权限,无需请求WRITE_EXTERNAL_STORAGE
  • 异常处理:记得在获取URI失败时添加回退逻辑(比如切换到内部存储),避免崩溃并提升用户体验。

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

火山引擎 最新活动