You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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原生端处理。

解决方案

核心思路

  1. 识别Blob URL:在DownloadListener中判断下载链接是否以blob:开头。
  2. 注入JavaScript:通过WebView执行JS代码,读取Blob内容并转换为Base64格式。
  3. 回调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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 22:02:00