Skip to content

Commit

Permalink
Merge pull request #249 from DanicMa/custom-android-chrome-client
Browse files Browse the repository at this point in the history
Custom android chrome client (allowing for file selection)
  • Loading branch information
KevinnZou authored Nov 5, 2024
2 parents ab2ea16 + 1ee4041 commit 3e95ced
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.kevinnzou.sample

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 android.widget.Toast
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 com.multiplatform.webview.web.AccompanistWebChromeClient
import com.multiplatform.webview.web.PlatformWebViewParams

@Composable
actual fun getPlatformWebViewParams(): PlatformWebViewParams? {
var fileChooserIntent by remember { mutableStateOf<Intent?>(null) }

val webViewChromeClient =
remember {
FileChoosableWebChromeClient(onShowFilePicker = { fileChooserIntent = it })
}

val launcher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
) { result: ActivityResult ->
if (result.resultCode != Activity.RESULT_OK) {
Toast.makeText(
webViewChromeClient.context,
"resultCode is not RESULT_OK (value: ${result.resultCode})",
Toast.LENGTH_SHORT,
).show()
webViewChromeClient.cancelFileChooser()
return@rememberLauncherForActivityResult
}

val intent = result.data
if (intent == null) {
Toast.makeText(
webViewChromeClient.context,
"result intent is null",
Toast.LENGTH_SHORT,
).show()
webViewChromeClient.cancelFileChooser()
return@rememberLauncherForActivityResult
}

val singleFile: Uri? = intent.data
val multiFiles: List<Uri>? = intent.getUris()

when {
singleFile != null -> webViewChromeClient.onReceiveFiles(arrayOf(singleFile))
multiFiles != null -> webViewChromeClient.onReceiveFiles(multiFiles.toTypedArray())
else -> {
Toast.makeText(
webViewChromeClient.context,
"data and clipData is null",
Toast.LENGTH_SHORT,
).show()
webViewChromeClient.cancelFileChooser()
}
}
}

LaunchedEffect(key1 = fileChooserIntent) {
if (fileChooserIntent != null) {
try {
launcher.launch(fileChooserIntent)
} catch (e: ActivityNotFoundException) {
webViewChromeClient.cancelFileChooser()
}
}
}

return PlatformWebViewParams(chromeClient = webViewChromeClient)
}

private fun Intent.getUris(): List<Uri>? {
val clipData = clipData ?: return null
return (0 until clipData.itemCount).map { clipData.getItemAt(it).uri }
}

private class FileChoosableWebChromeClient(
private val onShowFilePicker: (Intent) -> Unit,
) : AccompanistWebChromeClient() {
private var filePathCallback: ValueCallback<Array<Uri>>? = null

override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?,
): Boolean {
this.filePathCallback = filePathCallback
val filePickerIntent = fileChooserParams?.createIntent()

if (filePickerIntent == null) {
cancelFileChooser()
} else {
onShowFilePicker(filePickerIntent)
}
return true
}

fun onReceiveFiles(uris: Array<Uri>) {
filePathCallback?.onReceiveValue(uris)
filePathCallback = null
}

fun cancelFileChooser() {
filePathCallback?.onReceiveValue(null)
filePathCallback = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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.PlatformWebViewParams
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,
platformWebViewParams = getPlatformWebViewParams(),
)
}
}
}
}

@Composable
expect fun getPlatformWebViewParams(): PlatformWebViewParams?
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ internal fun WebViewApp() {
composable("intercept") {
InterceptRequestSample(controller)
}
composable("file") {
FileChooseWebViewSample(controller)
}
}
}

Expand Down Expand Up @@ -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)
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions sample/shared/src/commonMain/resources/assets/fileChoose.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<html>
<head>
<header>
<meta name='viewport'
content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no'>
</header>
<link rel="stylesheet" type="text/css" href="styles.css">
<title>Compose WebView Multiplatform</title>
</head>
<body>
<h1>Compose WebView Multiplatform</h1>
<div>
<text>image: </text>
<input type="file" multiple name="image" id="imageChoose" accept="image/*">
</div>
<div>
<text>video: </text>
<input type="file" multiple name="video" id="videoChoose" accept="video/*">
</div>
<div>
<text>audio: </text>
<input type="file" multiple name="audio" id="audioChoose" accept="audio/*">
</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.kevinnzou.sample

import androidx.compose.runtime.Composable
import com.multiplatform.webview.web.PlatformWebViewParams

@Composable
actual fun getPlatformWebViewParams(): PlatformWebViewParams? = null
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.kevinnzou.sample

import androidx.compose.runtime.Composable
import com.multiplatform.webview.web.PlatformWebViewParams

@Composable
actual fun getPlatformWebViewParams(): PlatformWebViewParams? = null
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.multiplatform.webview.web

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.multiplatform.webview.jsbridge.WebViewJsBridge

