Skip to content

Commit 1d4c170

Browse files
ly85206559steipete
andauthored
fix(android): keep link preview metadata UTF-16 safe (#102988)
* fix(android): keep link preview metadata UTF-16 safe * refactor(android): share UTF-16-safe truncation --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 7d5eba3 commit 1d4c170

4 files changed

Lines changed: 26 additions & 12 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: 2 additions & 1 deletion
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
@@ -524,7 +525,7 @@ private fun sanitizeMetadataText(
524525
.filterNot(Character::isISOControl)
525526
.replace(Regex("\\s+"), " ")
526527
.trim()
527-
return sanitized.take(maxChars).takeIf(String::isNotEmpty)
528+
return sanitized.takeUtf16Safe(maxChars).takeIf(String::isNotEmpty)
528529
}
529530

530531
private fun findTitle(html: String): String? =

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@ class ChatLinkPreviewTest {
164164
)
165165
}
166166

167+
@Test
168+
fun metadataTruncationPreservesUtf16Boundaries() {
169+
val titlePrefix = "t".repeat(LINK_PREVIEW_TITLE_MAX_CHARS - 1)
170+
val descriptionPrefix = "d".repeat(LINK_PREVIEW_DESCRIPTION_MAX_CHARS - 2)
171+
val result =
172+
parseOpenGraph(
173+
"<meta property='og:title' content='$titlePrefix\uD83D\uDE80 trailing'>" +
174+
"<meta property='og:description' content='$descriptionPrefix\uD83D\uDE80 trailing'>",
175+
"https://example.com",
176+
) as LinkPreviewResult.Loaded
177+
178+
assertEquals(titlePrefix, result.metadata.title)
179+
assertEquals("$descriptionPrefix\uD83D\uDE80", result.metadata.description)
180+
}
181+
167182
@Test
168183
fun fetchesHtmlWithoutAmbientHeaders() =
169184
withServer { server ->

0 commit comments

Comments
 (0)