说明:android的webview是不支持下载的!!!
所以我们需要监听下载接口 然后手动执行下载操作,分为三种类型
- 直接打开浏览器下载(最简单),但是一些下载接口需要cookie信息时不能满足
/*** 下载文件选择指定应用下载*/webView.setDownloadListener(new DownloadListener() {@Overridepublic void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {Uri uri = Uri.parse(url);Intent intent = new Intent(Intent.ACTION_VIEW, uri);activity.startActivity(intent);}});
- 在后台内置下载(此时还是在监听里面执行下载操作,还不算复杂)
//在webview属性设置处添加webView.setDownloadListener(new DownloadListener() {@RequiresApi(api = Build.VERSION_CODES.KITKAT)@Overridepublic void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {if(URLUtil.isNetworkUrl(url)){// 解析 contentDisposition 以获取文件名String fileName = contentDisposition.split("filename=")[1].trim().replace("\"", "");try {fileName = URLDecoder.decode(fileName, "UTF-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}downloadUrl(url, fileName);} }});//下载方法private void downloadUrl(String url, final String fileName) {//获取cookieCookieManager cookieManager = CookieManager.getInstance();String cookie = cookieManager.getCookie(url);DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);request.setTitle(fileName); // 使用文件名作为标题request.setDescription("Downloading " + fileName + "...");request.setVisibleInDownloadsUi(true); // 在下载管理器中显示下载if (cookie != null) {// 处理获取到的 Cookie
// Log.d("Cookie", "Cookie for URL " + url + ": " + cookie);request.addRequestHeader("Cookie", cookie);} else {Log.d("Cookie", "No cookie found for URL " + url);}request.setMimeType("application/octet-stream");// 设置下载路径和文件名request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);// 获取下载管理器并开始下载DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);downloadManager.enqueue(request);final Handler handler = new Handler(Looper.getMainLooper());handler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(context, "下载完成,请到通知栏点击安装或打开文件", Toast.LENGTH_SHORT).show();}});}
-
监听下载已经无法满足下载:如果浏览器是以下载接口请求后生成
blob:https:
下载地址,去获取浏览器存储的文件,在监听里便无法直接下载原文件,因为无法获取到响应头信息,因此无法获取文件名来保存文件。当然如果知道下载的文件类型 可以 通过自定义文件名方式来下载blob流文件。- 下载blob流文件:有固定的文件类型 或者通过魔法字符获取文件类型,仍在下载监听内下载
实现思路:
(1)判断为blob下载
- 下载blob流文件:有固定的文件类型 或者通过魔法字符获取文件类型,仍在下载监听内下载
webView.setDownloadListener(new DownloadListener() {@RequiresApi(api = Build.VERSION_CODES.KITKAT)@Overridepublic void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {if (url.startsWith("blob:")) {// 获取文件名String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);// 获取 blob 数据并写入文件 已经在拦截地方下载downloadBlobFile(url, fileName);}}});
(2)定义下载事件,通过调用js请求 blob接口,获取返回数据 回传给android (需要先设置监听才能收到返回数据)
//下载方法定义@RequiresApi(api = Build.VERSION_CODES.KITKAT)private void downloadBlobFile(String blobUrl, final String fileName) {// 由于 WebView 不能直接处理 blob URL,我们需要使用 JavaScript 接口或其他方法来获取 blob 数据// 以下代码是一个示例,展示了如何使用 WebView 获取 blob 数据并写入文件webView.evaluateJavascript("console.log('开始请求blob...');" +"var xhr = new XMLHttpRequest();" +"xhr.open('GET', '"+blobUrl+"', true);" +"xhr.responseType = 'blob';" +"xhr.onload = function() {" +" if (this.status === 200) {" +" const blob = this.response;" +" var reader = new FileReader();" +" reader.readAsDataURL(blob);" +" reader.onloadend = function() {" +" var base64data = reader.result.split(',')[1];" +" console.log('base64data:'+base64data);" +" Android.androidCallback( base64data);" +" };" +" } else {" +" console.error('Failed to fetch blob data');" +" }" +"};" +"xhr.onerror = function() {" +" console.error('Error fetching blob data');" +"};" +"xhr.send();", null);}
(3)保存文件
//在webview设置处设置监听webView.addJavascriptInterface(new WebAppInterface(context), "Android");//定义APP监听接口 获取传回的数据public class WebAppInterface {Context mContext;WebAppInterface(Context c) {mContext = c;}@RequiresApi(api = Build.VERSION_CODES.O)@JavascriptInterfacepublic void androidCallback(String message) {// 在这里处理从 JavaScript 传递过来的数据Toast.makeText(mContext, "Received message: " + message, Toast.LENGTH_SHORT).show();String base64Data = message;//保存成文件saveBase64ToFile(base64Data, "downloaded_file");}}//保存文件方法定义@RequiresApi(api = Build.VERSION_CODES.O)private void saveBase64ToFile(String base64Data, String fileName) {try {byte[] decodedBytes = Base64.decode(base64Data, Base64.DEFAULT);File file = new File(context.getFilesDir(), fileName);FileOutputStream outputStream = new FileOutputStream(file);outputStream.write(decodedBytes);outputStream.close();//在通知栏显示下载内容方便用户打开文件showDownloadNotification(file);} catch (IOException e) {e.printStackTrace();}}
(4)显示下载通知
//通知方法定义@RequiresApi(api = Build.VERSION_CODES.O)private void showDownloadNotification(File file) throws IOException {NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {NotificationChannel channel = new NotificationChannel("file_open", "File Open", NotificationManager.IMPORTANCE_DEFAULT);notificationManager.createNotificationChannel(channel);}// 创建一个 Intent 来打开文件Intent intent = new Intent(Intent.ACTION_VIEW);String mimeType = Files.probeContentType(Paths.get(file.getPath()));
// 创建一个文件提供者 URIUri fileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".flutterWebviewPluginProvider", file);intent.setDataAndType(fileUri, mimeType); // 设置文件的 MIME 类型intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予临时权限// 创建 PendingIntentPendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);long[] pattern = {0, 100, 200, 300};Notification notification = new NotificationCompat.Builder(context, "file_open").setSmallIcon(R.drawable.ic_download).setContentTitle(file.getName()).setContentText("下载完成,点击打开文件").setPriority(NotificationCompat.PRIORITY_DEFAULT).setContentIntent(pendingIntent) // 设置点击通知后执行的操作.setAutoCancel(true) // 点击后自动取消通知.build();notificationManager.notify(1, notification);}
2、 监听所有接口请求,过滤出下载接口,自定义下载
(1)设置setWebViewClientwebView.setWebViewClient(webViewClient);
(2)定义webViewClient
(3)过滤出下载接口后,先发起请求获取文件名信息,再进行下载操作
webViewClient = new BrowserClient() {/*监听所有接口请求,过滤所有下载请求*/@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {String url = request.getUrl().toString();if (isDownloadUrl(url)) {Map<String, String> requestHeaders= request.getRequestHeaders();String acceptHeaderValue = requestHeaders.get("Accept");if(acceptHeaderValue != null && acceptHeaderValue.contains("image/")){//忽略页面上img地址为下载接口return super.shouldInterceptRequest(view, request); // 拦截请求}// 处理下载请求startDownloadWithDisposition(url);return null; // 拦截请求}return super.shouldInterceptRequest(view, request);}private boolean isDownloadUrl(String url) {// 检查 URL 是否是下载 URL,例如检查文件扩展名或 MIME 类型return url.contains("downloadFile"); // 示例:检查接口地址}/*先请求获取响应头获取文件名后再进行下载操作*/private void startDownloadWithDisposition(String urlString) {try {URL url = new URL(urlString);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("GET"); // 只请求头部信息 HEAD请求的响应头数据不全 所以用GET// 获取 Content-Disposition 头String contentDisposition = connection.getHeaderField("Content-Disposition");// 解析文件名String fileName = parseFileNameFromContentDisposition(contentDisposition);fileName = URLDecoder.decode(fileName, "UTF-8");// 断开连接connection.disconnect();downloadUrl(urlString,fileName);} catch (IOException e) {e.printStackTrace();}}/*获取文件名*/private String parseFileNameFromContentDisposition(String contentDisposition) {if (contentDisposition == null) {return null;}String[] parts = contentDisposition.split(";");for (String part : parts) {if (part.trim().startsWith("filename=")) {return part.substring("filename=".length()).trim().replace("\"", "");}}return null;}};
注意:获取到的文件名需转码为utf8
fileName = URLDecoder.decode(fileName, "UTF-8");
补充:
(1)okhttp引入报红时,需要在你的webview插件导入 okhttp包
//webview_plugin\android\build.gradle中dependencies {implementation 'com.squareup.okhttp3:okhttp:3.11.0'}
(2)消息通知引入 自定义图标.setSmallIcon(R.drawable.ic_download)
在webview插件的资源部中引入webview_plugin\android\src\main\res\
(3)通过请求头accept 来判断是否img等资源文件下载还是接口请求 资源文件下载要忽略掉不进行下载操作
Map<String, String> requestHeaders= request.getRequestHeaders();String acceptHeaderValue = requestHeaders.get("Accept");if(acceptHeaderValue != null && acceptHeaderValue.contains("image/")){//忽略页面上img地址为下载接口return super.shouldInterceptRequest(view, request); // 拦截请求}
通常情况下:
- 加载API接口:API接口一般返回JSON、XML或其他结构化数据格式,所以Accept头部可能会包含类似application/json、application/xml等值。
- 加载图片:对于图片资源,Accept头部则会指向图像相关的MIME类型,如image/jpeg、image/png、image/gif等。