<think>我们需要在WebView中拦截请求,判断是否是下载地址(通过URL的特征,如文件扩展名等),然后触发下载操作。
我们可以通过自定义WebViewClient,重写shouldOverrideUrlLoading方法,在加载URL之前进行判断。
此外,我们还应该考虑拦截页面内的资源请求(如点击链接、表单提交等)。对于下载链接,我们可能还需要拦截其他类型的请求,比如页面中有iframe加载了下载链接,我们可以通过重写shouldInterceptRequest方法(但注意,这个方法主要用于资源拦截,通常不用于处理下载)。
然而,对于下载链接的拦截,更常见的做法是:
1. 在shouldOverrideUrlLoading中拦截导航请求(用户点击链接或通过JavaScript触发的导航)。
2. 在onLoadResource中拦截资源加载?但这个方法已经被废弃,且不推荐使用。
3. 在shouldInterceptRequest中拦截所有请求,包括资源请求(图片、CSS、JS等),但这样会非常频繁,而且通常我们不希望将资源请求当作下载链接。
因此,我们通常只处理导航请求(shouldOverrideUrlLoading)和页面重新加载(通过WebViewClient的其他回调)。另外,我们还可以处理WebView下载事件(通过设置WebChromeClient的onDownloadStart,但这是针对用户点击下载链接的情况)。
这里提供两种方案:
方案一:通过WebViewClient的shouldOverrideUrlLoading拦截导航请求(适用于用户点击链接跳转下载的情况)
方案二:通过WebChromeClient的onDownloadStart拦截用户点击下载链接的事件(这是专门用于处理下载的方法)。
我们分别介绍两种方案:
方案一:使用WebViewClient拦截
我们在之前的回答中已经展示过,通过重写shouldOverrideUrlLoading,判断URL是否为下载链接(根据文件扩展名),然后触发下载。
方案二:使用WebChromeClient的onDownloadStart
这是更推荐的方式,因为当用户点击一个下载链接时,系统会回调onDownloadStart方法,我们可以在这里处理下载。
下面我们给出两种方案的代码示例:
### 方案一:WebViewClient拦截(在shouldOverrideUrlLoading中处理)
已在之前的回答中给出,这里再稍微补充一下:
```kotlin
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (isDownloadLink(url)) {
// 显示弹窗确认下载
showDownloadDialog(url)
return true // 拦截,不再加载
}
return false // 正常加载
}
// 新版本API(针对Android 24+)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url.toString()
if (isDownloadLink(url)) {
showDownloadDialog(url)
return true
}
return false
}
}
```
### 方案二:WebChromeClient的onDownloadStart(推荐)
这是专门用于处理下载链接的回调。当WebView检测到用户点击了下载链接(通常是<a>标签并且指向一个可下载资源,且没有设置target属性)时,会触发此回调。
```kotlin
webView.webChromeClient = object : WebChromeClient() {
override fun onDownloadStart(
url: String?,
userAgent: String?,
contentDisposition: String?,
mimeType: String?,
contentLength: Long
) {
url?.let {
// 直接开始下载,或者显示弹窗
showDownloadDialog(it)
}
}
}
```
注意:onDownloadStart方法在用户点击下载链接时调用,因此它更符合用户意图。而shouldOverrideUrlLoading会拦截所有导航请求,包括非下载的页面跳转,如果我们错误地将一个普通页面链接判断为下载链接,就会导致无法正常浏览。
因此,我们通常结合两者:
- 对于用户明确点击的下载链接,使用onDownloadStart处理(更准确,因为这是系统识别的下载行为)。
- 对于其他情况(比如通过JavaScript自动触发的下载),我们可以在shouldOverrideUrlLoading中处理。
但是,有些时候onDownloadStart可能不会被触发(比如下载链接是通过JavaScript动态生成的,或者有target="_blank"等属性),所以为了更全面的拦截,我们可以同时使用两种方法。
下面给出一个整合的示例:
### 整合方案:同时使用WebViewClient和WebChromeClient
```kotlin
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
setupWebView()
}
private fun setupWebView() {
webView.settings.javaScriptEnabled = true
// 设置WebViewClient拦截导航请求
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (isDownloadLink(url)) {
showDownloadDialog(url)
return true
}
return false
}
@TargetApi(Build.VERSION_CODES.N)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url.toString()
if (isDownloadLink(url)) {
showDownloadDialog(url)
return true
}
return false
}
}
// 设置WebChromeClient处理下载链接点击
webView.webChromeClient = object : WebChromeClient() {
override fun onDownloadStart(
url: String?,
userAgent: String?,
contentDisposition: String?,
mimeType: String?,
contentLength: Long
) {
url?.let { showDownloadDialog(it) }
}
}
}
// 判断是否为下载链接(根据扩展名)
private fun isDownloadLink(url: String): Boolean {
// 常见的下载文件扩展名
val downloadExtensions = arrayOf(
".apk", ".zip", ".rar", ".tar", ".gz", ".7z", ".exe",
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff",
".mp3", ".wav", ".ogg", ".mp4", ".avi", ".mkv",
".txt", ".csv", ".log", ".xml", ".json", ".html", ".htm"
)
return downloadExtensions.any { url.toLowerCase(Locale.ROOT).endsWith(it) }
}
private fun showDownloadDialog(url: String) {
// 显示下载确认弹窗,用户确认后调用startDownload(url)
// 这里简单起见,直接开始下载
startDownload(url)
}
private fun startDownload(url: String) {
// 使用DownloadManager下载
val request = DownloadManager.Request(Uri.parse(url))
.setTitle("文件下载")
.setDescription("正在下载文件")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, Uri.parse(url).lastPathSegment)
.allowScanningByMediaScanner()
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadManager.enqueue(request)
}
}
```
### 注意事项
1. **扩展名判断的局限性**:
- 某些下载链接可能没有扩展名(比如通过动态生成的下载链接),这时候需要结合MIME类型判断。
- 在onDownloadStart方法中,我们可以通过参数`mimeType`来判断,但在shouldOverrideUrlLoading中,我们无法直接获取MIME类型。
2. **动态请求的处理**:
- 如果下载是通过JavaScript动态触发的(比如通过XMLHttpRequest下载),上述方法都无法拦截。这种情况需要前端配合,或者使用其他方案(如Service Worker等),但在移动端WebView中较难实现。
3. **权限问题**:
- 确保在AndroidManifest.xml中声明了必要的权限:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```
- 在Android 6.0及以上,需要动态请求存储权限。
4. **Android 10+存储权限变更**:
- 在Android 10及以上,应用默认只能访问自己的沙盒目录。如果要将文件保存到公共Downloads目录,需要添加权限:
```xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> <!-- 仅Android 9及以下需要 -->
```
- 对于Android 10及以上,可以使用`MediaStore` API,或者继续使用`DownloadManager`(它已经适配了作用域存储)。
### 相关问题
§§相关问题§§:
1. 在Android WebView中如何处理没有文件扩展名的下载链接?
2. 如何通过MIME类型(Content-Type)来判断WebView中的下载链接?
3. WebView中如何实现文件上传功能?
4. 如何优化WebView下载大文件时的用户体验(如后台下载、断点续传等)?
</think>### WebView 拦截下载链接实现方案
在 Android WebView 中精确拦截下载链接需要综合考虑多种识别方法,以下是一个完整的实现方案,结合了 URL 扩展名、MIME 类型和自定义请求头分析:
#### 完整实现代码(Kotlin)
```kotlin
class DownloadInterceptorWebViewClient : WebViewClient() {
// 扩展名检测列表
private val downloadExtensions = setOf(
"apk", "zip", "rar", "7z", "tar", "gz", "bz2",
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
"mp3", "wav", "flac", "mp4", "avi", "mkv", "mov",
"jpg", "jpeg", "png", "gif", "bmp", "webp",
"exe", "dmg", "deb", "rpm", "ipa"
)
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
request?.url?.let { url ->
val urlStr = url.toString()
if (isDownloadLink(urlStr)) {
handleDownload(view, urlStr)
return true
}
}
return super.shouldOverrideUrlLoading(view, request)
}
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
request?.let {
// 通过Content-Type检测
it.requestHeaders["Content-Type"]?.let { contentType ->
if (isDownloadContentType(contentType)) {
handleDownload(view, it.url.toString())
return createEmptyResponse()
}
}
// 通过Content-Disposition检测
it.requestHeaders["Content-Disposition"]?.let { disposition ->
if (disposition.contains("attachment")) {
handleDownload(view, it.url.toString())
return createEmptyResponse()
}
}
}
return super.shouldInterceptRequest(view, request)
}
// 检测是否为下载链接
private fun isDownloadLink(url: String): Boolean {
// 方法1:文件扩展名检测
val path = Uri.parse(url).path ?: return false
val extension = path.substringAfterLast('.', "").lowercase()
if (downloadExtensions.contains(extension)) return true
// 方法2:常见下载路径模式匹配
val downloadPatterns = listOf(
"/download/", "/files/", "/attachments/",
"/export?format=", "action=download"
)
if (downloadPatterns.any { url.contains(it) }) return true
return false
}
// 检测Content-Type是否为下载类型
private fun isDownloadContentType(contentType: String): Boolean {
val downloadTypes = listOf(
"application/octet-stream",
"application/zip",
"application/x-rar-compressed",
"application/x-msdownload", // .exe
"application/vnd.android.package-archive" // .apk
)
// 匹配主要类型:application/*
return contentType.startsWith("application/") &&
!contentType.startsWith("application/json") &&
!contentType.startsWith("application/xml") ||
downloadTypes.any { contentType.contains(it) }
}
// 创建空响应(用于阻断下载)
private fun createEmptyResponse(): WebResourceResponse {
return WebResourceResponse("text/plain", "UTF-8", null)
}
// 处理下载逻辑
private fun handleDownload(webView: WebView?, url: String) {
webView?.context?.let { context ->
AlertDialog.Builder(context)
.setTitle("下载确认")
.setMessage("检测到下载请求:\n${Uri.parse(url).lastPathSegment}")
.setPositiveButton("下载") { _, _ ->
startDownload(context, url)
}
.setNegativeButton("取消", null)
.show()
}
}
// 启动下载
private fun startDownload(context: Context, url: String) {
val request = DownloadManager.Request(Uri.parse(url))
.setTitle("文件下载")
.setDescription("正在下载 ${Uri.parse(url).lastPathSegment}")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
Uri.parse(url).lastPathSegment
)
.allowScanningByMediaScanner()
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadManager.enqueue(request)
}
}
```
#### 在 Activity 中使用
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val webView: WebView = findViewById(R.id.webView)
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
}
// 设置自定义WebViewClient拦截下载
webView.webViewClient = DownloadInterceptorWebViewClient()
// 加载网页
webView.loadUrl("https://your-website.com")
}
}
```
### 实现原理详解
#### 1. 多维度下载链接识别
- **文件扩展名检测**:
```kotlin
val extension = path.substringAfterLast('.', "").lowercase()
if (downloadExtensions.contains(extension)) return true
```
检测超过 30 种常见文件扩展名
- **URL 路径模式匹配**:
```kotlin
val downloadPatterns = listOf("/download/", "/files/", ...)
if (downloadPatterns.any { url.contains(it) }) return true
```
识别包含 `/download/`、`/files/` 等常见下载路径
- **Content-Type 检测**:
```kotlin
contentType.startsWith("application/") &&
!contentType.startsWith("application/json")
```
拦截 `application/octet-stream` 等下载类型,排除 JSON/XML
- **Content-Disposition 检测**:
```kotlin
if (disposition.contains("attachment"))
```
识别 `Content-Disposition: attachment` 响应头
#### 2. 双重拦截机制
1. **`shouldOverrideUrlLoading`**:
- 处理页面导航请求
- 基于 URL 模式识别下载链接
2. **`shouldInterceptRequest`**:
- 拦截所有资源请求(API 21+)
- 基于 HTTP 头信息识别下载内容
- 返回空响应阻断下载:
```kotlin
return WebResourceResponse("text/plain", "UTF-8", null)
```
#### 3. 下载处理流程
1. 显示确认对话框
2. 使用 DownloadManager 处理下载
3. 文件保存到 Downloads 目录
4. 显示通知栏进度
### 高级优化建议
#### 1. 动态扩展名列表
```kotlin
// 从服务器获取扩展名列表
fun fetchDownloadExtensions() {
val extensions = listOf("xyz", "newformat") // 从API获取
downloadExtensions.addAll(extensions)
}
```
#### 2. MIME 类型白名单
```kotlin
private val allowedMimeTypes = setOf(
"text/html", "text/css", "application/javascript",
"image/*", "font/*", "application/json"
)
private fun isAllowedContentType(contentType: String): Boolean {
return allowedMimeTypes.any { pattern ->
when {
pattern.endsWith("/*") ->
contentType.startsWith(pattern.substringBefore("/*"))
else -> contentType == pattern
}
}
}
```
#### 3. 下载管理器增强
```kotlin
private fun enhancedDownload(context: Context, url: String) {
// 自定义文件名解析
val fileName = resolveFileName(url) ?: "download_${System.currentTimeMillis()}"
// 创建下载目录
val downloadDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"MyAppDownloads"
).apply { mkdirs() }
// 设置下载请求
val request = DownloadManager.Request(Uri.parse(url))
.setTitle("下载: $fileName")
.setDestinationUri(Uri.fromFile(File(downloadDir, fileName)))
.setRequiresCharging(false) // 充电时下载
.setAllowedOverMetered(true) // 允许移动数据
// 大文件警告
if (contentLength > 50 * 1024 * 1024) { // 50MB
request.setDescription("警告:大文件下载 (${contentLength / (1024*1024)}MB)")
}
}
```
### 注意事项
1. **动态权限处理**:
- Android 6.0+ 需要请求 WRITE_EXTERNAL_STORAGE 权限
- Android 10+ 需要适配 Scoped Storage
2. **安全限制**:
```xml
<application
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
```
3. **性能优化**:
- 避免在 shouldInterceptRequest 执行耗时操作
- 使用缓存机制减少重复判断