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 <steipete@gmail.com>
This commit is contained in:
Ben.Li
2026-07-10 03:58:18 +08:00
committed by GitHub
parent 7d5eba3106
commit 1d4c17073c
4 changed files with 26 additions and 12 deletions
@@ -0,0 +1,8 @@
package ai.openclaw.app
internal fun String.takeUtf16Safe(maxChars: Int): String {
if (length <= maxChars) return this
// Keep the code-unit cap without leaving a high surrogate at its boundary.
val endsOnHighSurrogate = maxChars > 0 && Character.isHighSurrogate(this[maxChars - 1])
return take(if (endsOnHighSurrogate) maxChars - 1 else maxChars)
}
@@ -4,6 +4,7 @@ import ai.openclaw.app.NotificationBurstLimiter
import ai.openclaw.app.SecurePrefs
import ai.openclaw.app.allowsPackage
import ai.openclaw.app.isWithinQuietHours
import ai.openclaw.app.takeUtf16Safe
import android.app.Notification
import android.app.NotificationManager
import android.app.RemoteInput
@@ -30,17 +31,6 @@ internal fun sanitizeNotificationText(value: CharSequence?): String? {
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)
}
/**
* Stable notification snapshot entry exposed through the Android notifications command.
*/
@@ -1,5 +1,6 @@
package ai.openclaw.app.ui.chat
import ai.openclaw.app.takeUtf16Safe
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.LruCache
@@ -524,7 +525,7 @@ private fun sanitizeMetadataText(
.filterNot(Character::isISOControl)
.replace(Regex("\\s+"), " ")
.trim()
return sanitized.take(maxChars).takeIf(String::isNotEmpty)
return sanitized.takeUtf16Safe(maxChars).takeIf(String::isNotEmpty)
}
private fun findTitle(html: String): String? =
@@ -164,6 +164,21 @@ class ChatLinkPreviewTest {
)
}
@Test
fun metadataTruncationPreservesUtf16Boundaries() {
val titlePrefix = "t".repeat(LINK_PREVIEW_TITLE_MAX_CHARS - 1)
val descriptionPrefix = "d".repeat(LINK_PREVIEW_DESCRIPTION_MAX_CHARS - 2)
val result =
parseOpenGraph(
"<meta property='og:title' content='$titlePrefix\uD83D\uDE80 trailing'>" +
"<meta property='og:description' content='$descriptionPrefix\uD83D\uDE80 trailing'>",
"https://example.com",
) as LinkPreviewResult.Loaded
assertEquals(titlePrefix, result.metadata.title)
assertEquals("$descriptionPrefix\uD83D\uDE80", result.metadata.description)
}
@Test
fun fetchesHtmlWithoutAmbientHeaders() =
withServer { server ->