Android 您所在的位置:网站首页 qq浏览器下载文件的位置不对 Android

Android

2023-11-16 17:29| 来源: 网络整理| 查看: 265

目录 1. 前言2. WebView 设置下载监听器3. WebView 下载文件3.1 跳转到浏览器下载3.2 使用系统的下载服务坑点1、下载文件格式错误2、下载文件名问题 3.3 自定义下载任务 4. 总结

1. 前言

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

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

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

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

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

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’'测试专用.wpsmimeType: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 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 { // 传递两个参数: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. 总结 对下载没有任何要求,可使用 跳转到浏览器下载 方式实现下载。只关心下载的 开始 和 结束 的,可使用 系统的下载服务 方式实现下载。关心下载的全过程,即 开始、结束、暂停、下载进度 等过程的,可 自定义下载任务 方式实现下载。


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有