Expand All @@ -17,6 +19,7 @@ actual fun ActualWebView(
webViewJsBridge: WebViewJsBridge?,
onCreated: (NativeWebView) -> Unit,
onDispose: (NativeWebView) -> Unit,
platformWebViewParams: PlatformWebViewParams?,
factory: (WebViewFactoryParam) -> NativeWebView,
) {
AccompanistWebView(
Expand All @@ -27,6 +30,8 @@ actual fun ActualWebView(
webViewJsBridge,
onCreated = onCreated,
onDispose = onDispose,
client = platformWebViewParams?.client ?: remember { AccompanistWebViewClient() },
chromeClient = platformWebViewParams?.chromeClient ?: remember { AccompanistWebChromeClient() },
factory = { factory(WebViewFactoryParam(it)) },
)
}
Expand All @@ -36,3 +41,9 @@ actual data class WebViewFactoryParam(val context: Context)

/** Default WebView factory for Android. */
actual fun defaultWebViewFactory(param: WebViewFactoryParam) = android.webkit.WebView(param.context)

@Immutable
actual data class PlatformWebViewParams(
val client: AccompanistWebViewClient? = null,
val chromeClient: AccompanistWebChromeClient? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ fun WebView(
webViewJsBridge: WebViewJsBridge? = null,
onCreated: () -> Unit = {},
onDispose: () -> Unit = {},
platformWebViewParams: PlatformWebViewParams? = null,
) {
WebView(
state = state,
Expand All @@ -48,6 +49,7 @@ fun WebView(
webViewJsBridge = webViewJsBridge,
onCreated = { _ -> onCreated() },
onDispose = { _ -> onDispose() },
platformWebViewParams = platformWebViewParams,
)
}

Expand All @@ -74,6 +76,7 @@ fun WebView(
webViewJsBridge: WebViewJsBridge? = null,
onCreated: (NativeWebView) -> Unit = {},
onDispose: (NativeWebView) -> Unit = {},
platformWebViewParams: PlatformWebViewParams? = null,
factory: ((WebViewFactoryParam) -> NativeWebView)? = null,
) {
val webView = state.webView
Expand Down Expand Up @@ -154,6 +157,7 @@ fun WebView(
webViewJsBridge = webViewJsBridge,
onCreated = onCreated,
onDispose = onDispose,
platformWebViewParams = platformWebViewParams,
factory = factory ?: ::defaultWebViewFactory,
)

Expand All @@ -179,6 +183,14 @@ fun WebView(
*/
expect class WebViewFactoryParam

/**
* Platform specific parameters given to the WebView composable function:
* - On Android, this contains an optional `AccompanistWebViewClient` and `AccompanistWebChromeClient`
* - On iOS, this is currently unused
* - On Desktop, this is currently unused
*/
expect class PlatformWebViewParams

/**
* Platform specific default WebView factory function. This can be called from
* a custom factory function for any platforms that don't need to be customized.
Expand All @@ -197,5 +209,6 @@ expect fun ActualWebView(
webViewJsBridge: WebViewJsBridge? = null,
onCreated: (NativeWebView) -> Unit = {},
onDispose: (NativeWebView) -> Unit = {},
platformWebViewParams: PlatformWebViewParams? = null,
factory: (WebViewFactoryParam) -> NativeWebView = ::defaultWebViewFactory,
)
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ actual fun ActualWebView(
webViewJsBridge: WebViewJsBridge?,
onCreated: (NativeWebView) -> Unit,
onDispose: (NativeWebView) -> Unit,
platformWebViewParams: PlatformWebViewParams?,
factory: (WebViewFactoryParam) -> NativeWebView,
) {
DesktopWebView(
Expand Down Expand Up @@ -54,6 +55,8 @@ actual class WebViewFactoryParam(
val requestContext: CefRequestContext get() = createModifiedRequestContext(webSettings)
}

actual class PlatformWebViewParams

/** Default WebView factory for Desktop. */
actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView =
when (val content = param.state.content) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ actual fun ActualWebView(
webViewJsBridge: WebViewJsBridge?,
onCreated: (NativeWebView) -> Unit,
onDispose: (NativeWebView) -> Unit,
platformWebViewParams: PlatformWebViewParams?,
factory: (WebViewFactoryParam) -> NativeWebView,
) {
IOSWebView(
Expand All @@ -47,6 +48,8 @@ actual fun ActualWebView(
/** iOS WebView factory parameters: configuration created from WebSettings. */
actual data class WebViewFactoryParam(val config: WKWebViewConfiguration)

actual class PlatformWebViewParams

/** Default WebView factory for iOS. */
@OptIn(ExperimentalForeignApi::class)
actual fun defaultWebViewFactory(param: WebViewFactoryParam) = WKWebView(frame = CGRectZero.readValue(), configuration = param.config)
Expand Down

0 comments on commit 3e95ced

Please sign in to comment.