Android -- WebView 支持文件下载的几种方式

1. 前言

最近在做 Android 混合项目的开发,涉及到 WebView 控件的文件下载功能,这里总结一下。

Android 中 Webview 控件默认是不支持文件下载的,需要设置其属性才支持。Webview 实现下载的方式主要有三种:

  1. 跳转到浏览器下载
  2. 使用系统的下载服务
  3. 自定义下载

本人能想到的只有三种,如有遗漏,还请赐教~~

记得添加 网络权限&文件读取权限,此处忽略不计,不懂请自行百度

2. WebView 设置下载监听器

Webview 默认是不支持文件下载的,需要给其设置下载监听器setDownloadListener

webView.setDownloadListener(new DownloadListener() {
      @Override
      public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
          //处理下载事件
      }
});
  • DownloadListener 里面只有一个方法 onDownloadStart,具体的参数含义可查看源码,里面注释的很详细。
  • 下载的URL 通过onDownloadStart方法参数传递,可以在这里处理下载事件。
  • 每当有文件需要下载时,该方法就会被回调。

3. WebView 下载文件

3.1 跳转到浏览器下载

这种方式最为简单粗暴,直接把下载任务抛给浏览器,剩下的就不用我们管了。

private void downloadByBrowser(String url) {
     Intent intent = new Intent(Intent.ACTION_VIEW);
     intent.addCategory(Intent.CATEGORY_BROWSABLE);
     intent.setData(Uri.parse(url));
     startActivity(intent);
}

缺点:
无法感知下载完成,所以就没有后续的处理,如 apk 下载完成后打开安装界面、文件下载完后打开文件 等。

3.2 使用系统的下载服务

DownloadManager 是系统提供的用于处理下载的服务,使用者只需提供 下载 URI 和 存储路径,并进行简单的设置。

DownloadManager 会在后台进行下载,并且在下载失败、网络切换以及系统重启后尝试重新下载。

添加下载任务:

/**
 * 使用系统的下载服务
 *
 * @param url                下载地址
 * @param contentDisposition attachment;filename=测试专用.wps;filename*=utf-8''测试专用.wps
 * @param mimeType           application/octet-stream
 */
private void downloadBySystem(String url, String contentDisposition, String mimeType) {
    LogUtil.d(TAG, "downloadBySystem:url=" + url + ",contentDisposition="
                + contentDisposition + ",mimeType=" + mimeType);
   // 指定下载地址
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
    // 允许媒体扫描,根据下载的文件类型被加入相册、音乐等媒体库
    request.allowScanningByMediaScanner();
    // 设置通知的显示类型,下载进行时和完成后显示通知
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
    // 设置通知栏的标题,如果不设置,默认使用文件名
    // request.setTitle("This is title");
    // 设置通知栏的描述
    // request.setDescription("This is description");
    // 允许在计费流量下下载
    request.setAllowedOverMetered(false);
    // 允许该记录在下载管理界面可见
    request.setVisibleInDownloadsUi(false);
    // 允许漫游时下载
    request.setAllowedOverRoaming(true);
    // 允许下载的网路类型
    request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
    // 设置下载文件保存的路径和文件名
    String fileName = URLUtil.guessFileName(url, contentDisposition, mimeType);
    LogUtil.d(TAG, "fileName:" + fileName);

    //storage/emulated/0/Android/data/项目名/files
	// request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
	// request.setDestinationInExternalPublicDir(ConstantPath.getCommonPath(mContext), fileName);

    //Android/data/项目名/files/storage/emulated/0/Android/data/项目名/files
	// request.setDestinationInExternalFilesDir(this, ConstantPath.getCommonPath(mContext), fileName);

    //另外可选一下方法,自定义下载路径
    Uri mDestinationUri = Uri.withAppendedPath(Uri.fromFile(
            new File(ConstantPath.getRootPath(ConstantPath.ANDROIDMOBILE))), fileName);
	// Uri mDestinationUri = Uri.withAppendedPath(Uri.fromFile(
	//                new File(ConstantPath.getCommonPath(mContext))), fileName);
    request.setDestinationUri(mDestinationUri);

    final DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
    // 添加一个下载任务
    long downloadId = downloadManager.enqueue(request);
    LogUtil.d(TAG, "downloadId:" + downloadId);
}

怎么知道文件下载成功呢?

系统在下载完成后会发送一条广播,里面有任务 ID,告诉调用者任务完成,通过 DownloadManager 获取到文件信息就可以进一步处理。

接收系统下载完成发出的广播:

