fix(android): preserve emoji in debug diagnostics (#108823)

* fix(android): preserve unicode in debug diagnostics

* chore(android): refresh native i18n inventory

* test(android): satisfy debug handler ktlint

* refactor(android): centralize UTF-safe diagnostics

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Leon-SK668
2026-07-16 09:53:14 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent 618002c141
commit a8fe7dd834
7 changed files with 103 additions and 44 deletions
+35 -35
View File
@@ -1859,7 +1859,7 @@
},
{
"kind": "ui-call",
"line": 41,
"line": 42,
"path": "apps/android/app/src/main/java/ai/openclaw/app/node/DebugHandler.kt",
"source": "${signature.take(20)}... (OK)",
"surface": "android",
@@ -1867,7 +1867,7 @@
},
{
"kind": "ui-call",
"line": 41,
"line": 42,
"path": "apps/android/app/src/main/java/ai/openclaw/app/node/DebugHandler.kt",
"source": "NULL (FAILED)",
"surface": "android",
@@ -3499,7 +3499,7 @@
},
{
"kind": "ui-call",
"line": 77,
"line": 78,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Health",
"surface": "android",
@@ -3507,7 +3507,7 @@
},
{
"kind": "ui-call",
"line": 78,
"line": 79,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Gateway status, phone node readiness, and recent log stream.",
"surface": "android",
@@ -3515,7 +3515,7 @@
},
{
"kind": "ui-call",
"line": 85,
"line": 86,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Offline",
"surface": "android",
@@ -3523,7 +3523,7 @@
},
{
"kind": "ui-call",
"line": 86,
"line": 87,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Node",
"surface": "android",
@@ -3531,7 +3531,7 @@
},
{
"kind": "ui-call",
"line": 88,
"line": 89,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Logs",
"surface": "android",
@@ -3539,7 +3539,7 @@
},
{
"kind": "ui-call",
"line": 93,
"line": 94,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Online",
"surface": "android",
@@ -3547,7 +3547,7 @@
},
{
"kind": "ui-call",
"line": 93,
"line": 94,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Waiting",
"surface": "android",
@@ -3555,7 +3555,7 @@
},
{
"kind": "ui-call",
"line": 94,
"line": 95,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Needs connection",
"surface": "android",
@@ -3563,7 +3563,7 @@
},
{
"kind": "ui-call",
"line": 94,
"line": 95,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Ready",
"surface": "android",
@@ -3571,7 +3571,7 @@
},
{
"kind": "ui-call",
"line": 95,
"line": 96,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "${modelCount.size} available",
"surface": "android",
@@ -3579,7 +3579,7 @@
},
{
"kind": "ui-call",
"line": 97,
"line": 98,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "$pendingRunCount active",
"surface": "android",
@@ -3587,7 +3587,7 @@
},
{
"kind": "ui-call",
"line": 97,
"line": 98,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Idle",
"surface": "android",
@@ -3595,7 +3595,7 @@
},
{
"kind": "ui-call",
"line": 113,
"line": 114,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Refresh Logs",
"surface": "android",
@@ -3603,7 +3603,7 @@
},
{
"kind": "ui-call",
"line": 113,
"line": 114,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Refreshing",
"surface": "android",
@@ -3611,7 +3611,7 @@
},
{
"kind": "ui-call",
"line": 148,
"line": 149,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Log Entry",
"surface": "android",
@@ -3619,7 +3619,7 @@
},
{
"kind": "ui-call",
"line": 149,
"line": 150,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Readable gateway log detail.",
"surface": "android",
@@ -3627,7 +3627,7 @@
},
{
"kind": "ui-call",
"line": 156,
"line": 157,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Time",
"surface": "android",
@@ -3635,7 +3635,7 @@
},
{
"kind": "ui-call",
"line": 157,
"line": 158,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Level",
"surface": "android",
@@ -3643,7 +3643,7 @@
},
{
"kind": "ui-call",
"line": 158,
"line": 159,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Subsystem",
"surface": "android",
@@ -3651,7 +3651,7 @@
},
{
"kind": "ui-call",
"line": 158,
"line": 159,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Unknown",
"surface": "android",
@@ -3659,7 +3659,7 @@
},
{
"kind": "ui-call",
"line": 163,
"line": 164,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Message",
"surface": "android",
@@ -3667,7 +3667,7 @@
},
{
"kind": "ui-call",
"line": 169,
"line": 170,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Raw",
"surface": "android",
@@ -3675,7 +3675,7 @@
},
{
"kind": "ui-call",
"line": 196,
"line": 197,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Gateway",
"surface": "android",
@@ -3683,7 +3683,7 @@
},
{
"kind": "ui-call",
"line": 198,
"line": 199,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Phone Node",
"surface": "android",
@@ -3691,7 +3691,7 @@
},
{
"kind": "ui-call",
"line": 200,
"line": 201,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Chat",
"surface": "android",
@@ -3699,7 +3699,7 @@
},
{
"kind": "ui-call",
"line": 202,
"line": 203,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Models",
"surface": "android",
@@ -3707,7 +3707,7 @@
},
{
"kind": "ui-call",
"line": 204,
"line": 205,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Voice",
"surface": "android",
@@ -3715,7 +3715,7 @@
},
{
"kind": "ui-call",
"line": 206,
"line": 207,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Runs",
"surface": "android",
@@ -3723,7 +3723,7 @@
},
{
"kind": "ui-call",
"line": 219,
"line": 220,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "RECENT LOGS",
"surface": "android",
@@ -3731,7 +3731,7 @@
},
{
"kind": "ui-call",
"line": 227,
"line": 228,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Connect the gateway to load recent logs.",
"surface": "android",
@@ -3739,7 +3739,7 @@
},
{
"kind": "ui-call",
"line": 231,
"line": 232,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "No recent log entries.",
"surface": "android",
@@ -3747,7 +3747,7 @@
},
{
"kind": "ui-call",
"line": 247,
"line": 248,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Showing the latest log chunk.",
"surface": "android",
@@ -3755,7 +3755,7 @@
},
{
"kind": "ui-call",
"line": 261,
"line": 262,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/HealthLogsSettingsScreen.kt",
"source": "Open log entry",
"surface": "android",
@@ -6549,7 +6549,7 @@ class NodeRuntime private constructor(
sanitizeGatewayLogText(message)
.trim()
.replace(Regex("\\s+"), " ")
.take(240)
.takeUtf16Safe(240)
.ifEmpty { "Log entry" }
return GatewayLogEntry(
time = time,
@@ -3,6 +3,7 @@ package ai.openclaw.app.node
import ai.openclaw.app.BuildConfig
import ai.openclaw.app.CameraHudKind
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.takeUtf16Safe
import android.content.Context
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -13,6 +14,7 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
internal const val CAMERA_CLIP_MAX_RAW_BYTES: Long = 18L * 1024L * 1024L
private const val CAMERA_DEBUG_STACK_TRACE_MAX_CHARS = 2_000
/**
* Raw MP4 size guard before base64 encoding the clip into a node.invoke response.
@@ -84,7 +86,7 @@ class CameraHandler(
throw err
} catch (err: Throwable) {
camLog("inner error: ${err::class.java.simpleName}: ${err.message}")
camLog("stack: ${err.stackTraceToString().take(2000)}")
camLog("stack: ${err.stackTraceToString().takeUtf16Safe(CAMERA_DEBUG_STACK_TRACE_MAX_CHARS)}")
val (code, message) = invokeErrorFromThrowable(err)
showCameraHud(message, CameraHudKind.Error, 2200)
return GatewaySession.InvokeResult.error(code = code, message = message)
@@ -96,7 +98,7 @@ class CameraHandler(
throw err
} catch (err: Throwable) {
camLog("outer error: ${err::class.java.simpleName}: ${err.message}")
camLog("stack: ${err.stackTraceToString().take(2000)}")
camLog("stack: ${err.stackTraceToString().takeUtf16Safe(CAMERA_DEBUG_STACK_TRACE_MAX_CHARS)}")
return GatewaySession.InvokeResult.error(code = "UNAVAILABLE", message = err.message ?: "camera snap failed")
}
}
@@ -134,7 +136,7 @@ class CameraHandler(
throw err
} catch (err: Throwable) {
clipLog("inner error: ${err::class.java.simpleName}: ${err.message}")
clipLog("stack: ${err.stackTraceToString().take(2000)}")
clipLog("stack: ${err.stackTraceToString().takeUtf16Safe(CAMERA_DEBUG_STACK_TRACE_MAX_CHARS)}")
val (code, message) = invokeErrorFromThrowable(err)
showCameraHud(message, CameraHudKind.Error, 2400)
return GatewaySession.InvokeResult.error(code = code, message = message)
@@ -170,7 +172,7 @@ class CameraHandler(
throw err
} catch (err: Throwable) {
clipLog("outer error: ${err::class.java.simpleName}: ${err.message}")
clipLog("stack: ${err.stackTraceToString().take(2000)}")
clipLog("stack: ${err.stackTraceToString().takeUtf16Safe(CAMERA_DEBUG_STACK_TRACE_MAX_CHARS)}")
return GatewaySession.InvokeResult.error(code = "UNAVAILABLE", message = err.message ?: "camera clip failed")
} finally {
// Prevent talk/transcription capture from competing with camera audio after every exit path.
@@ -4,6 +4,7 @@ import ai.openclaw.app.BuildConfig
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.i18n.nativeString
import ai.openclaw.app.takeUtf16Safe
import android.content.Context
import kotlinx.serialization.json.JsonPrimitive
@@ -75,7 +76,7 @@ class DebugHandler(
} catch (e: Throwable) {
return GatewaySession.InvokeResult.error(
code = "ED25519_TEST_FAILED",
message = "${e.javaClass.simpleName}: ${e.message}\n${e.stackTraceToString().take(500)}",
message = "${e.javaClass.simpleName}: ${e.message}\n${e.stackTraceToString().takeUtf16Safe(500)}",
)
}
}
@@ -103,7 +104,7 @@ class DebugHandler(
if (!finished) proc.destroyForcibly()
val raw =
if (tmpFile.exists() && tmpFile.length() > 0) {
tmpFile.readText().take(128000)
tmpFile.readText().takeUtf16Safe(128_000)
} else {
"(no output, finished=$finished, exists=${tmpFile.exists()})"
}
@@ -147,7 +148,7 @@ class DebugHandler(
val camLogFile = java.io.File(appContext.cacheDir, "camera_debug.log")
val camLog =
if (camLogFile.exists() && camLogFile.length() > 0) {
"\n--- camera_debug.log ---\n" + camLogFile.readText().take(4000)
"\n--- camera_debug.log ---\n" + camLogFile.readText().takeUtf16Safe(4_000)
} else {
""
}
@@ -5,6 +5,7 @@ import ai.openclaw.app.GatewayLogEntry
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.VoiceCaptureMode
import ai.openclaw.app.i18n.nativeString
import ai.openclaw.app.takeUtf16Safe
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawStatus
@@ -168,7 +169,7 @@ private fun GatewayLogDetailSettingsScreen(
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = nativeString("Raw"), style = ClawTheme.type.section, color = ClawTheme.colors.text)
Text(
text = entry.raw.take(4_000),
text = entry.raw.takeUtf16Safe(4_000),
style = ClawTheme.type.caption,
color = ClawTheme.colors.textMuted,
)
@@ -0,0 +1,14 @@
package ai.openclaw.app
import org.junit.Assert.assertEquals
import org.junit.Test
class Utf16TextTest {
@Test
fun takeUtf16SafePreservesCodeUnitLimitWithoutSplittingSurrogatePairs() {
assertEquals("ab", "ab".takeUtf16Safe(2))
assertEquals("ab", "abc".takeUtf16Safe(2))
assertEquals("", "\uD83D\uDE00tail".takeUtf16Safe(1))
assertEquals("\uD83D\uDE00", "\uD83D\uDE00tail".takeUtf16Safe(2))
}
}
@@ -0,0 +1,41 @@
package ai.openclaw.app.node
import ai.openclaw.app.gateway.DeviceIdentityStore
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class DebugHandlerTest : NodeHandlerRobolectricTest() {
@Test
fun handleLogs_preservesUtf16BoundariesInCameraLog() {
val splitPairPrefix = "x".repeat(3_999)
assertEquals(splitPairPrefix, cameraLogFromResponse("${splitPairPrefix}\uD83D\uDE00tail"))
val completePairPrefix = "x".repeat(3_998)
assertEquals(
"${completePairPrefix}\uD83D\uDE00",
cameraLogFromResponse("${completePairPrefix}\uD83D\uDE00tail"),
)
}
private fun cameraLogFromResponse(raw: String): String {
val context = appContext()
File(context.cacheDir, "camera_debug.log").writeText(raw)
val result = DebugHandler(context, DeviceIdentityStore(context)).handleLogs()
assertTrue(result.ok)
val logs =
Json
.parseToJsonElement(result.payloadJson ?: error("missing payload"))
.jsonObject
.getValue("logs")
.jsonPrimitive
.content
return logs.substringAfter("\n--- camera_debug.log ---\n")
}
}