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