webview里blob的下载和保存
# webview里blob的下载和保存
# 先说结论
Android里的blob下载,触发webview的DownloadListener,下载的目标url为blob:null/9296a454-2bc1-4a59-9ee7-bf1e659377a1这种格式,是无法通过注入js,js里使用XMLHttpRequest来转换为base64的,会被webview直接拦掉
需要通过webview的OnLongClickListener,在其回调里能直接拿到blob的base64字符串,解码成对应二进制然后保存就行.
放在google ai studio页面就是
页面上图片上方的下载按钮点击后无法成功下载,
但是长按图片,就可以成功保存图片到相册
# 被拦截掉的老方法
# js注入的方法:
- 发现下载里的blob协议
- 注入js到webview
- js里使用XMLHttpRequest来转换为base64
- 将base64通过jsbridge传给java代码,java代码里解码base64拿到二进制,保存到文件
结果是第三步直接报错:
(webview的settings全放开,也是过不去)
Android webview禁止通过XMLHttpRequest读取,xhr.send()直接报错:Not allowed to load local resource: blob:null/c9543d96-d22f-4fa8-ba71-3df2cca08958
1
# 通过html源码里匹配blob:的方法
拿到的html源码不全,很多隐藏在js里,所以不能像chrome tools里直接能找到blob:的链接
此路也是不通
# 测试代码:
private void doDownload(boolean hidden, String url, String name, long contentLength, boolean closeAfterSuccess, Activity activity) {
if(url.startsWith("blob:")){
WebConfigger.printSettings(webView);
//<img _ngcontent-ng-c2656717358="" class="main-media-item main-image"
// src="blob:https://aistudio.google.com/d05f4db1-d82d-43dd-9add-16865de1e3f5"
// alt="Generated Image September 05, 2025 - 2:50PM.jpeg">
//点击下载按钮后,回调到这里,url为: blob:null/c9543d96-d22f-4fa8-ba71-3df2cca08958,已经是在本地磁盘的缓存文件了,
// Android webview禁止通过XMLHttpRequest读取,xhr.send()直接报错:Not allowed to load local resource: blob:null/c9543d96-d22f-4fa8-ba71-3df2cca08958
//直接拿到src后下载https://aistudio.google.com/d05f4db1-d82d-43dd-9add-16865de1e3f5,报404,似乎一次下载后图片被删除
//每次查看大图,每次url不一样,所以应该是接到download回调后,拿到url后,去html中查找blob:字符串,拿到真正的url,然后用js侧的XMLHttpRequest转为base64
if(url.startsWith("blob:null/")){
quickWebview.getSource(new ValueCallback<String>() {
@Override
public void onReceiveValue(String html) {
List<BlobUrlParser.BlobImageInfo> blobImageInfos = BlobUrlParser.parseHtml(html);
if(blobImageInfos ==null || blobImageInfos.isEmpty()){
MyToast.error("blobImageInfos is empty");
return;
}
BlobUrlParser.BlobImageInfo info = blobImageInfos.get(0);
doDownload(hidden, info.getOriginalHttpsUrl(), info.getAltText(), contentLength, closeAfterSuccess, activity);
}
});
return;
}
//调用js桥接:
//AndroidFileWriterUtil.writeFileToDownloads(null,name,DownloadApi.create(url).getInputStream());
//args[0] = blob:null/9296a454-2bc1-4a59-9ee7-bf1e659377a1
//│ args[1] = Mozilla/5.0 (Linux; Android 11; Redmi K30 Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML,
// like Gecko) Version/4.0 Chrome/139.0.7258.158 Mobile Safari/537.36 终极图片压缩/3.1.00-debug/3100
//│ args[2] =
// │ args[3] = text/plain
// │ args[4] = 362196
// 注入JavaScript获取blob数据
// 改进的JS注入代码,避免null origin问题
// 使用XMLHttpRequest替代Fetch API,解决blob协议不支持问题
//That's the origin, file system and sandboxed iframes(maybe others) have null as their origin.
// If you set up a local sever it should say http%3A//localhost, that's http://localhost url encoded
String js = "javascript:(function() {" +
" var xhr = new XMLHttpRequest();" +
" xhr.open('GET', '" + url + "', true);" +
" xhr.responseType = 'blob';" +
" xhr.onload = function() {" +
" if (this.status === 200) {" +
" var blob = this.response;" +
" var reader = new FileReader();" +
" reader.onload = function(e) {" +
" window.blobDownload.downloadBlobFile('" + name + "', e.target.result);" +
" };" +
" reader.onerror = function() {" +
" console.error('Error reading blob file');" +
" };" +
" reader.readAsDataURL(blob);" +
" } else {" +
" console.error('Failed to fetch blob: ' + this.status);" +
" }" +
" };" +
" xhr.onerror = function() {" +
" console.error('Error fetching blob');" +
" };" +
" xhr.send();" +
"})()";
// 在主线程执行JavaScript
if(webView ==null){
MyToast.error("webView ==null, 无法下载blob,请在代码里传入webview引用");
return;
}
webView.post(() -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript(js, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
String filePath = value;
if(TextUtils.isEmpty(value) || "null".equals(value)){
LogUtils.w("file path return: ",value);
MyToast.error("文件保存失败:"+name);
}else {
DownloadCallbackDbDecorator.cutFileToMediaStore(new File(filePath),
"/"+ AppUtils.getAppName().toLowerCase()+"/nano-banana");
//todo hidden的处理
}
}
});
} else {
webView.loadUrl(js);
}
});
return;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# 通过OnLongClickListener回调直接拿base64
@Override
public boolean onLongClick(View v) {
final WebView.HitTestResult hitTestResult = quickWebview.getWebView().getHitTestResult();
int type = hitTestResult.getType();
// case WebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项
String extra = hitTestResult.getExtra();
if(!TextUtils.isEmpty(extra) && extra.contains("data:image/") && extra.contains(";base64,")){
saveBase64ImgToGallery(extra);
}else {
downloadAndSaveImgToGallery(extra,hitTestResult.getType());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
private void saveBase64ImgToGallery(String base64String) {
try {
// 提取纯Base64数据(去除前缀)
String[] parts = base64String.split(";base64,");
if (parts.length < 2) {
LogUtils.w( "Invalid Base64 string format", base64String);
MyToast.error("Invalid Base64 string format: "+base64String);
return;
}
// 解码Base64字符串为字节数组
byte[] decodedBytes = Base64.decode(parts[1], Base64.DEFAULT);
// 将字节数组转换为Bitmap
Bitmap bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);
if (bitmap == null) {
LogUtils.w("Failed to decode Base64 to Bitmap");
MyToast.error( "base64图片解码失败");
return;
}
File tmpFile = new File(Utils.getApp().getExternalCacheDir(), "webview-download");
tmpFile.mkdirs();
File file = new File(tmpFile, System.currentTimeMillis()+".jpg");
boolean compress = bitmap.compress(Bitmap.CompressFormat.JPEG, 85, new FileOutputStream(file));
if(!compress){
MyToast.error("保存图片失败");
return;
}
MediaStoreUtil.writeMediaToMediaStore(file, AppUtils.getAppName().toLowerCase(), new MyCommonCallback3<String>() {
@Override
public void onSuccess(String s) {
MyToast.success("保存成功: \n"+s);
file.delete();
}
@Override
public void onError(String code, String msg, @Nullable Throwable throwable) {
MyCommonCallback3.super.onError(code, msg, throwable);
MyToast.error(msg);
}
});
} catch (Exception e) {
LogUtils.w( "Error saving Base64 image: " ,e);
MyToast.error("保存图片失败:\n"+e.getMessage());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private void downloadAndSaveImgToGallery(String extra,int type) {
if(TextUtils.isEmpty(extra)){
MyToast.debug("图片url为空");
return;
}
String mime = null;
if(extra.startsWith("blob:")){
extra = extra.replace("blob:","");
//需要用到type
if(type == WebView.HitTestResult.IMAGE_TYPE){
mime = "image/jpeg";
}
//extra.contains("data:image/") && extra.contains(";base64,")
//todo 这里https://拿到的二进制是base64,所以需要转换成图片,要在下载框架里处理好
}
new WebviewDownladListenerImpl()
.setWebView(quickWebview.getWebView(),quickWebview)
.onDownloadStart(extra,"","",mime,-1,false);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 源代码
https://github.com/hss01248/utilcodeEnhance2/blob/combine-image-loader/baseWebview/src/main/java/com/hss01248/basewebview/TheLongPressListener.java
编辑 (opens new window)