From db4f9779828169baa99514203c201270a781800f Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sun, 5 Apr 2026 14:10:24 +0800 Subject: [PATCH 01/10] Add dark theme and dynamic colors support --- .../java/com/duckai/app/web/MainActivity.kt | 6 ++++++ app/src/main/res/values-night/themes.xml | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 app/src/main/res/values-night/themes.xml diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index c7cd84f..de8c01f 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -3,6 +3,7 @@ package com.duckai.app.web import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle import android.view.inputmethod.InputMethodManager import android.webkit.CookieManager @@ -10,6 +11,7 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AppCompatActivity import com.duckai.app.R +import com.google.android.material.color.DynamicColors class MainActivity : AppCompatActivity() { @@ -20,6 +22,10 @@ class MainActivity : AppCompatActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + DynamicColors.applyToActivityIfAvailable(this) + } + super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..b9c1557 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file From fbd68b1c1cc2b7b4a05d737a9fc2030d627e00fd Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sun, 5 Apr 2026 14:15:26 +0800 Subject: [PATCH 02/10] Remove dynamic colors - not needed for webview app --- app/src/main/java/com/duckai/app/web/MainActivity.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index de8c01f..c7cd84f 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -3,7 +3,6 @@ package com.duckai.app.web import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.inputmethod.InputMethodManager import android.webkit.CookieManager @@ -11,7 +10,6 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.appcompat.app.AppCompatActivity import com.duckai.app.R -import com.google.android.material.color.DynamicColors class MainActivity : AppCompatActivity() { @@ -22,10 +20,6 @@ class MainActivity : AppCompatActivity() { } override fun onCreate(savedInstanceState: Bundle?) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - DynamicColors.applyToActivityIfAvailable(this) - } - super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) From 8637603930a5fcb6a523a7ede2b7be23a40e2c41 Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sun, 5 Apr 2026 16:18:21 +0800 Subject: [PATCH 03/10] Add file attachment functionality with permission handling --- app/build.gradle | 3 +- .../java/com/duckai/app/web/MainActivity.kt | 85 ++++++++++++++++++- app/src/main/res/drawable/ic_attach.xml | 10 +++ app/src/main/res/layout/activity_main.xml | 13 +++ app/src/main/res/values/strings.xml | 4 + 5 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_attach.xml diff --git a/app/build.gradle b/app/build.gradle index 1631f76..5066320 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "com.duckai.app" - minSdk 31 + minSdk 33 targetSdk 34 versionCode 1 versionName "1.0" @@ -35,5 +35,6 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.activity:activity-ktx:1.8.2' implementation 'com.google.android.material:material:1.11.0' } \ No newline at end of file diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index c7cd84f..3fc35a3 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -1,31 +1,67 @@ package com.duckai.app.web +import android.Manifest import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.os.Bundle +import android.view.View import android.view.inputmethod.InputMethodManager import android.webkit.CookieManager +import android.webkit.ValueCallback +import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import com.duckai.app.R class MainActivity : AppCompatActivity() { private lateinit var webView: WebView + private lateinit var attachButton: View + private var filePathCallback: ValueCallback>? = null companion object { private const val BASE_URL = "https://duck.ai" } + private val filePickerLauncher = registerForActivityResult( + ActivityResultContracts.OpenMultipleDocuments() + ) { uris -> + if (uris.isNotEmpty()) { + filePathCallback?.onReceiveValue(uris.toTypedArray()) + Toast.makeText(this, getString(R.string.file_attached), Toast.LENGTH_SHORT).show() + } else { + filePathCallback?.onReceiveValue(null) + } + filePathCallback = null + } + + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val allGranted = permissions.values.all { it } + if (allGranted) { + openFilePicker() + } else { + Toast.makeText(this, getString(R.string.permission_required), Toast.LENGTH_SHORT).show() + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) webView = findViewById(R.id.webView) + attachButton = findViewById(R.id.attachButton) setupWebView() + setupAttachButton() loadUrlFromIntent() } @@ -44,13 +80,25 @@ class MainActivity : AppCompatActivity() { CookieManager.getInstance().setAcceptCookie(true) CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true) + + webView.webChromeClient = object : WebChromeClient() { + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>?, + fileChooserParams: FileChooserParams? + ): Boolean { + this@MainActivity.filePathCallback = filePathCallback + checkPermissionAndPickFile() + return true + } + } webView.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { view?.postDelayed({ view.evaluateJavascript( "setTimeout(() => {" + - " const input = document.querySelector('input[type=\"text\"], textarea[id*=\"message\"], [role=\"combobox\"]');" + + " const input = document.querySelector('input[type=\"text\"], textarea[id*=\"message\"], [role=\"combobox\"], input[type=\"file\"]');" + " if(input) { input.focus(); input.click(); }" + "}, 100);" ) { _ -> } @@ -59,6 +107,41 @@ class MainActivity : AppCompatActivity() { } } + private fun setupAttachButton() { + attachButton.setOnClickListener { + checkPermissionAndPickFile() + } + } + + private fun checkPermissionAndPickFile() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val permissions = arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_AUDIO + ) + val notGranted = permissions.filter { + ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED + } + if (notGranted.isEmpty()) { + openFilePicker() + } else { + permissionLauncher.launch(notGranted.toTypedArray()) + } + } else { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED) { + openFilePicker() + } else { + permissionLauncher.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)) + } + } + } + + private fun openFilePicker() { + filePickerLauncher.launch(arrayOf("image/*", "video/*", "audio/*", "application/pdf")) + } + private fun loadUrlFromIntent() { val query = intent?.data?.getQueryParameter("q") val url = if (query != null) { diff --git a/app/src/main/res/drawable/ic_attach.xml b/app/src/main/res/drawable/ic_attach.xml new file mode 100644 index 0000000..7c9d932 --- /dev/null +++ b/app/src/main/res/drawable/ic_attach.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 22c3a4e..f160df8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -12,4 +12,17 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aba6527..253b724 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,4 +9,8 @@ Yes No Chat history cleared + Attach file + File attached + No app found to open this file + Storage permission required \ No newline at end of file From 00c5db0253d6ad0584ed8e671080d92cf5b524f1 Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sun, 5 Apr 2026 16:27:38 +0800 Subject: [PATCH 04/10] Handle PDF file upload from website with proper permission requests --- .../main/java/com/duckai/app/web/MainActivity.kt | 12 +----------- app/src/main/res/layout/activity_main.xml | 13 ------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index 3fc35a3..edc6d31 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -7,7 +7,6 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle -import android.view.View import android.view.inputmethod.InputMethodManager import android.webkit.CookieManager import android.webkit.ValueCallback @@ -23,7 +22,6 @@ import com.duckai.app.R class MainActivity : AppCompatActivity() { private lateinit var webView: WebView - private lateinit var attachButton: View private var filePathCallback: ValueCallback>? = null companion object { @@ -58,10 +56,8 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) webView = findViewById(R.id.webView) - attachButton = findViewById(R.id.attachButton) setupWebView() - setupAttachButton() loadUrlFromIntent() } @@ -107,12 +103,6 @@ class MainActivity : AppCompatActivity() { } } - private fun setupAttachButton() { - attachButton.setOnClickListener { - checkPermissionAndPickFile() - } - } - private fun checkPermissionAndPickFile() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val permissions = arrayOf( @@ -139,7 +129,7 @@ class MainActivity : AppCompatActivity() { } private fun openFilePicker() { - filePickerLauncher.launch(arrayOf("image/*", "video/*", "audio/*", "application/pdf")) + filePickerLauncher.launch(arrayOf("application/pdf")) } private fun loadUrlFromIntent() { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f160df8..22c3a4e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -12,17 +12,4 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - - \ No newline at end of file From 1558ead809cf3f9b40bf11c98f98dce91de77bae Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sun, 5 Apr 2026 16:33:57 +0800 Subject: [PATCH 05/10] Fix permission request - single permission on Android 12 and below --- .../java/com/duckai/app/web/MainActivity.kt | 43 ++++++------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index edc6d31..9457777 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -22,7 +22,7 @@ import com.duckai.app.R class MainActivity : AppCompatActivity() { private lateinit var webView: WebView - private var filePathCallback: ValueCallback>? = null + private var pendingFileCallback: ValueCallback>? = null companion object { private const val BASE_URL = "https://duck.ai" @@ -31,23 +31,18 @@ class MainActivity : AppCompatActivity() { private val filePickerLauncher = registerForActivityResult( ActivityResultContracts.OpenMultipleDocuments() ) { uris -> - if (uris.isNotEmpty()) { - filePathCallback?.onReceiveValue(uris.toTypedArray()) - Toast.makeText(this, getString(R.string.file_attached), Toast.LENGTH_SHORT).show() - } else { - filePathCallback?.onReceiveValue(null) - } - filePathCallback = null + pendingFileCallback?.onReceiveValue(uris.toTypedArray()) + pendingFileCallback = null } private val permissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - val allGranted = permissions.values.all { it } - if (allGranted) { + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { openFilePicker() } else { - Toast.makeText(this, getString(R.string.permission_required), Toast.LENGTH_SHORT).show() + pendingFileCallback?.onReceiveValue(null) + pendingFileCallback = null } } @@ -83,8 +78,8 @@ class MainActivity : AppCompatActivity() { filePathCallback: ValueCallback>?, fileChooserParams: FileChooserParams? ): Boolean { - this@MainActivity.filePathCallback = filePathCallback - checkPermissionAndPickFile() + pendingFileCallback = filePathCallback + checkPermissionAndOpenPicker() return true } } @@ -103,27 +98,15 @@ class MainActivity : AppCompatActivity() { } } - private fun checkPermissionAndPickFile() { + private fun checkPermissionAndOpenPicker() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val permissions = arrayOf( - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO, - Manifest.permission.READ_MEDIA_AUDIO - ) - val notGranted = permissions.filter { - ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED - } - if (notGranted.isEmpty()) { - openFilePicker() - } else { - permissionLauncher.launch(notGranted.toTypedArray()) - } + openFilePicker() } else { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { openFilePicker() } else { - permissionLauncher.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)) + permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } } } From 541b5c0672bbe523714bb3ffce7ed07d0b3d96b6 Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sun, 5 Apr 2026 16:37:45 +0800 Subject: [PATCH 06/10] Support multiple file formats for upload --- .../java/com/duckai/app/web/MainActivity.kt | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index 9457777..538736a 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -112,7 +112,34 @@ class MainActivity : AppCompatActivity() { } private fun openFilePicker() { - filePickerLauncher.launch(arrayOf("application/pdf")) + val mimeTypes = arrayOf( + "text/plain", + "text/markdown", + "text/csv", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/rtf", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "image/png", + "image/jpeg", + "image/webp", + "image/gif", + "application/x-python", + "application/javascript", + "application/java", + "application/json", + "application/xml", + "text/yaml", + "text/html", + "text/css", + "application/zip" + ) + filePickerLauncher.launch(mimeTypes) } private fun loadUrlFromIntent() { From ffb1b8dc0d386a4c5d8c61836645c0e8e791a861 Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sun, 5 Apr 2026 17:58:29 +0800 Subject: [PATCH 07/10] Add setAllowFileAccessFromFileURLs for better security --- app/src/main/java/com/duckai/app/web/MainActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index 538736a..bf3a348 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -64,6 +64,7 @@ class MainActivity : AppCompatActivity() { setSupportZoom(false) loadWithOverviewMode = true useWideViewPort = true + setAllowFileAccessFromFileURLs(false) } webView.isFocusable = true From 7349e4ef0c3dc73535c25842849d2dea85724102 Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Wed, 15 Apr 2026 16:45:05 +0800 Subject: [PATCH 08/10] Fix input field focus with proper selectors for duck.ai --- .../java/com/duckai/app/web/MainActivity.kt | 81 +++++++++++++++---- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index bf3a348..4373761 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -87,18 +87,61 @@ class MainActivity : AppCompatActivity() { webView.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { - view?.postDelayed({ - view.evaluateJavascript( - "setTimeout(() => {" + - " const input = document.querySelector('input[type=\"text\"], textarea[id*=\"message\"], [role=\"combobox\"], input[type=\"file\"]');" + - " if(input) { input.focus(); input.click(); }" + - "}, 100);" - ) { _ -> } - }, 800) + view?.postDelayed({ tryFocusInput(view) }, 2000) } } } - + + private fun tryFocusInput(view: WebView?) { + view?.evaluateJavascript( + "(function() {" + + " const selectors = [" + + " 'textarea[name=\"user-prompt\"]'," + + " 'textarea[placeholder=\"Ask privately\"]'," + + " 'textarea.JRDRiEf5NPKWK43sArdC'," + + " 'textarea[id*=\"message\"]'," + + " 'textarea[name*=\"message\"]'," + + " 'textarea[placeholder*=\"Сообщ\"]'," + + " 'textarea[placeholder*=\"Message\"]'," + + " 'textarea[data-id]'," + + " 'div[contenteditable=\"true\"]'," + + " 'div[role=\"textbox\"]'" + + " ];" + + " " + + " for (const sel of selectors) {" + + " const el = document.querySelector(sel);" + + " if (el && el.offsetParent !== null) {" + + " el.style.visibility = 'visible';" + + " el.style.display = 'block';" + + " el.scrollIntoView({block: 'center', behavior: 'instant'});" + + " el.focus();" + + " if (el.tagName === 'TEXTAREA' && el.setSelectionRange) {" + + " el.setSelectionRange(el.value?.length || 0, el.value?.length || 0);" + + " }" + + " return 'FOUND: ' + sel;" + + " }" + + " }" + + " " + + " const allTextareas = document.querySelectorAll('textarea');" + + " for (const el of allTextareas) {" + + " if (el.offsetParent !== null && el.clientHeight > 20) {" + + " el.style.visibility = 'visible';" + + " el.style.display = 'block';" + + " el.scrollIntoView({block: 'center', behavior: 'instant'});" + + " el.focus();" + + " return 'FOUND fallback: ' + el.name + ' class=' + el.className;" + + " }" + + " }" + + " " + + " return 'NOT FOUND';" + + "})();" + ) { result -> + if (result?.contains("NOT FOUND") == true) { + android.util.Log.d("DuckAI", "Focus result: $result") + } + } + } + private fun checkPermissionAndOpenPicker() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { openFilePicker() @@ -154,9 +197,20 @@ class MainActivity : AppCompatActivity() { webView.loadUrl(url) window.decorView.postDelayed({ - webView.requestFocus() + requestFocusAndShowKeyboard() + }, 1200) + } + + private fun requestFocusAndShowKeyboard() { + webView.requestFocus() + + // Try to focus input field + tryFocusInput(webView) + + // Show keyboard after JS + window.decorView.postDelayed({ showKeyboard() - }, 800) + }, 500) } private fun showKeyboard() { @@ -184,9 +238,8 @@ class MainActivity : AppCompatActivity() { val url = "$BASE_URL/?q=${Uri.encode(query)}" webView.loadUrl(url) webView.postDelayed({ - webView.requestFocus() - showKeyboard() - }, 1000) + requestFocusAndShowKeyboard() + }, 1200) } } From 1b409ea9a7ec77eda98bc2439d1b19c6f100778e Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Thu, 16 Apr 2026 00:58:18 +0800 Subject: [PATCH 09/10] Add focus on resume, duck icon, log result --- .../java/com/duckai/app/web/MainActivity.kt | 18 ++++++++++++------ .../res/drawable/ic_launcher_foreground.xml | 19 ++++++++++++------- app/src/main/res/values/colors.xml | 2 +- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/duckai/app/web/MainActivity.kt b/app/src/main/java/com/duckai/app/web/MainActivity.kt index 4373761..c53a4d4 100644 --- a/app/src/main/java/com/duckai/app/web/MainActivity.kt +++ b/app/src/main/java/com/duckai/app/web/MainActivity.kt @@ -96,14 +96,11 @@ class MainActivity : AppCompatActivity() { view?.evaluateJavascript( "(function() {" + " const selectors = [" + - " 'textarea[name=\"user-prompt\"]'," + " 'textarea[placeholder=\"Ask privately\"]'," + + " 'textarea[name=\"user-prompt\"]'," + " 'textarea.JRDRiEf5NPKWK43sArdC'," + " 'textarea[id*=\"message\"]'," + " 'textarea[name*=\"message\"]'," + - " 'textarea[placeholder*=\"Сообщ\"]'," + - " 'textarea[placeholder*=\"Message\"]'," + - " 'textarea[data-id]'," + " 'div[contenteditable=\"true\"]'," + " 'div[role=\"textbox\"]'" + " ];" + @@ -136,8 +133,9 @@ class MainActivity : AppCompatActivity() { " return 'NOT FOUND';" + "})();" ) { result -> + android.util.Log.d("DuckAI", "Focus result: " + result) if (result?.contains("NOT FOUND") == true) { - android.util.Log.d("DuckAI", "Focus result: $result") + view?.postDelayed({ tryFocusInput(view) }, 1000) } } } @@ -224,9 +222,17 @@ class MainActivity : AppCompatActivity() { super.onPause() } - override fun onResume() { +override fun onResume() { webView.onResume() super.onResume() + webView.postDelayed({ + tryFocusInput(webView) + showKeyboard() + }, 500) + webView.postDelayed({ + tryFocusInput(webView) + showKeyboard() + }, 1500) } override fun onNewIntent(intent: Intent?) { diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index b7bb7df..5859fe5 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -5,13 +5,18 @@ android:viewportWidth="108" android:viewportHeight="108"> + android:fillColor="#DE5833" + android:pathData="M54,0C24.18,0,0,24.18,0,54s24.18,54,54,54s54-24.18,54-54S83.82,0,54,0z"/> + android:pathData="M54,27c-14.91,0-27,12.09-27,27s12.09,27,27,27s27-12.09,27-27S68.91,27,54,27z"/> + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9b5e432..c197d89 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,4 +1,4 @@ - #6750A4 + #1A1A2E \ No newline at end of file From 6b940730f43206f199c775d10d6af1bb591bc40f Mon Sep 17 00:00:00 2001 From: Alex Abudaev Date: Sat, 9 May 2026 15:30:00 +0800 Subject: [PATCH 10/10] Add image download functionality from WebView --- app/src/main/AndroidManifest.xml | 2 + .../java/com/duckai/app/web/MainActivity.kt | 165 +++++++++++++++++- 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f384ed3..88115fc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + 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() { + override fun onConsoleMessage(message: String?, lineNumber: Int, sourceID: String?) { + message?.let { android.util.Log.d("DuckAI", it) } + } + override fun onShowFileChooser( webView: WebView?, filePathCallback: ValueCallback>?, @@ -88,6 +151,26 @@ class MainActivity : AppCompatActivity() { webView.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { 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';" + "})();" ) { result -> - android.util.Log.d("DuckAI", "Focus result: " + result) if (result?.contains("NOT FOUND") == true) { 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() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { openFilePicker() @@ -197,6 +351,9 @@ class MainActivity : AppCompatActivity() { window.decorView.postDelayed({ requestFocusAndShowKeyboard() }, 1200) + webView.postDelayed({ + injectDownloadHandler(webView) + }, 2000) } private fun requestFocusAndShowKeyboard() { @@ -233,6 +390,9 @@ override fun onResume() { tryFocusInput(webView) showKeyboard() }, 1500) + webView.postDelayed({ + injectDownloadHandler(webView) + }, 2000) } override fun onNewIntent(intent: Intent?) { @@ -246,6 +406,9 @@ override fun onResume() { webView.postDelayed({ requestFocusAndShowKeyboard() }, 1200) + webView.postDelayed({ + injectDownloadHandler(webView) + }, 2000) } }