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