如何打开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实现
核心修改点:
- 正确获取完整Cookie,不需要指定固定域名和Cookie名:
val cookie = CookieManager.getInstance().getCookie(url)
- 全局注册广播接收器,不要在下载回调里重复注册,页面销毁时记得反注册。
- 适配FileProvider:首先在
AndroidManifest.xml中配置FileProvider,然后将下载后的本地文件URI通过FileProvider.getUriForFile()转换为content://类型的URI,同时给Intent加临时读取权限:
viewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- 如果存在跨域跳转问题,可以先自行请求一次下载地址,拿到最终跳转后的直链再交给DownloadManager下载。
方案2:使用OkHttp自行下载(更可控)
如果不想踩DownloadManager的坑,直接用OkHttp自己做下载逻辑,自行处理Cookie(可直接复用WebView的CookieManager的Cookie)、跳转、进度,可控性更高,适合对下载逻辑定制要求高的场景。
自动删除PDF实现
使用Activity Result API启动PDF阅读器,等用户关闭阅读器返回应用时,直接删除本地文件即可:
- 先注册返回回调:
val openPdfLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { // 用户返回后删除对应PDF文件 val file = File(downloadLocalPath) if (file.exists()) file.delete() }
- 启动阅读器时调用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
相关产品推荐
相关产品推荐

