Skip to content

Commit 74386e4

Browse files
committed
refactor(android): share UTF-16-safe truncation
1 parent 354b2d5 commit 74386e4

4 files changed

Lines changed: 12 additions & 24 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package ai.openclaw.app
2+
3+
internal fun String.takeUtf16Safe(maxChars: Int): String {
4+
if (length <= maxChars) return this
5+
// Keep the code-unit cap without leaving a high surrogate at its boundary.
6+
val endsOnHighSurrogate = maxChars > 0 && Character.isHighSurrogate(this[maxChars - 1])
7+
return take(if (endsOnHighSurrogate) maxChars - 1 else maxChars)
8+
}

apps/android/app/src/main/java/ai/openclaw/app/node/DeviceNotificationListenerService.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ai.openclaw.app.NotificationBurstLimiter
44
import ai.openclaw.app.SecurePrefs
55
import ai.openclaw.app.allowsPackage
66
import ai.openclaw.app.isWithinQuietHours
7+
import ai.openclaw.app.takeUtf16Safe
78
import android.app.Notification
89
import android.app.NotificationManager
910
import android.app.RemoteInput
@@ -30,17 +31,6 @@ internal fun sanitizeNotificationText(value: CharSequence?): String? {
3031
return normalized.takeUtf16Safe(MAX_NOTIFICATION_TEXT_CHARS).ifEmpty { null }
3132
}
3233

33-
private fun String.takeUtf16Safe(maxChars: Int): String {
34-
if (length <= maxChars) return this
35-
val end =
36-
if (maxChars > 0 && Character.isHighSurrogate(this[maxChars - 1])) {
37-
maxChars - 1
38-
} else {
39-
maxChars
40-
}
41-
return take(end)
42-
}
43-
4434
/**
4535
* Stable notification snapshot entry exposed through the Android notifications command.
4636
*/

apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatLinkPreview.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ai.openclaw.app.ui.chat
22

3+
import ai.openclaw.app.takeUtf16Safe
34
import android.graphics.Bitmap
45
import android.graphics.BitmapFactory
56
import android.util.LruCache
@@ -527,17 +528,6 @@ private fun sanitizeMetadataText(
527528
return sanitized.takeUtf16Safe(maxChars).takeIf(String::isNotEmpty)
528529
}
529530

530-
private fun String.takeUtf16Safe(maxChars: Int): String {
531-
if (length <= maxChars) return this
532-
val end =
533-
if (maxChars > 0 && Character.isHighSurrogate(this[maxChars - 1])) {
534-
maxChars - 1
535-
} else {
536-
maxChars
537-
}
538-
return take(end)
539-
}
540-
541531
private fun findTitle(html: String): String? =
542532
Regex("(?is)<title(?:\\s[^>]*)?>(.*?)</title\\s*>")
543533
.find(html)

apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatLinkPreviewTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class ChatLinkPreviewTest {
167167
@Test
168168
fun metadataTruncationPreservesUtf16Boundaries() {
169169
val titlePrefix = "t".repeat(LINK_PREVIEW_TITLE_MAX_CHARS - 1)
170-
val descriptionPrefix = "d".repeat(LINK_PREVIEW_DESCRIPTION_MAX_CHARS - 1)
170+
val descriptionPrefix = "d".repeat(LINK_PREVIEW_DESCRIPTION_MAX_CHARS - 2)
171171
val result =
172172
parseOpenGraph(
173173
"<meta property='og:title' content='$titlePrefix\uD83D\uDE80 trailing'>" +
@@ -176,7 +176,7 @@ class ChatLinkPreviewTest {
176176
) as LinkPreviewResult.Loaded
177177

178178
assertEquals(titlePrefix, result.metadata.title)
179-
assertEquals(descriptionPrefix, result.metadata.description)
179+
assertEquals("$descriptionPrefix\uD83D\uDE80", result.metadata.description)
180180
}
181181

182182
@Test

0 commit comments

Comments
 (0)