Skip to content

Commit 209fac7

Browse files
authored
Merge pull request #372 from amirghm/feature/add-console-bridge-for-android
Feat: Add Console bridge for android
2 parents d72bf2b + c1a2410 commit 209fac7

14 files changed

Lines changed: 163 additions & 8 deletions

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ android.minSdk=21
1515
#Versions
1616
GROUP=io.github.kevinnzou
1717
POM_ARTIFACT_ID=compose-webview-multiplatform
18-
VERSION_NAME=2.0.3
18+
VERSION_NAME=2.0.4
1919
POM_NAME=Compose WebView Multiplatform
2020
POM_INCEPTION_YEAR=2023
2121
POM_DESCRIPTION=WebView for JetBrains Compose Multiplatform

webview/src/androidMain/kotlin/com/multiplatform/webview/util/InternalStoragePathHandler.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package com.multiplatform.webview.util
22

3-
import android.util.Log
43
import android.webkit.WebResourceResponse
54
import androidx.webkit.WebViewAssetLoader
65
import java.io.File
76
import java.io.FileInputStream
87

98
class InternalStoragePathHandler : WebViewAssetLoader.PathHandler {
109
override fun handle(path: String): WebResourceResponse? {
11-
Log.d("InternalStorageHandler", "Intercepted: $path")
1210
val file = File(path.removePrefix("/"))
1311
if (!file.exists() || !file.isFile) return null
1412

webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import android.content.pm.PackageManager
55
import android.content.res.Configuration
66
import android.graphics.Bitmap
77
import android.os.Build
8+
import android.view.View
89
import android.view.ViewGroup
10+
import android.webkit.ConsoleMessage
911
import android.webkit.PermissionRequest
1012
import android.webkit.WebChromeClient
1113
import android.webkit.WebResourceError
@@ -27,9 +29,11 @@ import androidx.core.graphics.createBitmap
2729
import androidx.webkit.WebSettingsCompat
2830
import androidx.webkit.WebViewAssetLoader
2931
import androidx.webkit.WebViewFeature
32+
import com.multiplatform.webview.jsbridge.ConsoleBridge
3033
import com.multiplatform.webview.jsbridge.WebViewJsBridge
3134
import com.multiplatform.webview.request.WebRequest
3235
import com.multiplatform.webview.request.WebRequestInterceptResult
36+
import com.multiplatform.webview.setting.PlatformWebSettings
3337
import com.multiplatform.webview.util.InternalStoragePathHandler
3438
import com.multiplatform.webview.util.KLogger
3539

@@ -69,6 +73,7 @@ fun AccompanistWebView(
6973
captureBackPresses: Boolean = true,
7074
navigator: WebViewNavigator = rememberWebViewNavigator(),
7175
webViewJsBridge: WebViewJsBridge? = null,
76+
consoleBridge: ConsoleBridge? = null,
7277
onCreated: (WebView) -> Unit = {},
7378
onDispose: (WebView) -> Unit = {},
7479
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
@@ -105,6 +110,7 @@ fun AccompanistWebView(
105110
captureBackPresses,
106111
navigator,
107112
webViewJsBridge,
113+
consoleBridge,
108114
onCreated,
109115
onDispose,
110116
client,
@@ -147,6 +153,7 @@ fun AccompanistWebView(
147153
captureBackPresses: Boolean = true,
148154
navigator: WebViewNavigator = rememberWebViewNavigator(),
149155
webViewJsBridge: WebViewJsBridge? = null,
156+
consoleBridge: ConsoleBridge? = null,
150157
onCreated: (WebView) -> Unit = {},
151158
onDispose: (WebView) -> Unit = {},
152159
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
@@ -183,8 +190,15 @@ fun AccompanistWebView(
183190
webChromeClient = chromeClient
184191
webViewClient = client
185192

186-
// Avoid covering other components
187-
this.setLayerType(state.webSettings.androidWebSettings.layerType, null)
193+
// Avoid covering other components - map to Android View constants explicitly
194+
val desiredLayerType =
195+
when (state.webSettings.androidWebSettings.layerType) {
196+
PlatformWebSettings.AndroidWebSettings.LayerType.NONE -> View.LAYER_TYPE_NONE
197+
PlatformWebSettings.AndroidWebSettings.LayerType.SOFTWARE -> View.LAYER_TYPE_SOFTWARE
198+
PlatformWebSettings.AndroidWebSettings.LayerType.HARDWARE -> View.LAYER_TYPE_HARDWARE
199+
else -> View.LAYER_TYPE_HARDWARE
200+
}
201+
this.setLayerType(desiredLayerType, null)
188202

189203
settings.apply {
190204
state.webSettings.let {
@@ -245,7 +259,7 @@ fun AccompanistWebView(
245259
}
246260
}
247261
}.also {
248-
val androidWebView = AndroidWebView(it, scope, webViewJsBridge)
262+
val androidWebView = AndroidWebView(it, scope, webViewJsBridge, consoleBridge)
249263
state.webView = androidWebView
250264
webViewJsBridge?.webView = androidWebView
251265
}
@@ -295,6 +309,15 @@ open class AccompanistWebViewClient : WebViewClient() {
295309
val script =
296310
"var meta = document.createElement('meta');meta.setAttribute('name', 'viewport');meta.setAttribute('content', 'width=device-width, initial-scale=${state.webSettings.zoomLevel}, maximum-scale=10.0, minimum-scale=0.1,user-scalable=$supportZoom');document.getElementsByTagName('head')[0].appendChild(meta);"
297311
navigator.evaluateJavaScript(script)
312+
313+
// Remove tap highlight color
314+
val removeHighlightScript =
315+
"""
316+
var style = document.createElement('style');
317+
style.innerHTML = '* { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; -webkit-user-select: none; }';
318+
document.head.appendChild(style);
319+
""".trimIndent()
320+
navigator.evaluateJavaScript(removeHighlightScript)
298321
}
299322

300323
override fun shouldInterceptRequest(
@@ -426,6 +449,41 @@ open class AccompanistWebChromeClient : WebChromeClient() {
426449
internal set
427450
private var lastLoadedUrl = ""
428451

452+
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
453+
try {
454+
val msg = consoleMessage ?: return super.onConsoleMessage(consoleMessage)
455+
val level =
456+
when (msg.messageLevel()) {
457+
ConsoleMessage.MessageLevel.ERROR -> "error"
458+
ConsoleMessage.MessageLevel.WARNING -> "warn"
459+
ConsoleMessage.MessageLevel.LOG -> "log"
460+
ConsoleMessage.MessageLevel.TIP -> "info"
461+
ConsoleMessage.MessageLevel.DEBUG -> "debug"
462+
else -> "log"
463+
}
464+
val timestamp =
465+
try {
466+
val sdf =
467+
java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US)
468+
sdf.timeZone = java.util.TimeZone.getTimeZone("UTC")
469+
sdf.format(java.util.Date())
470+
} catch (_: Throwable) {
471+
System.currentTimeMillis().toString()
472+
}
473+
val iwv = state.webView
474+
val bridge = iwv?.consoleBridge
475+
bridge?.emitFromPlatform(
476+
level = level,
477+
content = msg.message(),
478+
sourceId = msg.sourceId(),
479+
lineNumber = msg.lineNumber(),
480+
timestamp = timestamp,
481+
)
482+
} catch (_: Exception) {
483+
}
484+
return super.onConsoleMessage(consoleMessage)
485+
}
486+
429487
override fun onReceivedTitle(
430488
view: WebView,
431489
title: String?,

webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.multiplatform.webview.web
22

33
import android.webkit.JavascriptInterface
44
import android.webkit.WebView
5+
import com.multiplatform.webview.jsbridge.ConsoleBridge
56
import com.multiplatform.webview.jsbridge.JsMessage
67
import com.multiplatform.webview.jsbridge.WebViewJsBridge
78
import com.multiplatform.webview.util.KLogger
@@ -21,6 +22,7 @@ class AndroidWebView(
2122
override val webView: WebView,
2223
override val scope: CoroutineScope,
2324
override val webViewJsBridge: WebViewJsBridge?,
25+
override val consoleBridge: ConsoleBridge?,
2426
) : IWebView {
2527
init {
2628
initWebView()

webview/src/androidMain/kotlin/com/multiplatform/webview/web/WebView.android.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
55
import androidx.compose.runtime.Immutable
66
import androidx.compose.runtime.remember
77
import androidx.compose.ui.Modifier
8+
import com.multiplatform.webview.jsbridge.ConsoleBridge
89
import com.multiplatform.webview.jsbridge.WebViewJsBridge
910

1011
/**
@@ -17,6 +18,7 @@ actual fun ActualWebView(
1718
captureBackPresses: Boolean,
1819
navigator: WebViewNavigator,
1920
webViewJsBridge: WebViewJsBridge?,
21+
consoleBridge: ConsoleBridge?,
2022
onCreated: (NativeWebView) -> Unit,
2123
onDispose: (NativeWebView) -> Unit,
2224
platformWebViewParams: PlatformWebViewParams?,
@@ -28,6 +30,7 @@ actual fun ActualWebView(
2830
captureBackPresses,
2931
navigator,
3032
webViewJsBridge,
33+
consoleBridge,
3134
onCreated = onCreated,
3235
onDispose = onDispose,
3336
client = platformWebViewParams?.client ?: remember { AccompanistWebViewClient() },
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.multiplatform.webview.jsbridge
2+
3+
import kotlinx.serialization.json.Json
4+
import kotlinx.serialization.json.JsonObject
5+
import kotlinx.serialization.json.buildJsonObject
6+
import kotlinx.serialization.json.put
7+
8+
/**
9+
* Bridge for capturing platform WebView console logs and forwarding them to
10+
* the existing JS bridge as a JSON payload matching ConsoleLogMessage structure.
11+
*/
12+
class ConsoleBridge(
13+
/**
14+
* Callback invoked with a JSON string matching ConsoleLogMessage schema.
15+
*/
16+
var onLog: ((String) -> Unit)? = null,
17+
) {
18+
/**
19+
* Emit a console log event coming from the platform WebView.
20+
* This will be routed through the JS bridge using method "consoleLog".
21+
*/
22+
fun emitFromPlatform(
23+
level: String,
24+
content: String,
25+
sourceId: String?,
26+
lineNumber: Int,
27+
timestamp: String,
28+
) {
29+
val normalizedLevel = level.lowercase()
30+
val type =
31+
when (normalizedLevel) {
32+
"error" -> "error"
33+
"exception" -> "exception"
34+
else -> "normal"
35+
}
36+
val cause =
37+
when (normalizedLevel) {
38+
"warn", "warning" -> "warning"
39+
"error" -> "error"
40+
"debug" -> "debug"
41+
else -> "user_code"
42+
}
43+
val emoji =
44+
when (normalizedLevel) {
45+
"error" -> ""
46+
"warn", "warning" -> "⚠️"
47+
"debug" -> "🔍"
48+
"info" -> "ℹ️"
49+
else -> "📝"
50+
}
51+
val filePath = sourceId ?: ""
52+
val fileName = filePath.substringAfterLast('/').substringAfterLast('\\')
53+
54+
val json: JsonObject =
55+
buildJsonObject {
56+
put("emoji", emoji)
57+
put("type", type)
58+
put("level", normalizedLevel)
59+
put("content", content)
60+
put("cause", cause)
61+
put("lineNumber", lineNumber)
62+
put("fileName", fileName)
63+
put("filePath", filePath)
64+
put("timestamp", timestamp)
65+
}
66+
67+
val payload = Json.encodeToString(JsonObject.serializer(), json)
68+
69+
// Emit directly to any listener without using WebViewJsBridge
70+
onLog?.invoke(payload)
71+
}
72+
}

webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.multiplatform.webview.web
22

3+
import com.multiplatform.webview.jsbridge.ConsoleBridge
34
import com.multiplatform.webview.jsbridge.WebViewJsBridge
45
import com.multiplatform.webview.util.KLogger
56
import compose_webview_multiplatform.webview.generated.resources.Res
@@ -26,6 +27,8 @@ interface IWebView {
2627

2728
val webViewJsBridge: WebViewJsBridge?
2829

30+
val consoleBridge: ConsoleBridge?
31+
2932
/**
3033
* True when the web view is able to navigate backwards, false otherwise.
3134
*/
@@ -187,13 +190,11 @@ interface IWebView {
187190
};
188191
if (callback) {
189192
window.$jsBridgeName.callbacks[message.callbackId] = callback;
190-
console.log('add callback: ' + message.callbackId + ', ' + callback);
191193
}
192194
window.$jsBridgeName.postMessage(JSON.stringify(message));
193195
},
194196
onCallback: function (callbackId, data) {
195197
var callback = window.$jsBridgeName.callbacks[callbackId];
196-
console.log('onCallback: ' + callbackId + ', ' + data + ', ' + callback);
197198
if (callback) {
198199
callback(data);
199200
delete window.$jsBridgeName.callbacks[callbackId];

webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebView.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.compose.runtime.DisposableEffect
55
import androidx.compose.runtime.LaunchedEffect
66
import androidx.compose.runtime.snapshotFlow
77
import androidx.compose.ui.Modifier
8+
import com.multiplatform.webview.jsbridge.ConsoleBridge
89
import com.multiplatform.webview.jsbridge.WebViewJsBridge
910
import kotlinx.coroutines.flow.filter
1011
import kotlinx.coroutines.flow.merge
@@ -35,6 +36,7 @@ fun WebView(
3536
captureBackPresses: Boolean = true,
3637
navigator: WebViewNavigator = rememberWebViewNavigator(),
3738
webViewJsBridge: WebViewJsBridge? = null,
39+
consoleBridge: ConsoleBridge? = null,
3840
onCreated: () -> Unit = {},
3941
onDispose: () -> Unit = {},
4042
platformWebViewParams: PlatformWebViewParams? = null,
@@ -45,6 +47,7 @@ fun WebView(
4547
captureBackPresses = captureBackPresses,
4648
navigator = navigator,
4749
webViewJsBridge = webViewJsBridge,
50+
consoleBridge = consoleBridge,
4851
onCreated = { _ -> onCreated() },
4952
onDispose = { _ -> onDispose() },
5053
platformWebViewParams = platformWebViewParams,
@@ -72,6 +75,7 @@ fun WebView(
7275
captureBackPresses: Boolean = true,
7376
navigator: WebViewNavigator = rememberWebViewNavigator(),
7477
webViewJsBridge: WebViewJsBridge? = null,
78+
consoleBridge: ConsoleBridge? = null,
7579
onCreated: (NativeWebView) -> Unit = {},
7680
onDispose: (NativeWebView) -> Unit = {},
7781
platformWebViewParams: PlatformWebViewParams? = null,
@@ -118,6 +122,7 @@ fun WebView(
118122
captureBackPresses = captureBackPresses,
119123
navigator = navigator,
120124
webViewJsBridge = webViewJsBridge,
125+
consoleBridge = consoleBridge,
121126
onCreated = onCreated,
122127
onDispose = onDispose,
123128
platformWebViewParams = platformWebViewParams,
@@ -167,6 +172,7 @@ expect fun ActualWebView(
167172
captureBackPresses: Boolean = true,
168173
navigator: WebViewNavigator = rememberWebViewNavigator(),
169174
webViewJsBridge: WebViewJsBridge? = null,
175+
consoleBridge: ConsoleBridge? = null,
170176
onCreated: (NativeWebView) -> Unit = {},
171177
onDispose: (NativeWebView) -> Unit = {},
172178
platformWebViewParams: PlatformWebViewParams? = null,

webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.multiplatform.webview.web
22

3+
import com.multiplatform.webview.jsbridge.ConsoleBridge
34
import com.multiplatform.webview.jsbridge.JsMessage
45
import com.multiplatform.webview.jsbridge.WebViewJsBridge
56
import com.multiplatform.webview.util.KLogger
@@ -359,6 +360,8 @@ class DesktopWebView(
359360
return modifiedHtml
360361
}
361362

363+
override val consoleBridge: ConsoleBridge? = null
364+
362365
override fun saveState(): WebViewBundle? = null
363366

364367
override fun scrollOffset(): Pair<Int, Int> = Pair(0, 0)

webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.multiplatform.webview.web
33
import androidx.compose.runtime.*
44
import androidx.compose.ui.Modifier
55
import androidx.compose.ui.awt.SwingPanel
6+
import com.multiplatform.webview.jsbridge.ConsoleBridge
67
import com.multiplatform.webview.jsbridge.WebViewJsBridge
78
import dev.datlag.kcef.KCEF
89
import dev.datlag.kcef.KCEFBrowser
@@ -21,6 +22,7 @@ actual fun ActualWebView(
2122
captureBackPresses: Boolean,
2223
navigator: WebViewNavigator,
2324
webViewJsBridge: WebViewJsBridge?,
25+
consoleBridge: ConsoleBridge?,
2426
onCreated: (NativeWebView) -> Unit,
2527
onDispose: (NativeWebView) -> Unit,
2628
platformWebViewParams: PlatformWebViewParams?,

0 commit comments

Comments
 (0)