说明:android的webview是不支持下载的!!!
所以我们需要监听下载接口 然后手动执行下载操作,分为三种类型
- 直接打开浏览器下载(最简单),但是一些下载接口需要cookie信息时不能满足
/**
* 下载文件选择指定应用下载
*/
webView.setDownloadListener(new DownloadListener() {
@Override
public 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)
@Override
public 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) {
//获取cookie
CookieManager 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() {
@Override
public 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)
@Override
public 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)
@JavascriptInterface
public 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()));
// 创建一个文件提供者 URI
Uri fileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".flutterWebviewPluginProvider", file);
intent.setDataAndType(fileUri, mimeType); // 设置文件的 MIME 类型
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予临时权限
// 创建 PendingIntent
PendingIntent 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)
@Override
public 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等。