mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
co-authored by
Peter Steinberger
parent
618002c141
commit
a8fe7dd834
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user