Add image download functionality from WebView

This commit is contained in:
Alex Abudaev 2026-05-09 15:30:00 +08:00
parent 1b409ea9a7
commit 6b940730f4
2 changed files with 166 additions and 1 deletions

View file

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View file

@ -13,7 +13,12 @@ import android.webkit.ValueCallback
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.webkit.DownloadListener
import android.widget.Toast import android.widget.Toast
import android.os.Environment
import android.app.DownloadManager
import android.provider.Settings
import java.io.File as IoFile
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -73,7 +78,65 @@ class MainActivity : AppCompatActivity() {
CookieManager.getInstance().setAcceptCookie(true) CookieManager.getInstance().setAcceptCookie(true)
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true) CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
webView.addJavascriptInterface(object {
@android.webkit.JavascriptInterface
fun downloadImage(url: String) {
runOnUiThread {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
return@runOnUiThread
}
val fileName = android.webkit.URLUtil.guessFileName(url, null, null) ?: "image_${System.currentTimeMillis()}.png"
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadRequest = DownloadManager.Request(Uri.parse(url)).apply {
setMimeType("image/png")
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
}
downloadManager.enqueue(downloadRequest)
Toast.makeText(this@MainActivity, "Downloading...", Toast.LENGTH_SHORT).show()
}
}
@android.webkit.JavascriptInterface
fun downloadBase64Image(base64Data: String) {
runOnUiThread {
try {
val base64 = base64Data.substringAfter("base64,")
val imageBytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
val fileName = "duckai_image_${System.currentTimeMillis()}.jpg"
val contentValues = android.content.ContentValues().apply {
put(android.provider.MediaStore.Downloads.DISPLAY_NAME, fileName)
put(android.provider.MediaStore.Downloads.MIME_TYPE, "image/jpeg")
put(android.provider.MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = contentResolver
val uri = resolver.insert(android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
resolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(imageBytes)
}
contentValues.clear()
contentValues.put(android.provider.MediaStore.Downloads.IS_PENDING, 0)
resolver.update(uri, contentValues, null, null)
Toast.makeText(this@MainActivity, "Файл сохранён в папке «Загрузки» (Downloads)\n$fileName", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this@MainActivity, "Download failed", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Toast.makeText(this@MainActivity, "Ошибка сохранения: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
}, "Android")
webView.webChromeClient = object : WebChromeClient() { webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(message: String?, lineNumber: Int, sourceID: String?) {
message?.let { android.util.Log.d("DuckAI", it) }
}
override fun onShowFileChooser( override fun onShowFileChooser(
webView: WebView?, webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?, filePathCallback: ValueCallback<Array<Uri>>?,
@ -88,6 +151,26 @@ class MainActivity : AppCompatActivity() {
webView.webViewClient = object : WebViewClient() { webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
view?.postDelayed({ tryFocusInput(view) }, 2000) view?.postDelayed({ tryFocusInput(view) }, 2000)
view?.postDelayed({ injectDownloadHandler(view) }, 1500)
}
}
webView.setDownloadListener { url, userAgent, contentDisposition, mimeType, contentLength ->
if (url.startsWith("blob:") || url.startsWith("data:")) {
return@setDownloadListener
} else if (!url.startsWith("http://") && !url.startsWith("https://")) {
return@setDownloadListener
} else {
val fileName = android.webkit.URLUtil.guessFileName(url, contentDisposition, mimeType) ?: "download"
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadRequest = DownloadManager.Request(Uri.parse(url)).apply {
setMimeType(mimeType)
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
}
downloadManager.enqueue(downloadRequest)
Toast.makeText(this, "Downloading...", Toast.LENGTH_SHORT).show()
} }
} }
} }
@ -133,13 +216,84 @@ class MainActivity : AppCompatActivity() {
" return 'NOT FOUND';" + " return 'NOT FOUND';" +
"})();" "})();"
) { result -> ) { result ->
android.util.Log.d("DuckAI", "Focus result: " + result)
if (result?.contains("NOT FOUND") == true) { if (result?.contains("NOT FOUND") == true) {
view?.postDelayed({ tryFocusInput(view) }, 1000) view?.postDelayed({ tryFocusInput(view) }, 1000)
} }
} }
} }
private fun injectDownloadHandler(view: WebView?) {
view?.evaluateJavascript(
"(function() {" +
" if (window.downloadHandlerInjected) return;" +
" window.downloadHandlerInjected = true;" +
" " +
" function downloadBlobImage(src) {" +
" fetch(src)" +
" .then(res => res.blob())" +
" .then(blob => {" +
" const reader = new FileReader();" +
" reader.onloadend = function() {" +
" const base64 = reader.result.split(',')[1];" +
" const link = document.createElement('a');" +
" link.href = 'data:image/png;base64,' + base64;" +
" link.download = 'duckai_image_' + Date.now() + '.png';" +
" link.click();" +
" };" +
" reader.readAsDataURL(blob);" +
" })" +
" }" +
" " +
" function findImageSrc(btn) {" +
" let el = btn;" +
" for (let i = 0; i < 5 && el; i++) {" +
" const img = el.querySelector('img');" +
" if (img && img.src) return img.src;" +
" const sources = el.querySelectorAll('source');" +
" for (let s of sources) {" +
" if (s.src && s.src.startsWith('blob:')) return s.src;" +
" }" +
" el = el.parentElement;" +
" }" +
" const allImgs = document.querySelectorAll('img');" +
" for (let img of allImgs) {" +
" if (img.src.startsWith('blob:')) return img.src;" +
" }" +
" return null;" +
" }" +
" " +
" document.body.addEventListener('click', function(e) {" +
" const target = e.target;" +
" const btn = target.closest('button');" +
" if (!btn) return;" +
" " +
" const svg = btn.querySelector('svg');" +
" if (!svg) return;" +
" " +
" const path = svg.querySelector('path');" +
" if (!path || !path.getAttribute('d')) return;" +
" " +
" const d = path.getAttribute('d');" +
" " +
" if (d.includes('M8 .5') || d.includes('8.264')) {" +
" e.preventDefault();" +
" e.stopPropagation();" +
" " +
" const imgSrc = findImageSrc(btn);" +
" if (!imgSrc) return;" +
" " +
" if (imgSrc.startsWith('data:')) {" +
" window.Android && window.Android.downloadBase64Image && window.Android.downloadBase64Image(imgSrc);" +
" } else if (imgSrc.startsWith('blob:')) {" +
" downloadBlobImage(imgSrc);" +
" } else if (imgSrc.startsWith('http')) {" +
" window.Android && window.Android.downloadImage && window.Android.downloadImage(imgSrc);" +
" }" +
" }" +
" }, true);" +
"})();" , null)
}
private fun checkPermissionAndOpenPicker() { private fun checkPermissionAndOpenPicker() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
openFilePicker() openFilePicker()
@ -197,6 +351,9 @@ class MainActivity : AppCompatActivity() {
window.decorView.postDelayed({ window.decorView.postDelayed({
requestFocusAndShowKeyboard() requestFocusAndShowKeyboard()
}, 1200) }, 1200)
webView.postDelayed({
injectDownloadHandler(webView)
}, 2000)
} }
private fun requestFocusAndShowKeyboard() { private fun requestFocusAndShowKeyboard() {
@ -233,6 +390,9 @@ override fun onResume() {
tryFocusInput(webView) tryFocusInput(webView)
showKeyboard() showKeyboard()
}, 1500) }, 1500)
webView.postDelayed({
injectDownloadHandler(webView)
}, 2000)
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
@ -246,6 +406,9 @@ override fun onResume() {
webView.postDelayed({ webView.postDelayed({
requestFocusAndShowKeyboard() requestFocusAndShowKeyboard()
}, 1200) }, 1200)
webView.postDelayed({
injectDownloadHandler(webView)
}, 2000)
} }
} }