Android WebView内WhatsApp Web的Blob文件下载问题求助
Android WebView 下载WhatsApp Web Blob文件解决方案
问题背景
在自主开发的Android应用WebView中加载WhatsApp Web时,通过setDownloadListener获取到的下载链接为blob:协议格式,直接使用HttpURLConnection访问会抛出unknown protocol: blob错误,无法完成文件下载。
问题原因
blob: URL是浏览器在当前会话内生成的临时本地链接,仅对当前WebView上下文有效,无法通过常规HTTP请求直接访问。必须通过JavaScript在WebView内部读取Blob对象内容,再传递到Android原生端处理。
解决方案
核心思路
- 识别Blob URL:在
DownloadListener中判断下载链接是否以blob:开头。 - 注入JavaScript:通过WebView执行JS代码,读取Blob内容并转换为Base64格式。
- 回调Android端:将Base64数据传递到Android原生代码,解码后保存为本地文件。
修改后完整代码实现
首先补充必要的导入:
import android.os.Build; import android.provider.MediaStore; import android.util.Base64; import android.content.ContentValues; import android.media.MediaScannerConnection;
1. 更新WebView初始化与下载监听逻辑
在loadSite()方法中调整下载监听和添加JS接口:
@SuppressLint("ClickableViewAccessibility") public void loadSite(){ // 原有WebSettings配置保持不变... // 添加JS接口,用于接收Blob转换后的数据 webView.addJavascriptInterface(new Object() { @JavascriptInterface public void handleBlobData(String dataUrl, String contentDisposition, String mimetype) { // 子线程处理文件保存,避免阻塞UI new Thread(() -> { try { // 分离Data URL中的Base64数据部分 String base64Data = dataUrl.split(",")[1]; byte[] fileBytes = Base64.decode(base64Data, Base64.DEFAULT); // 生成文件名 String fileName = URLUtil.guessFileName(null, contentDisposition, mimetype); fileName = fileName.isEmpty() ? UUID.randomUUID().toString() : fileName; // 保存文件到下载目录 saveFileToDownloads(fileBytes, fileName, mimetype, MainActivity.this); } catch (Exception e) { runOnUiThread(() -> Toast.makeText(MainActivity.this, "下载失败: " + e.getMessage(), Toast.LENGTH_SHORT).show()); Log.e("BlobDownload", "处理Blob数据失败", e); } }).start(); } }, "AndroidBlobHandler"); // 修改下载监听逻辑 webView.setDownloadListener(new DownloadListener() { public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { Toast.makeText(MainActivity.this, "准备下载文件...", Toast.LENGTH_SHORT).show(); mCurrentDownloadRequest = url; if (checkPermission(STORAGE_PERMISSION)) { if (url.startsWith("blob:")) { // 注入JS读取Blob内容 String jsCode = "fetch('" + url + "')" + ".then(response => response.blob())" + ".then(blob => {" + " const reader = new FileReader();" + " reader.onloadend = function() {" + " window.AndroidBlobHandler.handleBlobData(this.result, '" + contentDisposition + "', '" + mimetype + "');" + " };" + " reader.readAsDataURL(blob);" + "});"; webView.evaluateJavascript(jsCode, null); } else { // 常规URL直接使用DownloadManager下载 String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype); downloadFile(url, fileName); } } else { requestPermission(STORAGE_PERMISSION); } } }); // 原有WebViewClient、WebChromeClient配置保持不变... }
2. 实现文件保存方法
添加适配不同Android版本的文件保存逻辑:
private void saveFileToDownloads(byte[] fileBytes, String fileName, String mimetype, Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10+ 适配Scoped Storage ContentValues values = new ContentValues(); values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); values.put(MediaStore.Downloads.MIME_TYPE, mimetype); values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); Uri uri = context.getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); if (uri != null) { try (OutputStream outputStream = context.getContentResolver().openOutputStream(uri)) { outputStream.write(fileBytes); runOnUiThread(() -> Toast.makeText(context, "文件已保存到下载目录", Toast.LENGTH_SHORT).show()); } catch (IOException e) { runOnUiThread(() -> Toast.makeText(context, "保存文件失败", Toast.LENGTH_SHORT).show()); Log.e("BlobDownload", "保存文件失败", e); } } } else { // Android 9及以下直接保存到公共下载目录 File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); File file = new File(downloadsDir, fileName); try (FileOutputStream outputStream = new FileOutputStream(file)) { outputStream.write(fileBytes); runOnUiThread(() -> Toast.makeText(context, "文件已保存到下载目录", Toast.LENGTH_SHORT).show()); // 通知系统扫描文件,确保文件在相册/文件管理器中可见 MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, new String[]{mimetype}, null); } catch (IOException e) { runOnUiThread(() -> Toast.makeText(context, "保存文件失败", Toast.LENGTH_SHORT).show()); Log.e("BlobDownload", "保存文件失败", e); } } } // 常规URL下载方法 private void downloadFile(String url, String fileName) { DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); request.setTitle(fileName); DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); downloadManager.enqueue(request); }
3. 补充权限请求回调
在onRequestPermissionsResult中处理权限通过后的下载触发:
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == STORAGE_PERMISSION_RESULTCODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (mCurrentDownloadRequest != null) { // 重新触发下载逻辑 if (mCurrentDownloadRequest.startsWith("blob:")) { String jsCode = "fetch('" + mCurrentDownloadRequest + "')" + ".then(response => response.blob())" + ".then(blob => {" + " const reader = new FileReader();" + " reader.onloadend = function() {" + " window.AndroidBlobHandler.handleBlobData(this.result, '', '');" + " };" + " reader.readAsDataURL(blob);" + "});"; webView.evaluateJavascript(jsCode, null); } else { String fileName = URLUtil.guessFileName(mCurrentDownloadRequest, "", ""); downloadFile(mCurrentDownloadRequest, fileName); } } } else { Toast.makeText(this, "存储权限被拒绝,无法下载文件", Toast.LENGTH_SHORT).show(); } } }
注意事项
- 确保WebView开启JavaScript支持:
webView.getSettings().setJavaScriptEnabled(true); - Android 11及以上版本,若需访问公共下载目录,需在
AndroidManifest.xml中添加android.permission.MANAGE_EXTERNAL_STORAGE权限,并引导用户手动授权。 - JS接口必须添加
@JavascriptInterface注解,否则Android 4.2+版本无法生效。
内容的提问来源于stack exchange,提问作者SHAW
相关产品推荐
相关产品推荐

