


























@@ -1,10 +1,10 @@
11package ai.openclaw.app.ui
2233import android.annotation.SuppressLint
4+import android.net.Uri
45import android.util.Log
56import android.view.View
67import android.webkit.ConsoleMessage
7-import android.webkit.JavascriptInterface
88import android.webkit.WebChromeClient
99import android.webkit.WebResourceError
1010import android.webkit.WebResourceRequest
@@ -14,135 +14,155 @@ import android.webkit.WebView
1414import android.webkit.WebViewClient
1515import androidx.compose.runtime.Composable
1616import androidx.compose.runtime.DisposableEffect
17-import androidx.compose.runtime.mutableStateOf
1817import androidx.compose.runtime.remember
1918import androidx.compose.ui.Modifier
2019import androidx.compose.ui.platform.LocalContext
2120import androidx.compose.ui.viewinterop.AndroidView
21+import androidx.webkit.JavaScriptReplyProxy
22+import androidx.webkit.WebMessageCompat
2223import androidx.webkit.WebSettingsCompat
24+import androidx.webkit.WebViewCompat
2325import androidx.webkit.WebViewFeature
2426import ai.openclaw.app.MainViewModel
2527import java.util.concurrent.atomic.AtomicReference
26282729@SuppressLint("SetJavaScriptEnabled")
30+@Suppress("DEPRECATION")
2831@Composable
2932fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier = Modifier) {
3033val context = LocalContext.current
3134val isDebuggable = (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
32-val webViewRef = remember { mutableStateOf<WebView?>(null) }
35+val webViewRef = remember { arrayOfNulls<WebView>(1) }
3336val currentPageUrlRef = remember { AtomicReference<String?>(null) }
34373538DisposableEffect(viewModel) {
3639 onDispose {
37-val webView = webViewRef.value ?: return@onDispose
40+val webView = webViewRef[0] ?: return@onDispose
3841 viewModel.canvas.detach(webView)
39- webView.removeJavascriptInterface(CanvasA2UIActionBridge.interfaceName)
42+if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
43+WebViewCompat.removeWebMessageListener(webView, CanvasA2UIActionBridge.interfaceName)
44+ }
4045 webView.stopLoading()
4146 webView.destroy()
42- webViewRef.value = null
47+ webViewRef[0] = null
4348 }
4449 }
45504651AndroidView(
4752 modifier = modifier,
4853 factory = {
49-WebView(context).apply {
50- visibility = if (visible) View.VISIBLE else View.INVISIBLE
51- settings.javaScriptEnabled = true
52- settings.domStorageEnabled = true
53- settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
54- settings.useWideViewPort = false
55- settings.loadWithOverviewMode = false
56- settings.builtInZoomControls = false
57- settings.displayZoomControls = false
58- settings.setSupportZoom(false)
59-// targetSdk 33+ ignores Force Dark APIs, so only opt out through the supported
60-// algorithmic darkening flag when this WebView implementation exposes it.
61-if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
62-WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
63- }
64-if (isDebuggable) {
65-Log.d("OpenClawWebView", "userAgent: ${settings.userAgentString}")
66- }
67- isScrollContainer = true
68- overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
69- isVerticalScrollBarEnabled = true
70- isHorizontalScrollBarEnabled = true
71- webViewClient =
72-object : WebViewClient() {
73-override fun onPageStarted(
74-view: WebView,
75-url: String?,
76-favicon: android.graphics.Bitmap?,
77- ) {
78- currentPageUrlRef.set(url)
79- }
80-81-override fun onReceivedError(
82-view: WebView,
83-request: WebResourceRequest,
84-error: WebResourceError,
85- ) {
86-if (!isDebuggable || !request.isForMainFrame) return
87-Log.e("OpenClawWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}")
88- }
54+val webView = WebView(context)
55+val webSettings = webView.settings
56+ webSettings.setAllowContentAccess(false)
57+ webSettings.setAllowFileAccess(false)
58+ webSettings.setAllowFileAccessFromFileURLs(false)
59+ webSettings.setAllowUniversalAccessFromFileURLs(false)
60+ webSettings.setSafeBrowsingEnabled(true)
61+ webSettings.javaScriptEnabled = true
62+ webSettings.domStorageEnabled = true
63+ webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
64+ webSettings.useWideViewPort = false
65+ webSettings.loadWithOverviewMode = false
66+ webSettings.builtInZoomControls = false
67+ webSettings.displayZoomControls = false
68+ webSettings.setSupportZoom(false)
69+ webView.visibility = if (visible) View.VISIBLE else View.INVISIBLE
70+// targetSdk 33+ ignores Force Dark APIs, so only opt out through the supported
71+// algorithmic darkening flag when this WebView implementation exposes it.
72+if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
73+WebSettingsCompat.setAlgorithmicDarkeningAllowed(webSettings, false)
74+ }
75+if (isDebuggable) {
76+Log.d("OpenClawWebView", "userAgent: ${webSettings.userAgentString}")
77+ }
78+ webView.isScrollContainer = true
79+ webView.overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
80+ webView.isVerticalScrollBarEnabled = true
81+ webView.isHorizontalScrollBarEnabled = true
82+ webView.webViewClient =
83+object : WebViewClient() {
84+override fun onPageStarted(
85+view: WebView,
86+url: String?,
87+favicon: android.graphics.Bitmap?,
88+ ) {
89+ currentPageUrlRef.set(url)
90+ }
899190-override fun onReceivedHttpError(
91-view: WebView,
92-request: WebResourceRequest,
93-errorResponse: WebResourceResponse,
94- ) {
95-if (!isDebuggable || !request.isForMainFrame) return
96-Log.e(
97-"OpenClawWebView",
98-"onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}",
99- )
100- }
92+override fun onReceivedError(
93+view: WebView,
94+request: WebResourceRequest,
95+error: WebResourceError,
96+ ) {
97+if (!isDebuggable || !request.isForMainFrame) return
98+Log.e("OpenClawWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}")
99+ }
101100102-override fun onPageFinished(view: WebView, url: String?) {
103- currentPageUrlRef.set(url)
104-if (isDebuggable) {
105-Log.d("OpenClawWebView", "onPageFinished: $url")
106- }
107- viewModel.canvas.onPageFinished()
108- }
101+override fun onReceivedHttpError(
102+view: WebView,
103+request: WebResourceRequest,
104+errorResponse: WebResourceResponse,
105+ ) {
106+if (!isDebuggable || !request.isForMainFrame) return
107+Log.e(
108+"OpenClawWebView",
109+"onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}",
110+ )
111+ }
109112110-override fun onRenderProcessGone(
111-view: WebView,
112-detail: android.webkit.RenderProcessGoneDetail,
113- ): Boolean {
114-if (isDebuggable) {
115-Log.e(
116-"OpenClawWebView",
117-"onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}",
118- )
119- }
120-return true
113+override fun onPageFinished(view: WebView, url: String?) {
114+ currentPageUrlRef.set(url)
115+if (isDebuggable) {
116+Log.d("OpenClawWebView", "onPageFinished: $url")
121117 }
118+ viewModel.canvas.onPageFinished()
122119 }
123- webChromeClient =
124-object : WebChromeClient() {
125-override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
126-if (!isDebuggable) return false
127-val msg = consoleMessage ?: return false
128-Log.d(
120+121+override fun onRenderProcessGone(
122+view: WebView,
123+detail: android.webkit.RenderProcessGoneDetail,
124+ ): Boolean {
125+if (isDebuggable) {
126+Log.e(
129127"OpenClawWebView",
130-"console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}",
128+"onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}",
131129 )
132-return false
133130 }
131+return true
134132 }
135-136-val bridge =
137-CanvasA2UIActionBridge(
138- isTrustedPage = { viewModel.isTrustedCanvasActionUrl(currentPageUrlRef.get()) },
139- ) { payload ->
140- viewModel.handleCanvasA2UIActionFromWebView(payload)
133+ }
134+ webView.webChromeClient =
135+object : WebChromeClient() {
136+override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
137+if (!isDebuggable) return false
138+val msg = consoleMessage ?: return false
139+Log.d(
140+"OpenClawWebView",
141+"console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}",
142+ )
143+return false
141144 }
142- addJavascriptInterface(bridge, CanvasA2UIActionBridge.interfaceName)
143- viewModel.canvas.attach(this)
144- webViewRef.value = this
145+ }
146+147+val bridge =
148+CanvasA2UIActionBridge(
149+ isTrustedPage = { viewModel.isTrustedCanvasActionUrl(currentPageUrlRef.get()) },
150+ ) { payload ->
151+ viewModel.handleCanvasA2UIActionFromWebView(payload)
152+ }
153+if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
154+WebViewCompat.addWebMessageListener(
155+ webView,
156+CanvasA2UIActionBridge.interfaceName,
157+CanvasA2UIActionBridge.allowedOriginRules,
158+ bridge,
159+ )
160+ } else if (isDebuggable) {
161+Log.w("OpenClawWebView", "WebMessageListener unsupported; canvas actions disabled")
145162 }
163+ viewModel.canvas.attach(webView)
164+ webViewRef[0] = webView
165+ webView
146166 },
147167 update = { webView ->
148168 webView.visibility = if (visible) View.VISIBLE else View.INVISIBLE
@@ -160,8 +180,18 @@ fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier
160180internal class CanvasA2UIActionBridge(
161181private val isTrustedPage: () -> Boolean,
162182private val onMessage: (String) -> Unit,
163-) {
164- @JavascriptInterface
183+) : WebViewCompat.WebMessageListener {
184+override fun onPostMessage(
185+view: WebView,
186+message: WebMessageCompat,
187+sourceOrigin: Uri,
188+isMainFrame: Boolean,
189+replyProxy: JavaScriptReplyProxy,
190+ ) {
191+if (!isMainFrame) return
192+ postMessage(message.data)
193+ }
194+165195fun postMessage(payload: String?) {
166196val msg = payload?.trim().orEmpty()
167197if (msg.isEmpty()) return
@@ -171,5 +201,6 @@ internal class CanvasA2UIActionBridge(
171201172202companion object {
173203const val interfaceName: String = "openclawCanvasA2UIAction"
204+val allowedOriginRules: Set<String> = setOf("*")
174205 }
175206}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。