diff --git a/gradle.properties b/gradle.properties
index df9d799..ce5f709 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
-PUBLISH_VERSION=0.4.3
+PUBLISH_VERSION=0.4.9
diff --git a/sdk-webview/src/main/AndroidManifest.xml b/sdk-webview/src/main/AndroidManifest.xml
index 94cbbcf..cef4c1f 100644
--- a/sdk-webview/src/main/AndroidManifest.xml
+++ b/sdk-webview/src/main/AndroidManifest.xml
@@ -1 +1,11 @@
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewActivity.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewActivity.kt
new file mode 100644
index 0000000..fb0b0e9
--- /dev/null
+++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewActivity.kt
@@ -0,0 +1,222 @@
+package com.xuqm.sdk.webview
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.pm.PackageManager
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.PermissionRequest
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import android.widget.FrameLayout
+import androidx.activity.ComponentActivity
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import java.io.File
+
+class XWebViewActivity : ComponentActivity() {
+
+ private lateinit var webView: WebView
+ private val mainHandler = Handler(Looper.getMainLooper())
+ private var pendingWebRtcRequest: PermissionRequest? = null
+ private var pendingFileCallback: ValueCallback>? = null
+ private var pendingCameraUri: Uri? = null
+ private var shouldLaunchCamera = false
+ private var currentUrl: String? = null
+
+ private val webRtcPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ val req = pendingWebRtcRequest
+ pendingWebRtcRequest = null
+ if (granted) req?.grant(req.resources) else req?.deny()
+ }
+
+ private val takePictureLauncher = registerForActivityResult(
+ ActivityResultContracts.TakePicture()
+ ) { success ->
+ val cb = pendingFileCallback
+ val uri = pendingCameraUri
+ pendingFileCallback = null
+ pendingCameraUri = null
+ cb?.onReceiveValue(if (success && uri != null) arrayOf(uri) else null)
+ }
+
+ private val pickContentLauncher = registerForActivityResult(
+ ActivityResultContracts.GetContent()
+ ) { uri ->
+ val cb = pendingFileCallback
+ pendingFileCallback = null
+ cb?.onReceiveValue(if (uri != null) arrayOf(uri) else null)
+ }
+
+ private val fileCameraPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ val launch = shouldLaunchCamera
+ shouldLaunchCamera = false
+ if (granted && launch) {
+ runCatching {
+ val imageFile = File.createTempFile("cam_", ".jpg", cacheDir)
+ val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", imageFile)
+ pendingCameraUri = uri
+ takePictureLauncher.launch(uri)
+ }.onFailure {
+ pendingFileCallback?.onReceiveValue(null)
+ pendingFileCallback = null
+ }
+ } else {
+ pendingFileCallback?.onReceiveValue(null)
+ pendingFileCallback = null
+ }
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ val config = getXWebViewConfig()
+
+ val container = FrameLayout(this)
+ webView = WebView(this).apply {
+ settings.javaScriptEnabled = true
+ settings.domStorageEnabled = true
+ settings.useWideViewPort = true
+ settings.loadWithOverviewMode = true
+ config.userAgent?.let { settings.userAgentString = it }
+
+ addJavascriptInterface(
+ XWebViewJsBridge(mainHandler) { config.onMessage },
+ "ReactNativeWebView",
+ )
+
+ webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView?, url: String?) {
+ currentUrl = url ?: view?.url
+ super.onPageFinished(view, url)
+ view?.evaluateJavascript(buildInjectedJs(config), null)
+ }
+
+ override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false
+ }
+
+ webChromeClient = object : WebChromeClient() {
+ override fun onPermissionRequest(request: PermissionRequest) {
+ if (PermissionRequest.RESOURCE_VIDEO_CAPTURE in request.resources) {
+ if (ContextCompat.checkSelfPermission(this@XWebViewActivity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+ request.grant(request.resources)
+ } else {
+ pendingWebRtcRequest = request
+ webRtcPermissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ } else {
+ request.deny()
+ }
+ }
+
+ override fun onShowFileChooser(
+ webView: WebView?,
+ filePathCallback: ValueCallback>,
+ fileChooserParams: FileChooserParams,
+ ): Boolean {
+ pendingFileCallback?.onReceiveValue(null)
+ pendingFileCallback = filePathCallback
+
+ val isCameraCapture = fileChooserParams.isCaptureEnabled &&
+ fileChooserParams.acceptTypes.any { it.contains("image") }
+
+ if (isCameraCapture) {
+ if (ContextCompat.checkSelfPermission(this@XWebViewActivity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+ runCatching {
+ val imageFile = File.createTempFile("cam_", ".jpg", cacheDir)
+ val uri = FileProvider.getUriForFile(this@XWebViewActivity, "${packageName}.fileprovider", imageFile)
+ pendingCameraUri = uri
+ takePictureLauncher.launch(uri)
+ }.onFailure {
+ pendingFileCallback?.onReceiveValue(null)
+ pendingFileCallback = null
+ }
+ } else {
+ shouldLaunchCamera = true
+ fileCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+ } else {
+ val mimeType = fileChooserParams.acceptTypes
+ .firstOrNull { it.isNotBlank() } ?: "image/*"
+ pickContentLauncher.launch(mimeType)
+ }
+ return true
+ }
+ }
+ }
+ container.addView(webView, FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ))
+ setContentView(container)
+
+ ViewCompat.setOnApplyWindowInsetsListener(container) { v, insets ->
+ val bars = insets.getInsets(
+ WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
+ )
+ v.setPadding(0, bars.top, 0, bars.bottom)
+ insets
+ }
+
+ container.viewTreeObserver.addOnGlobalLayoutListener {
+ recalculateBottomPadding(container)
+ }
+
+ setXWebViewController(object : XWebViewController {
+ override fun canGoBack(): Boolean = webView.canGoBack()
+ override fun canGoForward(): Boolean = webView.canGoForward()
+ override fun currentUrl(): String? = currentUrl ?: webView.url
+ override fun goBack() { if (webView.canGoBack()) webView.goBack() }
+ override fun goForward() { if (webView.canGoForward()) webView.goForward() }
+ override fun reload() { webView.reload() }
+ override fun loadUrl(url: String) { webView.loadUrl(url) }
+ override fun postMessageToWeb(js: String) { webView.evaluateJavascript(js, null) }
+ })
+
+ if (config.url.isNotBlank()) {
+ webView.loadUrl(config.url)
+ }
+ }
+
+ private fun recalculateBottomPadding(container: View) {
+ val visibleFrame = Rect()
+ container.getWindowVisibleDisplayFrame(visibleFrame)
+ val screenHeight = resources.displayMetrics.heightPixels
+ val bottomHidden = screenHeight - visibleFrame.bottom
+ val currentTop = container.paddingTop
+ val currentBottom = container.paddingBottom
+ val neededBottom = maxOf(currentBottom, bottomHidden)
+ if (neededBottom != currentBottom) {
+ container.setPadding(0, currentTop, 0, neededBottom)
+ }
+ }
+
+ @Suppress("OVERRIDE_DEPRECATION")
+ override fun onBackPressed() {
+ if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
+ }
+
+ override fun onDestroy() {
+ setXWebViewController(null)
+ webView.destroy()
+ super.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewBridge.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewBridge.kt
index f14bec7..fbc6315 100644
--- a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewBridge.kt
+++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewBridge.kt
@@ -1,5 +1,7 @@
package com.xuqm.sdk.webview
+import android.content.Context
+import android.content.Intent
import androidx.compose.runtime.mutableStateOf
private val currentConfig = mutableStateOf(XWebViewConfig())
@@ -9,6 +11,11 @@ fun openXWebView(config: XWebViewConfig) {
currentConfig.value = config
}
+fun openXWebView(context: Context, config: XWebViewConfig) {
+ currentConfig.value = config
+ context.startActivity(Intent(context, XWebViewActivity::class.java))
+}
+
fun getXWebViewConfig(): XWebViewConfig = currentConfig.value
fun setXWebViewController(controller: XWebViewController?) {
diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt
index b1f7217..cbb9809 100644
--- a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt
+++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt
@@ -32,7 +32,7 @@ import java.io.File
// JS injected into every page to bridge dialog APIs and download interception.
// Uses addJavascriptInterface("ReactNativeWebView") for the JS→Native channel.
-private val DIALOG_OVERRIDE_JS = """
+internal val DIALOG_OVERRIDE_JS = """
(function() {
function post(obj) {
window.ReactNativeWebView.postMessage(JSON.stringify(obj));
@@ -103,12 +103,12 @@ private val DIALOG_OVERRIDE_JS = """
true;
""".trimIndent()
-private fun buildInjectedJs(config: XWebViewConfig): String =
+internal fun buildInjectedJs(config: XWebViewConfig): String =
DIALOG_OVERRIDE_JS + "\n" + (config.injectedJavaScript ?: "") + "\ntrue;"
// Routes window.ReactNativeWebView.postMessage() calls to [onMessage].
// @JavascriptInterface methods are called on a background thread; we post to main.
-private class XWebViewJsBridge(
+internal class XWebViewJsBridge(
private val mainHandler: Handler,
private val onMessage: () -> ((String) -> Unit)?,
) {