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

如何打开WebView中受Cookie保护的PDF?DownloadManager下载失败如何排查

现有代码存在的问题

  • Cookie设置错误:硬编码了获取Cookie的域名和单个Cookie名,应该直接获取待下载URL对应的完整Cookie,否则鉴权信息不全直接导致下载请求被后端拒绝。
  • 广播接收器泄漏:每次触发下载都会注册一次广播接收器,没有做反注册逻辑,会导致内存泄漏,多次下载后会出现重复回调的问题。
  • 缺少FileProvider适配:Android 7.0及以上版本禁止跨应用传递file://类型的URI,直接将下载后的本地URI传给第三方PDF阅读器,会导致阅读器无权访问文件,即使下载成功也无法打开。
  • 未捕获下载错误原因:只打印了下载状态码,没有读取COLUMN_REASON字段的具体错误原因,无法直接定位失败类型。
  • 跳转请求Cookie丢失:如果下载链接存在302跨域跳转,DownloadManager只会给第一个原始请求加你设置的Cookie头,跳转后的请求不会携带Cookie,也会导致下载失败。
  • 未适配明文网络请求:如果下载链接是HTTP协议,Android 9及以上版本默认禁止明文流量,会直接拦截下载请求,需要配置网络安全规则放行对应域名。

下载失败定位方法

  • 打印错误码:在查询下载状态时追加读取DownloadManager.COLUMN_REASON字段的数值,对照官方错误码表判断失败类型,比如4xx是客户端错误、5xx是服务端错误、文件写入错误等。
  • 抓包验证请求:对下载请求抓包,确认请求头是否携带了完整的Cookie、是否存在跨域跳转、后端返回的状态码是否正常。
  • 查看系统下载管理器通知:系统自带的DownloadManager会在通知栏显示具体的失败原因,比如存储空间不足、网络错误等,比自定义日志信息更全。

优化方案

推荐两种实现方案,按需选择:

方案1:修复现有DownloadManager实现

核心修改点:

  1. 正确获取完整Cookie,不需要指定固定域名和Cookie名:
val cookie = CookieManager.getInstance().getCookie(url)
  1. 全局注册广播接收器,不要在下载回调里重复注册,页面销毁时记得反注册。
  2. 适配FileProvider:首先在AndroidManifest.xml中配置FileProvider,然后将下载后的本地文件URI通过FileProvider.getUriForFile()转换为content://类型的URI,同时给Intent加临时读取权限:
viewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
  1. 如果存在跨域跳转问题,可以先自行请求一次下载地址,拿到最终跳转后的直链再交给DownloadManager下载。

方案2:使用OkHttp自行下载(更可控)

如果不想踩DownloadManager的坑,直接用OkHttp自己做下载逻辑,自行处理Cookie(可直接复用WebView的CookieManager的Cookie)、跳转、进度,可控性更高,适合对下载逻辑定制要求高的场景。

自动删除PDF实现

使用Activity Result API启动PDF阅读器,等用户关闭阅读器返回应用时,直接删除本地文件即可:

  1. 先注册返回回调:
val openPdfLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    // 用户返回后删除对应PDF文件
    val file = File(downloadLocalPath)
    if (file.exists()) file.delete()
}
  1. 启动阅读器时调用launcher:
openPdfLauncher.launch(viewIntent)

如果担心用户直接杀掉应用没触发回调,可以把PDF存在应用专属的缓存目录context.cacheDir下,系统会在存储空间不足时自动清理缓存文件,也可以在每次应用启动时批量清理超过24小时的缓存PDF。

修复后核心代码示例

// 全局注册广播接收器,不要放在下载回调里
private val downloadReceiver = object: BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action != DownloadManager.ACTION_DOWNLOAD_COMPLETE) return
        val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
        val manager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
        val cursor = manager.query(DownloadManager.Query().apply{ setFilterById(downloadId) })
        if (cursor.moveToFirst()) {
            val downloadStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
            val reason = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON))
            Log.w("download", "status:$downloadStatus, reason:$reason")
            if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
                val localPath = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI))
                val mimeType = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE))
                val file = File(Uri.parse(localPath).path!!)
                // 转换为FileProvider URI
                val contentUri = FileProvider.getUriForFile(
                    this@YourActivity,
                    "${packageName}.fileprovider",
                    file
                )
                val viewIntent = Intent(Intent.ACTION_VIEW).apply {
                    setDataAndType(contentUri, mimeType)
                    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                }
                if (viewIntent.resolveActivity(packageManager) != null) {
                    openPdfLauncher.launch(Intent.createChooser(viewIntent, "选择打开应用"))
                } else {
                    Toast.makeText(context, "没有可打开该文件的应用", Toast.LENGTH_SHORT).show()
                }
            }
        }
        cursor.close()
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // 注册广播
    registerReceiver(downloadReceiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
    
    webView?.apply {
        settings.domStorageEnabled = true
        settings.javaScriptEnabled = true
        setDownloadListener { url, _, _, mimetype, _ ->
            Log.w("downloading file", url)
            val downloadUri = Uri.parse(url)
            val downloadRequest = DownloadManager.Request(downloadUri).apply {
                setTitle("下载文件")
                setDescription("正在下载PDF")
                setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
                // 正确获取完整Cookie
                val cookie = CookieManager.getInstance().getCookie(url)
                addRequestHeader("Cookie", cookie)
            }
            val manager: DownloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
            manager.enqueue(downloadRequest)
        }
    }
}

override fun onDestroy() {
    super.onDestroy()
    // 反注册广播
    unregisterReceiver(downloadReceiver)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 00:18:02