|
@@ -0,0 +1,171 @@
|
|
|
+package com.hzliuzhi.applet.browser.webview
|
|
|
+
|
|
|
+import android.content.Context
|
|
|
+import android.net.Uri
|
|
|
+import android.os.Environment
|
|
|
+import android.webkit.ValueCallback
|
|
|
+import android.webkit.WebChromeClient.FileChooserParams
|
|
|
+import androidx.activity.compose.BackHandler
|
|
|
+import androidx.activity.compose.rememberLauncherForActivityResult
|
|
|
+import androidx.activity.result.contract.ActivityResultContracts
|
|
|
+import androidx.compose.material3.AlertDialog
|
|
|
+import androidx.compose.material3.Text
|
|
|
+import androidx.compose.material3.TextButton
|
|
|
+import androidx.compose.runtime.Composable
|
|
|
+import androidx.compose.runtime.LaunchedEffect
|
|
|
+import androidx.compose.runtime.collectAsState
|
|
|
+import androidx.compose.runtime.getValue
|
|
|
+import androidx.compose.runtime.mutableStateOf
|
|
|
+import androidx.compose.runtime.remember
|
|
|
+import androidx.compose.runtime.setValue
|
|
|
+import androidx.compose.ui.platform.LocalContext
|
|
|
+import androidx.core.content.FileProvider
|
|
|
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
|
+import com.google.accompanist.permissions.isGranted
|
|
|
+import com.google.accompanist.permissions.rememberPermissionState
|
|
|
+import java.io.File
|
|
|
+import java.text.SimpleDateFormat
|
|
|
+import java.util.Date
|
|
|
+import java.util.Locale
|
|
|
+
|
|
|
+@OptIn(ExperimentalPermissionsApi::class)
|
|
|
+@Composable
|
|
|
+fun WebViewFileChooser(controller: WebViewController) {
|
|
|
+ val context = LocalContext.current.applicationContext
|
|
|
+
|
|
|
+ val fileChooser by controller.fileChooser.collectAsState()
|
|
|
+ var showDialog by remember { mutableStateOf(false) }
|
|
|
+ var temporaryUri by remember { mutableStateOf<Uri?>(null) }
|
|
|
+
|
|
|
+ var file: File? = null
|
|
|
+
|
|
|
+
|
|
|
+ fun callback(values: Array<Uri>?) {
|
|
|
+ fileChooser?.callback?.onReceiveValue(values ?: emptyArray())
|
|
|
+ controller.fileChooser.value = null
|
|
|
+ file = null
|
|
|
+ }
|
|
|
+
|
|
|
+ fun callback(value: Uri?) = callback(if (value != null) arrayOf(value) else emptyArray())
|
|
|
+
|
|
|
+ fun callback() {
|
|
|
+ callback(emptyArray())
|
|
|
+ // 清理未用的临时文件
|
|
|
+ file?.also { deleteTempFileByUri(it) }
|
|
|
+ file = null
|
|
|
+ showDialog = false
|
|
|
+ }
|
|
|
+
|
|
|
+ val capturePictureLauncher = rememberLauncherForActivityResult(
|
|
|
+ contract = ActivityResultContracts.TakePicture()
|
|
|
+ ) { success ->
|
|
|
+ temporaryUri.takeIf { success }?.also { callback(it) } ?: callback()
|
|
|
+ temporaryUri = null
|
|
|
+ }
|
|
|
+
|
|
|
+ val selectPictureLauncher = rememberLauncherForActivityResult(
|
|
|
+ contract = ActivityResultContracts.GetContent(),
|
|
|
+ onResult = ::callback
|
|
|
+ )
|
|
|
+
|
|
|
+ val selectMultiPictureLauncher = rememberLauncherForActivityResult(
|
|
|
+ contract = ActivityResultContracts.GetMultipleContents()
|
|
|
+ ) { uris ->
|
|
|
+ callback(uris.takeIf { it.isNotEmpty() }?.toTypedArray())
|
|
|
+ }
|
|
|
+
|
|
|
+ // 单选 launcher
|
|
|
+ val singleLauncher = rememberLauncherForActivityResult(
|
|
|
+ contract = ActivityResultContracts.GetContent(),
|
|
|
+ onResult = ::callback
|
|
|
+ )
|
|
|
+
|
|
|
+ // 多选 launcher
|
|
|
+ val multiLauncher = rememberLauncherForActivityResult(
|
|
|
+ contract = ActivityResultContracts.OpenMultipleDocuments()
|
|
|
+ ) { uris ->
|
|
|
+ callback(uris.takeIf { it.isNotEmpty() }?.toTypedArray())
|
|
|
+ }
|
|
|
+
|
|
|
+ val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
|
|
|
+ fun capturePicture() {
|
|
|
+ file?.also { deleteTempFileByUri(it) }
|
|
|
+ if (cameraPermissionState.status.isGranted) {
|
|
|
+ temporaryUri = createImageUri(context) { file = it }.also { capturePictureLauncher.launch(it) }
|
|
|
+ } else {
|
|
|
+ cameraPermissionState.launchPermissionRequest()
|
|
|
+ }
|
|
|
+ showDialog = false
|
|
|
+ }
|
|
|
+
|
|
|
+ fun selectPicture() {
|
|
|
+ showDialog = false
|
|
|
+ if (fileChooser?.params?.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
|
|
|
+ selectMultiPictureLauncher.launch("image/*")
|
|
|
+ } else {
|
|
|
+ selectPictureLauncher.launch("image/*")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ LaunchedEffect(fileChooser) {
|
|
|
+ fileChooser?.let { request ->
|
|
|
+ val mimeTypes = request.params?.acceptTypes?.filter { it.isNotBlank() }?.toTypedArray()
|
|
|
+ val mimeType = when {
|
|
|
+ mimeTypes.isNullOrEmpty() -> "*/*"
|
|
|
+ mimeTypes.size == 1 -> mimeTypes[0]
|
|
|
+ else -> "*/*"
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mimeType.startsWith("image/")) {
|
|
|
+ request.params?.isCaptureEnabled?.takeIf { it }?.also {
|
|
|
+ capturePicture()
|
|
|
+ } ?: run {
|
|
|
+ showDialog = true
|
|
|
+ }
|
|
|
+ } else if (request.params?.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {
|
|
|
+ multiLauncher.launch(mimeTypes?.takeIf { it.isNotEmpty() } ?: arrayOf("*/*"))
|
|
|
+ } else {
|
|
|
+ singleLauncher.launch(mimeType)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (showDialog) {
|
|
|
+ AlertDialog(
|
|
|
+ onDismissRequest = ::callback,
|
|
|
+ title = { Text("选择操作") },
|
|
|
+ text = { Text("请选择要拍照还是从相册选择照片") },
|
|
|
+ confirmButton = {
|
|
|
+ TextButton(onClick = ::capturePicture) { Text("拍照") }
|
|
|
+ },
|
|
|
+ dismissButton = {
|
|
|
+ TextButton(onClick = ::selectPicture) { Text("选择照片") }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ BackHandler(showDialog) {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建临时图片 Uri
|
|
|
+private fun createImageUri(context: Context, onSave: ((File) -> Unit) = {}): Uri {
|
|
|
+ val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
|
|
+ val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
|
|
+ val file = File.createTempFile("JPEG_${timeStamp}_", ".jpg", storageDir).also { onSave.invoke(it) }
|
|
|
+ return FileProvider.getUriForFile(context, "${context.packageName}.file.provider", file)
|
|
|
+}
|
|
|
+
|
|
|
+private fun deleteTempFileByUri(file: File): Boolean {
|
|
|
+ return try {
|
|
|
+ if (file.exists()) file.delete() else false
|
|
|
+ } catch (_: Exception) {
|
|
|
+ false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+data class FileChooserRequest(
|
|
|
+ val callback: ValueCallback<Array<Uri>>,
|
|
|
+ val params: FileChooserParams?,
|
|
|
+)
|