Add image download functionality from WebView
This commit is contained in:
parent
1b409ea9a7
commit
6b940730f4
2 changed files with 166 additions and 1 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue