diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.kt new file mode 100644 index 00000000..ba7a9cad --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/FileChooseWebViewSample.kt @@ -0,0 +1,68 @@ +package com.kevinnzou.sample + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import com.multiplatform.webview.util.KLogSeverity +import com.multiplatform.webview.web.WebView +import com.multiplatform.webview.web.rememberWebViewNavigator +import com.multiplatform.webview.web.rememberWebViewStateWithHTMLFile + +/** + * Created By briandr97 2024/8/8 + * + * Basic Sample for choose file in webview + */ +@Composable +internal fun FileChooseWebViewSample(navHostController: NavHostController? = null) { + val webViewState = rememberWebViewStateWithHTMLFile(fileName = "fileChoose.html") + val webViewNavigator = rememberWebViewNavigator() + LaunchedEffect(Unit) { + webViewState.webSettings.apply { + zoomLevel = 1.0 + logSeverity = KLogSeverity.Debug + androidWebSettings.apply { + isAlgorithmicDarkeningAllowed = true + safeBrowsingEnabled = true + allowFileAccess = false + } + } + } + MaterialTheme { + Column { + TopAppBar( + title = { Text(text = "Html Sample") }, + navigationIcon = { + IconButton(onClick = { + navHostController?.popBackStack() + }) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) + + Box(Modifier.fillMaxSize()) { + WebView( + state = webViewState, + modifier = Modifier.fillMaxSize(), + captureBackPresses = false, + navigator = webViewNavigator, + ) + } + } + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt index 24d92ea4..cdfb1ea2 100644 --- a/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/kevinnzou/sample/WebViewApp.kt @@ -50,6 +50,9 @@ internal fun WebViewApp() { composable("intercept") { InterceptRequestSample(controller) } + composable("file") { + FileChooseWebViewSample(controller) + } } } @@ -83,6 +86,12 @@ fun MainScreen(controller: NavController) { }) { Text("Intercept Request Sample", fontSize = 18.sp) } + Spacer(modifier = Modifier.height(20.dp)) + Button(onClick = { + controller.navigate("file") + }) { + Text("File Choose Sample", fontSize = 18.sp) + } } } diff --git a/sample/shared/src/commonMain/resources/assets/fileChoose.html b/sample/shared/src/commonMain/resources/assets/fileChoose.html new file mode 100644 index 00000000..c59b8543 --- /dev/null +++ b/sample/shared/src/commonMain/resources/assets/fileChoose.html @@ -0,0 +1,25 @@ + + +
+ +
+ + Compose WebView Multiplatform + + +

Compose WebView Multiplatform

+
+ image: + +
+
+ video: + +
+
+ audio: + +
+ + \ No newline at end of file diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/FileChoosableWebView.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/FileChoosableWebView.kt new file mode 100644 index 00000000..956cee05 --- /dev/null +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/FileChoosableWebView.kt @@ -0,0 +1,128 @@ +package com.multiplatform.webview.web + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.webkit.ValueCallback +import android.webkit.WebView +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.multiplatform.webview.jsbridge.WebViewJsBridge +import com.multiplatform.webview.util.KLogger + +@Composable +fun FileChoosableWebView( + state: WebViewState, + modifier: Modifier, + captureBackPresses: Boolean, + navigator: WebViewNavigator, + webViewJsBridge: WebViewJsBridge?, + onCreated: (NativeWebView) -> Unit, + onDispose: (NativeWebView) -> Unit, + factory: (WebViewFactoryParam) -> NativeWebView, +) { + var fileChooserIntent by remember { mutableStateOf(null) } + + val webViewChromeClient = + remember { + FileChoosableWebChromeClient(onShowFilePicker = { fileChooserIntent = it }) + } + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + ) { result: ActivityResult -> + if (result.resultCode != Activity.RESULT_OK) { + KLogger.d { "resultCode is not RESULT_OK (value: ${result.resultCode})" } + webViewChromeClient.cancelFileChooser() + return@rememberLauncherForActivityResult + } + + val intent = result.data + if (intent == null) { + KLogger.d { "result intent is null" } + webViewChromeClient.cancelFileChooser() + return@rememberLauncherForActivityResult + } + + val singleFile: Uri? = intent.data + val multiFiles: List? = intent.getUris() + + when { + singleFile != null -> webViewChromeClient.onReceiveFiles(arrayOf(singleFile)) + multiFiles != null -> webViewChromeClient.onReceiveFiles(multiFiles.toTypedArray()) + else -> { + KLogger.d { "data and clipData is null" } + webViewChromeClient.cancelFileChooser() + } + } + } + + LaunchedEffect(key1 = fileChooserIntent) { + if (fileChooserIntent != null) { + try { + launcher.launch(fileChooserIntent) + } catch (e: ActivityNotFoundException) { + webViewChromeClient.cancelFileChooser() + } + } + } + + AccompanistWebView( + state, + modifier, + captureBackPresses, + navigator, + webViewJsBridge, + onCreated = onCreated, + onDispose = onDispose, + factory = { factory(WebViewFactoryParam(it)) }, + chromeClient = webViewChromeClient, + ) +} + +private fun Intent.getUris(): List? { + val clipData = clipData ?: return null + return (0 until clipData.itemCount).map { clipData.getItemAt(it).uri } +} + +class FileChoosableWebChromeClient( + private val onShowFilePicker: (Intent) -> Unit, +) : AccompanistWebChromeClient() { + private var filePathCallback: ValueCallback>? = null + + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>?, + fileChooserParams: FileChooserParams?, + ): Boolean { + this.filePathCallback = filePathCallback + val filePickerIntent = fileChooserParams?.createIntent() + + if (filePickerIntent == null) { + cancelFileChooser() + } else { + onShowFilePicker(filePickerIntent) + } + return true + } + + fun onReceiveFiles(uris: Array) { + filePathCallback?.onReceiveValue(uris) + filePathCallback = null + } + + fun cancelFileChooser() { + filePathCallback?.onReceiveValue(null) + filePathCallback = null + } +} diff --git a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt index f20b4c68..b5c30bec 100644 --- a/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt +++ b/webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt @@ -19,7 +19,7 @@ actual fun ActualWebView( onDispose: (NativeWebView) -> Unit, factory: (WebViewFactoryParam) -> NativeWebView, ) { - AccompanistWebView( + FileChoosableWebView( state, modifier, captureBackPresses, @@ -27,7 +27,7 @@ actual fun ActualWebView( webViewJsBridge, onCreated = onCreated, onDispose = onDispose, - factory = { factory(WebViewFactoryParam(it)) }, + factory = factory, ) }