From 3e787f31878553fa09f96d5d7a380e822bb5b782 Mon Sep 17 00:00:00 2001
From: "Ben.Li"
Date: Thu, 9 Jul 2026 15:11:32 +0800
Subject: [PATCH] fix(android): preserve UTF-16 boundaries in notification text
(#102442)
* fix(android): keep notification text UTF-16 safe
* test(android): cover UTF-16 notification boundaries
---------
Co-authored-by: Peter Steinberger
Co-authored-by: Peter Steinberger
---
.../app/node/DeviceNotificationListenerService.kt | 13 ++++++++++++-
.../openclaw/app/node/NotificationsHandlerTest.kt | 12 ++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceNotificationListenerService.kt b/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceNotificationListenerService.kt
index bacb99f81a0..ebdcfd9f8cc 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceNotificationListenerService.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceNotificationListenerService.kt
@@ -27,7 +27,18 @@ private const val NOTIFICATIONS_CHANGED_EVENT = "notifications.changed"
internal fun sanitizeNotificationText(value: CharSequence?): String? {
val normalized = value?.toString()?.trim().orEmpty()
// Notification extras can include long previews; cap before sending over node events.
- return normalized.take(MAX_NOTIFICATION_TEXT_CHARS).ifEmpty { null }
+ return normalized.takeUtf16Safe(MAX_NOTIFICATION_TEXT_CHARS).ifEmpty { null }
+}
+
+private fun String.takeUtf16Safe(maxChars: Int): String {
+ if (length <= maxChars) return this
+ val end =
+ if (maxChars > 0 && Character.isHighSurrogate(this[maxChars - 1])) {
+ maxChars - 1
+ } else {
+ maxChars
+ }
+ return take(end)
}
/**
diff --git a/apps/android/app/src/test/java/ai/openclaw/app/node/NotificationsHandlerTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/node/NotificationsHandlerTest.kt
index be5e611a059..f083ed1fc7e 100644
--- a/apps/android/app/src/test/java/ai/openclaw/app/node/NotificationsHandlerTest.kt
+++ b/apps/android/app/src/test/java/ai/openclaw/app/node/NotificationsHandlerTest.kt
@@ -265,6 +265,18 @@ class NotificationsHandlerTest {
assertTrue((sanitized ?: "").all { it == 'x' })
}
+ @Test
+ fun sanitizeNotificationTextPreservesUtf16BoundariesAtLimit() {
+ val splitPairPrefix = "a".repeat(511)
+ assertEquals(splitPairPrefix, sanitizeNotificationText("$splitPairPrefix🚀 trailing text"))
+
+ val completePairPrefix = "a".repeat(510)
+ assertEquals(
+ "$completePairPrefix🚀",
+ sanitizeNotificationText("$completePairPrefix🚀 trailing text"),
+ )
+ }
+
@Test
fun notificationsActionClearablePolicy_onlyRequiresClearableForDismiss() {
assertTrue(actionRequiresClearableNotification(NotificationActionKind.Dismiss))