private class DownloadCompleteReceiver extends BroadcastReceiver {
    @Override
     public void onReceive(Context context, Intent intent) {
         LogUtil.d(TAG + ",onReceive. intent:{}", intent != null ? intent.toUri(0) : null);
         if (intent != null) {
             if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
                 long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
                 LogUtil.d(TAG, "downloadId:" + downloadId);

                 DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
                 //application/octet-stream
                 String type = downloadManager.getMimeTypeForDownloadedFile(downloadId);
                 LogUtil.d(TAG, "getMimeTypeForDownloadedFile:" + type);
                 if (TextUtils.isEmpty(type)) {
                     type = "*/*";
                 }
                 Uri uri = downloadManager.getUriForDownloadedFile(downloadId);
                 LogUtil.d(TAG + ",UriForDownloadedFile:{}", uri.toString());

                 if (uri != null) {
//                        LogUtil.d(TAG + ",UriForDownloadedFile:{}", getRealFilePath(uri));
//                        jsRouteUtil.evaluateJavascript("javascript:getFilePath('" + ConstantPath.ANDROIDMOBILE + "')", webView);

//                        downLoadFile.openDonwload(FileUtil.getRealFilePath(mContext, uri));
                     downLoadFile.openDonwload(uri, type);
//
//                        String fileName = getFileRealNameFromUri(uri);
//                        LogUtil.d(TAG + ",UriForDownloadedFile:{}", fileName);
                 }
             }
         }
     }
 }

打开文件:

/**
 * 打开文件
 * @param uri 文件路劲 content://downloads/all_downloads/67
 * @param type application/octet-stream
 */
 public void openDonwload(Uri uri, String type) {
     if (uri != null) {
         try {
             Intent intent = new Intent();
             //设置intent的Action属性
             intent.setAction(Intent.ACTION_VIEW);
             //获取文件file的MIME类型
             //tring type = FileType.getType(".docx");
             LogUtil.d("type", type);
             //7.0和即以上:不加这句话,wps软件不会打开文件,只会打开到软件列表;7.0以下,不加这句话,wps无法访问文件
             //uri访问时,即content://..,要添加文件访问权限
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
             //设置intent的data和Type属性。
             intent.setDataAndType(uri, type);
             context.startActivity(intent);
         } catch (ActivityNotFoundException e) {
             Toast.makeText(context, "无法打开此文件", Toast.LENGTH_SHORT).show();
         }
     }
 }

到这里为止,利用系统服务下载就算结束了。

总结:
系统服务只能监测到下载的开始结束,至于下载过程中的暂停、重试等机制,系统已经帮我们做好了。

这种模式到底友不友好,要根据使用场景和具体的需求来评定了。

坑点

在实际开发中,本人遇到了一些坑点,这里做下记录。

1、下载文件格式错误

我要下载的文件是docx格式的文件,但实际下载下来的文件变成了bin格式。
在这里插入图片描述
在上诉代码中,通过 DownloadManager 指定了下载文件的文件名,而文件名是根据 URLUtil.guessFileName 方法对 contentDisposition 进行解析得到的。

//得到文件名
String fileName = URLUtil.guessFileName(url, contentDisposition, mimeType);

公司服务器上返回 contentDisposition 和 mimeType 值分别是:

  • contentDisposition:attachment;filename=测试专用.wps;filename*=utf-8’'测试专用.wps
  • mimeType:application/octet-stream

带着这两个值去看下 URLUtil.guessFileName 源码:
(此处只列出关键语句,可自行查看相关源码细节)

public static final String guessFileName(
            String url,
            @Nullable String contentDisposition,
            @Nullable String mimeType) {
        String filename = null;
        String extension = null;

        // If we couldn't do anything with the hint, move toward the content disposition
        if (filename == null && contentDisposition != null) {
            filename = parseContentDisposition(contentDisposition);
            if (filename != null) {
                int index = filename.lastIndexOf('/') + 1;
                if (index > 0) {
                    filename = filename.substring(index);
                }
            }
        }

        // If all the other http-related approaches failed, use the plain uri
        .....
        
        // Split filename between base and extension
        // Add an extension if filename does not have one
        int dotIndex = filename.indexOf('.');
        if (dotIndex < 0) {
            .......
        } else {
            if (mimeType != null) {
                // Compare the last segment of the extension against the mime type.
                // If there's a mismatch, discard the entire extension.
                int lastDotIndex = filename.lastIndexOf('.');
                //typeFromExt=wps
                String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
                        filename.substring(lastDotIndex + 1));
                //typeFromExt=wps,mimeType=application/octet-stream
                if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) {
                    //根据mimeType来得到相应的文件格式
                    extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
                    if (extension != null) {
                        extension = "." + extension;
                    }
                }
            }
            if (extension == null) {
                //extension=.wps
                extension = filename.substring(dotIndex);
            }
            //filename=测试
            filename = filename.substring(0, dotIndex);
        }

        //将文件名和格式后缀拼接
        return filename + extension;
}

