Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -754,16 +754,22 @@ internal class SelectionAwareEditText(context: Context) : EditText(context) {
return shouldIntercept
}

// coerceToText opens content: URIs synchronously. Leave URI-backed
// clipboard items to Android's normal paste path so the UI thread never
// reads an arbitrary provider just to measure a text paste.
private fun ClipData.plainText(): String? =
takeIf { itemCount > 0 }
?.getItemAt(0)
?.takeIf { it.uri == null }
?.coerceToText(context)
?.toString()
?.takeIf(String::isNotEmpty)
// Match native paste's newlines, including empty items.
// URI-backed clips stay native: coerceToText can read a provider synchronously.
private fun ClipData.plainText(): String? {
var joined: StringBuilder? = null
for (index in 0 until itemCount) {
val item = getItemAt(index)
if (item.uri != null) return null
val itemText = item.coerceToText(context) ?: continue
if (joined == null) {
joined = StringBuilder(itemText)
} else {
joined.append('\n').append(itemText)
}
}
return joined?.toString()?.takeIf(String::isNotEmpty)
}

override fun onKeyShortcut(keyCode: Int, event: KeyEvent): Boolean {
if (keyCode == KeyEvent.KEYCODE_V && event.isCtrlPressed && event.isShiftPressed) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package expo.modules.t3composereditor

import android.content.ClipData
import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.net.Uri
import android.view.KeyEvent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
Expand Down Expand Up @@ -91,6 +93,53 @@ class ComposerPasteTest {
assertEquals("old", editor.text.toString())
}

@Test
fun regularPasteMeasuresAndInterceptsEveryTextItemJoinedWithNewlines() {
// Only the complete UTF-8 payload, including both newlines, reaches 32 KiB.
val first = "é".repeat(8191)
val last = "x".repeat(16 * 1024)
val clip = ClipData.newPlainText("test", first).apply {
addItem(ClipData.Item(""))
addItem(ClipData.Item(last))
}
clipboard.setPrimaryClip(clip)
editor.setText("before old after")
editor.setSelection(10, 7)
val intercepted = mutableListOf<Triple<String, Int, Int>>()
editor.pasteTextListener = { text, start, end -> intercepted.add(Triple(text, start, end)) }

assertTrue(editor.onTextContextMenuItem(android.R.id.paste))
assertEquals(listOf(Triple("$first\n\n$last", 7, 10)), intercepted)
assertEquals("before old after", editor.text.toString())
assertEquals(10, editor.selectionStart)
assertEquals(7, editor.selectionEnd)
}

@Test
fun regularPasteKeepsClipsWithAnyUriItemNative() {
editor.textPasteThresholdBytes = 1
editor.pasteTextListener = { _, _, _ -> error("URI-backed clips must stay native") }
for (uriIndex in 0..2) {
val items = (0..2).map { index ->
if (index == uriIndex) {
ClipData.Item("ITEM_$index", null, Uri.parse("content://test/item/$index"))
} else {
ClipData.Item("ITEM_$index")
}
}
val clip = ClipData("test", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN), items[0]).apply {
addItem(items[1])
addItem(items[2])
}
clipboard.setPrimaryClip(clip)
editor.setText("before old after")
editor.setSelection(10, 7)

assertTrue(editor.onTextContextMenuItem(android.R.id.paste))
assertEquals("before ITEM_0\nITEM_1\nITEM_2 after", editor.text.toString())
}
}

@Test
fun shortcutPastesStructuredClipboardAsText() {
clipboard.setPrimaryClip(
Expand Down
Loading