惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
The Blog of Author Tim Ferriss
博客园 - 司徒正美
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
有赞技术团队
有赞技术团队
量子位
S
SegmentFault 最新的问题
博客园 - 聂微东
博客园 - 【当耐特】
J
Java Code Geeks
美团技术团队
Hugging Face - Blog
Hugging Face - Blog
H
Help Net Security
V
V2EX
人人都是产品经理
人人都是产品经理
博客园 - Franky
罗磊的独立博客
Engineering at Meta
Engineering at Meta
A
About on SuperTechFans
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
Apple Machine Learning Research
Apple Machine Learning Research

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(android): harden canvas webview bridge (#73240) · ope...
vincentkoc · 2026-04-28 · via Recent Commits to openclaw:main

@@ -1,10 +1,10 @@

11

package ai.openclaw.app.ui

2233

import android.annotation.SuppressLint

4+

import android.net.Uri

45

import android.util.Log

56

import android.view.View

67

import android.webkit.ConsoleMessage

7-

import android.webkit.JavascriptInterface

88

import android.webkit.WebChromeClient

99

import android.webkit.WebResourceError

1010

import android.webkit.WebResourceRequest

@@ -14,135 +14,155 @@ import android.webkit.WebView

1414

import android.webkit.WebViewClient

1515

import androidx.compose.runtime.Composable

1616

import androidx.compose.runtime.DisposableEffect

17-

import androidx.compose.runtime.mutableStateOf

1817

import androidx.compose.runtime.remember

1918

import androidx.compose.ui.Modifier

2019

import androidx.compose.ui.platform.LocalContext

2120

import androidx.compose.ui.viewinterop.AndroidView

21+

import androidx.webkit.JavaScriptReplyProxy

22+

import androidx.webkit.WebMessageCompat

2223

import androidx.webkit.WebSettingsCompat

24+

import androidx.webkit.WebViewCompat

2325

import androidx.webkit.WebViewFeature

2426

import ai.openclaw.app.MainViewModel

2527

import java.util.concurrent.atomic.AtomicReference

26282729

@SuppressLint("SetJavaScriptEnabled")

30+

@Suppress("DEPRECATION")

2831

@Composable

2932

fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier = Modifier) {

3033

val context = LocalContext.current

3134

val 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) }

3336

val currentPageUrlRef = remember { AtomicReference<String?>(null) }

34373538

DisposableEffect(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

}

45504651

AndroidView(

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

160180

internal class CanvasA2UIActionBridge(

161181

private val isTrustedPage: () -> Boolean,

162182

private 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+165195

fun postMessage(payload: String?) {

166196

val msg = payload?.trim().orEmpty()

167197

if (msg.isEmpty()) return

@@ -171,5 +201,6 @@ internal class CanvasA2UIActionBridge(

171201172202

companion object {

173203

const val interfaceName: String = "openclawCanvasA2UIAction"

204+

val allowedOriginRules: Set<String> = setOf("*")

174205

}

175206

}