根据源码可知,文件的格式是根据 mimeType 类型来决定的,查看相关的资料,得到 mimeType 与文件格式对应关系:

 private static String[][] MIME_MapTable = {
            //{后缀名,MIME类型}
            {".3gp", "video/3gpp"},
            {".apk", "application/vnd.android.package-archive"},
            {".asf", "video/x-ms-asf"},
            {".avi", "video/x-msvideo"},
            {".bin", "application/octet-stream"},
            {".bmp", "image/bmp"},
            {".c", "text/plain"},
            {".class", "application/octet-stream"},
            {".conf", "text/plain"},
            {".cpp", "text/plain"},
            {".doc", "application/msword"},
            {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
            {".xls", "application/vnd.ms-excel"},
            {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
            {".exe", "application/octet-stream"},
            {".gif", "image/gif"},
            {".gtar", "application/x-gtar"},
            {".gz", "application/x-gzip"},
            {".h", "text/plain"},
            {".htm", "text/html"},
            {".html", "text/html"},
            {".jar", "application/java-archive"},
            {".java", "text/plain"},
            {".jpeg", "image/jpeg"},
            {".jpg", "image/jpeg"},
            {".js", "application/x-javascript"},
            {".log", "text/plain"},
            {".m3u", "audio/x-mpegurl"},
            {".m4a", "audio/mp4a-latm"},
            {".m4b", "audio/mp4a-latm"},
            {".m4p", "audio/mp4a-latm"},
            {".m4u", "video/vnd.mpegurl"},
            {".m4v", "video/x-m4v"},
            {".mov", "video/quicktime"},
            {".mp2", "audio/x-mpeg"},
            {".mp3", "audio/x-mpeg"},
            {".mp4", "video/mp4"},
            {".mpc", "application/vnd.mpohun.certificate"},
            {".mpe", "video/mpeg"},
            {".mpeg", "video/mpeg"},
            {".mpg", "video/mpeg"},
            {".mpg4", "video/mp4"},
            {".mpga", "audio/mpeg"},
            {".msg", "application/vnd.ms-outlook"},
            {".ogg", "audio/ogg"},
            {".pdf", "application/pdf"},
            {".png", "image/png"},
            {".pps", "application/vnd.ms-powerpoint"},
            {".ppt", "application/vnd.ms-powerpoint"},
            {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
            {".prop", "text/plain"},
            {".rc", "text/plain"},
            {".rmvb", "audio/x-pn-realaudio"},
            {".rtf", "application/rtf"},
            {".sh", "text/plain"},
            {".tar", "application/x-tar"},
            {".tgz", "application/x-compressed"},
            {".txt", "text/plain"},
            {".wav", "audio/x-wav"},
            {".wma", "audio/x-ms-wma"},
            {".wmv", "audio/x-ms-wmv"},
            {".wps", "application/vnd.ms-works"},
            {".xml", "text/plain"},
            {".z", "application/x-compress"},
            {".zip", "application/x-zip-compressed"},
            {"", "*/*"}
    };

根据对应表可知,application/octet-stream 返回 .bin 格式。同时也能看到,要想得到 .docx 格式,mimeType 的值要为 application/vnd.openxmlformats-officedocument.wordprocessingml.document

String fileName = URLUtil.guessFileName(url, contentDisposition, "application/vnd.openxmlformats-officedocument.wordprocessingml.document");

//或者
String fileName = URLUtil.guessFileName(url, contentDisposition, "application/vnd.ms-works")

这样设置后,下载文件名的格式就是 .docx,也就是说,服务器给的 mimeType 有问题。

懒得和服务端撕逼,再细看下源码会发现,当 mimeType 为 null 时,会截取文件名后缀来得到格式名(已在上诉源码中标注了),所以我将mimeType设置为了null。

String fileName = URLUtil.guessFileName(url, contentDisposition, null);

String fileName = URLUtil.guessFileName(url, contentDisposition, mimeType);
这种设置方法得到下载文件格式,在有些设备上是正常的;但是在有些设备上(华为MatePad Pro)就会出现文件格式错误问题,这可能和系统有关系。

2、下载文件名问题

根据上诉那样设置后,下载下来的文件名如下:
在这里插入图片描述
可以看出,文件名是直接使用了 contentDisposition 这个值,但是实际上我想要的是 测试专用.docx

又去看了下 URLUtil.guessFileName 的源码,发现 filename 是根据 /截取 contentDisposition 值得到的,但是我的 contentDisposition=attachment;filename=测试专用.wps;filename*=utf-8''测试专用.wps,里面没有/,而是多了''

又去和服务端的撕逼了下,他们说这是服务器返回的,他们没改过,无语,那我自己改吧。。。

'' 替换成/

contentDisposition = contentDisposition.replace("''", "/");
String fileName = URLUtil.guessFileName(url, contentDisposition, null);

这样就ok了
在这里插入图片描述

3.3 自定义下载任务

有了下载链接就可以自己实现网络部分,自定义下载实现的方式有很多,根据你的项目框架选择自定义方式。

这里举个例子,使用 HttpURLConnection 和 AsyncTask 实现:

private class DownloadTask extends AsyncTask<String, Void, Void> {
    // 传递两个参数:URL 和 目标路径
    private String url;
    private String destPath;

    @Override
    protected void onPreExecute() {
        log.info("开始下载");
    }

    @Override
    protected Void doInBackground(String... params) {
        log.debug("doInBackground. url:{}, dest:{}", params[0], params[1]);
        url = params[0];
        destPath = params[1];
        OutputStream out = null;
        HttpURLConnection urlConnection = null;
        try {
            URL url = new URL(params[0]);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setConnectTimeout(15000);
            urlConnection.setReadTimeout(15000);
            InputStream in = urlConnection.getInputStream();
            out = new FileOutputStream(params[1]);
            byte[] buffer = new byte[10 * 1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            in.close();
        } catch (IOException e) {
            log.warn(e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    log.warn(e);
                }
            }
        }
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        log.info("完成下载");
        String mimeType = getMIMEType(url);
        Uri uri = Uri.fromFile(new File(destPath));
        log.debug("mimiType:{}, uri:{}", mimeType, uri);
        openDonwload(uri, type);
    }
}

private String getMIMEType(String url) {
    String type = null;
    String extension = MimeTypeMap.getFileExtensionFromUrl(url);
    log.debug("extension:{}", extension);
    if (extension != null) {
        type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
    }
    return type;
}

//  使用
mWebView.setDownloadListener(new DownloadListener() {
    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) {
        //fileName 的问题上诉已讲
        String fileName = URLUtil.guessFileName(url, contentDisposition, mimeType);
        
        //String destPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
        //        .getAbsolutePath() + File.separator + fileName;
        String destPath = ConstantPath.getCommonPath(mContext) + File.separator + fileName;
        new DownloadTask().execute(url, destPath);
    }
});

优点:
可以感知下载进度,处理开始、取消、失败、完成等事件。

缺点:
必须自己处理网络带来的问题。

4. 总结

  • 对下载没有任何要求,可使用 跳转到浏览器下载 方式实现下载。
  • 只关心下载的 开始 和 结束 的,可使用 系统的下载服务 方式实现下载。
  • 关心下载的全过程,即 开始、结束、暂停、下载进度 等过程的,可 自定义下载任务 方式实现下载。
Android WebView 中,AppCache(Application Cache)是一种用于离线应用的缓存机制,它允许网页应用在离线状态下加载和使用资源。AppCache 使用一个描述文件(通常是 `manifest.appcache`)来指定要缓存的文件列表。 当 WebView 加载包含 AppCache 的网页时,它会解析 manifest.appcache 文件,并按照其中指定的文件列表下载和缓存相关资源。manifest.appcache 文件是一个简单的文本文件,其内容由以下几个部分组成: 1. CACHE MANIFEST:指示这是一个 AppCache 文件。 2. CACHE:列出需要缓存的文件列表。每行表示一个文件的路径,可以是相对路径或绝对路径。这些文件将被下载并存储在本地缓存中。 3. NETWORK:列出不会被缓存的文件列表。这些文件将始终从网络加载。 4. FALLBACK:指定离线状态下的备用资源。当某个文件无法从缓存中加载时,会尝试从 FALLBACK 中指定的资源进行替代。 示例 manifest.appcache 文件内容如下: ``` CACHE MANIFEST # Version: 1.0.0 CACHE: index.html styles.css script.js NETWORK: api.example.com FALLBACK: offline.html /offline.html ``` 上述示例中,index.html、styles.css 和 script.js 会被缓存,api.example.com 不会被缓存(每次都从网络加载),而 offline.html 将作为离线状态下的替代资源。 需要注意的是,AppCache 在 Android 4.4(KitKat)之后被废弃,推荐使用 Service Worker 和其他离线技术来替代。因此,在开发新的应用时,建议考虑使用更现代的离线方案。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值