Add file attachment functionality with permission handling
This commit is contained in:
parent
fbd68b1c1c
commit
8637603930
5 changed files with 113 additions and 2 deletions
|
|
@ -9,7 +9,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.duckai.app"
|
applicationId "com.duckai.app"
|
||||||
minSdk 31
|
minSdk 33
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
@ -35,5 +35,6 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'androidx.activity:activity-ktx:1.8.2'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
}
|
}
|
||||||
|
|
@ -1,31 +1,67 @@
|
||||||
package com.duckai.app.web
|
package com.duckai.app.web
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.ValueCallback
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.duckai.app.R
|
import com.duckai.app.R
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var webView: WebView
|
private lateinit var webView: WebView
|
||||||
|
private lateinit var attachButton: View
|
||||||
|
private var filePathCallback: ValueCallback<Array<Uri>>? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BASE_URL = "https://duck.ai"
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
webView = findViewById(R.id.webView)
|
webView = findViewById(R.id.webView)
|
||||||
|
attachButton = findViewById(R.id.attachButton)
|
||||||
|
|
||||||
setupWebView()
|
setupWebView()
|
||||||
|
setupAttachButton()
|
||||||
loadUrlFromIntent()
|
loadUrlFromIntent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,12 +81,24 @@ class MainActivity : AppCompatActivity() {
|
||||||
CookieManager.getInstance().setAcceptCookie(true)
|
CookieManager.getInstance().setAcceptCookie(true)
|
||||||
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, 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() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
view?.postDelayed({
|
view?.postDelayed({
|
||||||
view.evaluateJavascript(
|
view.evaluateJavascript(
|
||||||
"setTimeout(() => {" +
|
"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(); }" +
|
" if(input) { input.focus(); input.click(); }" +
|
||||||
"}, 100);"
|
"}, 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() {
|
private fun loadUrlFromIntent() {
|
||||||
val query = intent?.data?.getQueryParameter("q")
|
val query = intent?.data?.getQueryParameter("q")
|
||||||
val url = if (query != null) {
|
val url = if (query != null) {
|
||||||
|
|
|
||||||
10
app/src/main/res/drawable/ic_attach.xml
Normal file
10
app/src/main/res/drawable/ic_attach.xml
Normal 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>
|
||||||
|
|
@ -12,4 +12,17 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
@ -9,4 +9,8 @@
|
||||||
<string name="yes">Yes</string>
|
<string name="yes">Yes</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="history_cleared">Chat history cleared</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>
|
</resources>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue