Skip to content

Commit 7c17677

Browse files
authored
Fix invalid selection/span ranges causing editor crashes #1112
2 parents 5a21fb5 + fceae44 commit 7c17677

File tree

4 files changed

+82
-14
lines changed

4 files changed

+82
-14
lines changed

aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1588,6 +1588,31 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
15881588

15891589
// Helper ======================================================================================
15901590

1591+
/**
1592+
* Some Android versions/framework builds are very strict about selection offsets and will crash
1593+
* when selection handles try to position at an invalid cursor offset (e.g. -1 or > text length).
1594+
*
1595+
* Aztec can temporarily compute such offsets while mutating text (indent/outdent, span re-application, etc),
1596+
* so we clamp selection to a valid range here to avoid framework crashes.
1597+
*
1598+
* NOTE: We clamp to [0, safeLength] to avoid placing selection on the end-of-buffer marker.
1599+
*/
1600+
override fun setSelection(index: Int) {
1601+
val max = EndOfBufferMarkerAdder.safeLength(this)
1602+
super.setSelection(index.coerceIn(0, max))
1603+
}
1604+
1605+
override fun setSelection(start: Int, stop: Int) {
1606+
val max = EndOfBufferMarkerAdder.safeLength(this)
1607+
val s = start.coerceIn(0, max)
1608+
val e = stop.coerceIn(0, max)
1609+
if (s <= e) {
1610+
super.setSelection(s, e)
1611+
} else {
1612+
super.setSelection(e, s)
1613+
}
1614+
}
1615+
15911616
fun consumeCursorPosition(text: SpannableStringBuilder): Int {
15921617
var cursorPosition = Math.min(selectionStart, text.length)
15931618

aztec/src/main/kotlin/org/wordpress/aztec/formatting/LineBlockFormatter.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,22 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
219219
this.onEach { editableText.removeSpan(it.span) }
220220
action()
221221
this.onEach {
222-
editableText.setSpan(it.span, it.spanStart, it.spanEnd, it.spanFlags)
222+
val len = editableText.length
223+
if (len <= 0) return@onEach
224+
225+
val start = it.spanStart.coerceIn(0, len)
226+
val end = it.spanEnd.coerceIn(0, len)
227+
if (start >= end) return@onEach
228+
229+
try {
230+
editableText.setSpan(it.span, start, end, it.spanFlags)
231+
} catch (_: IndexOutOfBoundsException) {
232+
// Defensive: text may have been modified by other watchers by the time we restore spans.
233+
} catch (_: IllegalArgumentException) {
234+
// Defensive: some framework builds throw IllegalArgumentException on invalid ranges.
235+
} catch (_: RuntimeException) {
236+
// Defensive: SpannableStringBuilder.setSpan can throw RuntimeException for invalid ranges.
237+
}
223238
}
224239
}
225240

aztec/src/main/kotlin/org/wordpress/aztec/watchers/SuggestionWatcher.kt

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,26 @@ class SuggestionWatcher(aztecText: AztecText) : TextWatcher {
142142

143143
private fun reapplyCarriedOverInlineSpans(editableText: Spannable) {
144144
carryOverSpans.forEach {
145-
if (it.start >= 0 && it.end <= editableText.length && it.start < it.end) {
146-
try {
147-
editableText.setSpan(
148-
it.span,
149-
it.start,
150-
it.end,
151-
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
152-
)
153-
} catch (e: IndexOutOfBoundsException) {
154-
// This is a workaround for a possible bug in the Android framework
155-
// https://github.com/wordpress-mobile/WordPress-Android/issues/20481
156-
e.printStackTrace()
157-
}
145+
val len = editableText.length
146+
if (len <= 0) return@forEach
147+
148+
val start = it.start.coerceIn(0, len)
149+
val end = it.end.coerceIn(0, len)
150+
if (start >= end) return@forEach
151+
152+
try {
153+
editableText.setSpan(
154+
it.span,
155+
start,
156+
end,
157+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
158+
)
159+
} catch (_: IndexOutOfBoundsException) {
160+
// Defensive: spans can become invalid if the framework modifies text concurrently.
161+
} catch (_: IllegalArgumentException) {
162+
// Defensive: newer framework builds can throw on edge-case span/selection interactions.
163+
} catch (_: RuntimeException) {
164+
// Defensive: SpannableStringBuilder.setSpan can throw RuntimeException for invalid ranges.
158165
}
159166
}
160167
}

aztec/src/test/kotlin/org/wordpress/aztec/IndentTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,27 @@ class IndentTest {
248248
Assert.assertEquals("123<br>456", editText.toHtml())
249249
}
250250

251+
@Test
252+
@Throws(Exception::class)
253+
fun testOutdentAtStartOfDocumentDoesNotCrashAndClampsSelection() {
254+
// Regression test:
255+
// Outdenting when the caret is at position 0 and the first character is a tab used to compute
256+
// selection = -1 and crash inside EditText.setSelection (setSpan(-1...-1)).
257+
editText.fromHtml("\t123")
258+
259+
// Place caret at the very beginning (this matches the crash repro from the app).
260+
editText.setSelection(0)
261+
Assert.assertTrue(editText.isOutdentAvailable())
262+
263+
// Should not throw.
264+
editText.outdent()
265+
266+
// Verify text was outdented and selection remained valid.
267+
Assert.assertEquals("123", editText.toHtml())
268+
Assert.assertEquals(0, editText.selectionStart)
269+
Assert.assertEquals(0, editText.selectionEnd)
270+
}
271+
251272
@Test
252273
@Throws(Exception::class)
253274
fun doesNotIndentMedia() {

0 commit comments

Comments
 (0)