Add file attachment functionality with permission handling

This commit is contained in:
Alex Abudaev 2026-04-05 16:18:21 +08:00
parent fbd68b1c1c
commit 8637603930
5 changed files with 113 additions and 2 deletions

View file

@ -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'
}

View file

@ -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<Array<Uri>>? = 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()
}
@ -45,12 +81,24 @@ class MainActivity : AppCompatActivity() {
CookieManager.getInstance().setAcceptCookie(true)
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
webView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
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) {

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/>
</vector>

View file

@ -12,4 +12,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/attachButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginBottom="80dp"
android:contentDescription="@string/attach_file"
android:src="@drawable/ic_attach"
app:fabSize="mini"
app:tint="?attr/colorOnPrimaryContainer"
app:backgroundTint="?attr/colorPrimaryContainer" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -9,4 +9,8 @@
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="history_cleared">Chat history cleared</string>
<string name="attach_file">Attach file</string>
<string name="file_attached">File attached</string>
<string name="no_app_for_file">No app found to open this file</string>
<string name="permission_required">Storage permission required</string>
</resources>