返回

解决Android WebView文件下载难题

Android

Android WebView 文件下载问题

Webview 文件下载是一个常见的开发挑战。尤其当在设备浏览器中可以正常下载的文件在 Webview 中却无法下载时,问题就更加棘手。本篇文章将探讨这个问题,并提供解决方案。

问题现象

在 Android 应用中使用 Webview 加载 HTML 页面,尝试下载 PDF 或其他类型的文件时,下载过程没有启动或失败。然而,同样的链接在设备自带的浏览器中可以正常下载。

问题原因分析

  1. 权限缺失: 应用可能没有获取必要的存储权限,导致无法将文件下载到本地存储。

  2. DownloadListener 未正确处理: Webview 的 DownloadListener 可能未被正确配置,导致下载请求没有被正确处理,比如:url 未传递。

  3. 文件类型识别问题: Webview 可能无法识别一些特定的文件类型,比如,虽然服务端告知是 application/pdf 但由于 Content-Disposition 等头信息缺失,下载被中断。

  4. shouldOverrideUrlLoading 的拦截: 在某些场景下 shouldOverrideUrlLoading 可能会错误拦截某些链接,而使DownloadListener失效。

  5. Http header配置错误: 部分服务器可能返回不正确的 Content-Type 头信息。

  6. https证书验证: 在android P (28)以上的版本,对webview链接有默认https证书安全验证,如果服务端使用的证书不合规可能会阻止请求。

解决方案

以下是一些常见的解决方案,按照由易到难的顺序排列,可根据具体情况选择尝试。

1. 检查并请求存储权限

应用必须获得存储权限才能下载文件。

步骤:

  • AndroidManifest.xml 中添加存储权限声明。
```xml
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```
  • 在 Activity 中动态请求运行时权限 (Android 6.0 及更高版本)。
```java
    private static final int PERMISSION_REQUEST_CODE = 1;
	 private void checkStoragePermission(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
         if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
           {
            requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},PERMISSION_REQUEST_CODE);
            }
         }
     }
  @Override
  public void onRequestPermissionsResult(int requestCode,String[] permissions, int[] grantResults) {
      switch(requestCode){
      case PERMISSION_REQUEST_CODE:{
          if(grantResults[0] == PackageManager.PERMISSION_GRANTED)
            {
            }else{
                checkStoragePermission();
           }
       }
      break;
       }
   }
 @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
	checkStoragePermission();

 }
```

原理: Android 从 6.0 开始引入运行时权限。在需要存储权限时,必须先获得用户的明确授权,程序才能读写外部存储设备。

2. 正确使用 DownloadListener

确保在 WebView 中正确设置 `DownloadListener`。同时也要在`DownloadListener`的`onDownloadStart`正确使用DownloadManager处理下载。

步骤:

  1. 设置 Webview 的 DownloadListener
```java
    webView.setDownloadListener(new DownloadListener() {
        @Override
        public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
             startDownload(url,contentDisposition,mimeType);
           
         }
    });

private void startDownload(String url, String contentDisposition,String mimeType) {

    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
        request.setMimeType(mimeType);
        String fileName = URLUtil.guessFileName(url,contentDisposition,mimeType);
        request.setDescription("Downloading file " + fileName);
        String cookie = CookieManager.getInstance().getCookie(url);
        request.addRequestHeader("Cookie", cookie);
        request.allowScanningByMediaScanner();
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
       
        File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(), fileName);

        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) {
           //request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
           request.setDestinationUri(Uri.fromFile(file));
         }
        else{
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
        }
            DownloadManager dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
        dm.enqueue(request);
         Toast.makeText(this,"开始下载 "+ fileName, Toast.LENGTH_LONG).show();

   }
**原理:**   `DownloadListener` 拦截 webview 发起的下载请求。`DownloadManager` 处理实际的下载任务,并在系统下载管理器中显示下载进度。

#### 3.  检查 shouldOverrideUrlLoading 的处理

检查`shouldOverrideUrlLoading`方法是否有不必要的拦截下载行为,可能导致跳转而不是触发下载,尤其当需要自定义处理某些URL时要注意文件下载的处理。如果出现类似问题,可以使用`if (url.contains(".pdf")) {return false;} `之类的方式提前结束拦截。
  
**步骤:** 
  
 *  `shouldOverrideUrlLoading`方法内排除文件链接拦截,优先执行下载。
  
 ```java

      @Override
     public boolean shouldOverrideUrlLoading(WebView view, String url) {

             if (url.contains(".pdf")||url.contains(".rar")||url.contains(".zip") ) {
                 return false; //不拦截,允许WebView使用DownloadListener进行下载
               }

          //其他的URL处理逻辑
           view.loadUrl(url);
           return true;
         }
    ```

**原理:**  当页面加载请求url时候,会调用该方法,如果返回值是 `true`,则该链接由app接管,webview不再继续加载, 并且可能跳过后续的`DownloadListener`.
当该链接是文件类型的时候, `false` 的返回可以让 webview 进行下一步,触发 `DownloadListener`.

#### 4. 处理服务端返回的 Content-Type 信息

某些服务端返回的文件类型不准确,导致 webview 下载出错。

**步骤:** 

 * 在 `DownloadListener`的`onDownloadStart`中自行判断MIME type是否匹配,否则设置成通用类型 application/octet-stream 。

 ```java
     @Override
 public void onDownloadStart(String url, String userAgent,String contentDisposition, String mimeType,long contentLength) {

       if(mimeType == null || "".equals(mimeType) ||  !mimeType.contains("application")){
        mimeType= "application/octet-stream";

        }

        startDownload(url,contentDisposition,mimeType);
     }
  ```
 
 **原理:**  当服务器的`Content-Type`配置错误,无法准确匹配对应文件格式时,直接使用 `application/octet-stream`可以让下载管理器进行通用的文件下载, 保证文件的有效落地,只是可能出现无法预览的问题,但是通常情况下可以使用外部应用打开文件,并不影响业务的流程。
#### 5. 解决Https 证书验证
在较新的android系统会默认https安全校验,需要特殊处理,部分开发者可能会使用  `webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);`的方式开启允许所有https和http的请求,这样可能带来安全风险,
如果不是测试环境建议使用更好的方式, 如下所示:
 **步骤:** 

   *  需要服务端提供正确合规的SSL证书
   *  同时可以手动校验证书是否有效,可以覆盖`onReceivedSslError`处理。
```java
webView.setWebViewClient(new WebViewClient(){
          @Override
         public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
 			handler.proceed();//表示等待证书响应
 			}
 	});

原理: 当 https 请求的服务端返回错误SSL证书时,会阻止webview正常加载. 覆写onReceivedSslError 可以选择无视错误,继续加载。注意,这个做法会存在一定的安全隐患,不建议在生产环境中使用。

额外建议

  1. 错误处理: 添加适当的错误处理逻辑,例如下载失败提示、网络连接错误提示等。

  2. 用户反馈: 确保下载过程中有清晰的用户反馈,比如下载进度、完成通知。

  3. 后台下载: 对于大型文件,考虑使用后台服务下载,以免应用在前台运行时阻塞。

  4. 文件保存路径: 需要设置清晰的文件保存路径和保存名称,方便用户查找和使用下载的文件,或者允许用户选择保存路径,进一步提升体验。

以上这些步骤提供了解决 WebView 文件下载问题的常见方法。请根据自己的具体情况,逐步排查问题并应用合适的解决方案。通过细致的检查和代码调整,大部分文件下载问题可以得到有效解决。