Skip to content

Conversation

@Twinsen81
Copy link
Contributor

@Twinsen81 Twinsen81 commented Dec 30, 2025

DODROID-621

Prevents crashes caused by invalid selection offsets and invalid span ranges during text mutations (e.g., outdent/indent, span restoration), which can surface as SpannableStringBuilder/selection handle failures on newer Android builds.

The crash recording:
https://github.com/user-attachments/assets/f32a42c5-fdf2-4db3-a55f-19c0b6494587

Changes:

  • Clamp AztecText.setSelection(...) to a valid range (also avoiding the end-of-buffer marker).
  • Guard/clamp span restoration ranges in SuggestionWatcher and LineBlockFormatter to avoid setSpan crashes on edge cases.
  • Add a regression test reproducing the “outdent at start of document” crash.

Testing Steps

  • Run the demo app
  • Long-click -> Select all -> Cut
  • Ensure the cursor is at the first line at position 0
  • Tap the Indent button on the toolbar
  • Enter some text
  • Move the cursor back to position 0
  • Tap the Outdent button
  • Verify the app doesn't crash
  • Smoke-test the editor in the demo app, especially the indentation

The crashes

Exception java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0
  at android.text.SpannableStringBuilder.checkRange (SpannableStringBuilder.java:1341)
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:695)
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:687)
  at androidx.emoji2.text.SpannableBuilder.setSpan (SpannableBuilder.java:135)
  at android.text.Selection.setSelection (Selection.java:94)
  at android.text.Selection.setSelection (Selection.java:78)
  at android.widget.EditText.setSelection (EditText.java:147)
  at org.wordpress.aztec.formatting.IndentFormatter.outdent (IndentFormatter.kt:111)
  at org.wordpress.aztec.formatting.BlockFormatter.outdent (BlockFormatter.kt:80)
  at org.wordpress.aztec.AztecText.outdent (AztecText.kt:1217)
  at org.wordpress.aztec.toolbar.AztecToolbar.onToolbarAction (AztecToolbar.kt:659)
  at org.wordpress.aztec.toolbar.AztecToolbar.setupToolbarItems$lambda$18$lambda$17 (AztecToolbar.kt:799)
Exception java.lang.RuntimeException:
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:694)
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:677)
  at androidx.emoji2.text.SpannableBuilder.setSpan (SpannableBuilder.java:135)
  at org.wordpress.aztec.formatting.LineBlockFormatter.applyWithRemovedSpans (LineBlockFormatter.kt:222)
  at org.wordpress.aztec.formatting.LineBlockFormatter.insertSpanAfterBlock (LineBlockFormatter.kt:201)
  at org.wordpress.aztec.formatting.LineBlockFormatter.applyHorizontalRule (LineBlockFormatter.kt:121)
  at org.wordpress.aztec.AztecText.toggleFormatting (AztecText.kt:1459)
  at org.wordpress.aztec.toolbar.AztecToolbar.onToolbarAction (AztecToolbar.kt:615)
  at org.wordpress.aztec.toolbar.AztecToolbar.setupToolbarItems$lambda$18$lambda$17 (AztecToolbar.kt:799)
  at android.view.View.performClick (View.java:8464)
Exception java.lang.IllegalArgumentException: Invalid offset: 3. Valid range is [0, 2]
  at android.text.method.WordIterator.checkOffsetIsValid (WordIterator.java:401)
  at android.text.method.WordIterator.isBoundary (WordIterator.java:112)
  at android.widget.Editor$SelectionHandleView.positionAtCursorOffset (Editor.java:7941)
  at android.widget.Editor$HandleView.updatePosition (Editor.java:6297)
  at android.widget.Editor$HandleView.updateDrawable (Editor.java:6052)
  at android.widget.Editor$SelectionHandleView.updateSelection (Editor.java:7708)
  at android.widget.Editor$HandleView.positionAtCursorOffset (Editor.java:6257)
  at android.widget.Editor$SelectionHandleView.positionAtCursorOffset (Editor.java:7940)
  at android.widget.Editor$HandleView.invalidate (Editor.java:6122)
  at android.widget.Editor$SelectionModifierCursorController.invalidateHandles (Editor.java:8981)
  at android.widget.Editor.invalidateHandlesAndActionMode (Editor.java:2595)
  at android.widget.TextView.spanChange (TextView.java:13810)
  at android.widget.TextView$ChangeWatcher.onSpanChanged (TextView.java:17629)
  at android.text.SpannableStringBuilder.sendSpanChanged (SpannableStringBuilder.java:1309)
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:754)
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:678)
  at androidx.emoji2.text.SpannableBuilder.setSpan (SpannableBuilder.java:135)
  at org.wordpress.aztec.watchers.SuggestionWatcher.reapplyCarriedOverInlineSpans (SuggestionWatcher.kt:147)
  at org.wordpress.aztec.watchers.SuggestionWatcher.onTextChanged (SuggestionWatcher.kt:82)
  at android.widget.TextView.sendOnTextChanged (TextView.java:13573)
  at android.widget.TextView.handleTextChanged (TextView.java:13702)
  at android.widget.TextView$ChangeWatcher.onTextChanged (TextView.java:17605)
  at android.text.SpannableStringBuilder.sendTextChanged (SpannableStringBuilder.java:1269)
  at android.text.SpannableStringBuilder.replace (SpannableStringBuilder.java:578)
  at androidx.emoji2.text.SpannableBuilder.replace (SpannableBuilder.java:308)
  at android.text.SpannableStringBuilder.append (SpannableStringBuilder.java:272)
  at androidx.emoji2.text.SpannableBuilder.append (SpannableBuilder.java:337)
  at androidx.emoji2.text.SpannableBuilder.append (SpannableBuilder.java:48)
  at org.wordpress.aztec.formatting.LineBlockFormatter$insertSpanAfterBlock$3.invoke (LineBlockFormatter.kt:202)
  at org.wordpress.aztec.formatting.LineBlockFormatter$insertSpanAfterBlock$3.invoke (LineBlockFormatter.kt:201)
  at org.wordpress.aztec.formatting.LineBlockFormatter.applyWithRemovedSpans (LineBlockFormatter.kt:220)
  at org.wordpress.aztec.formatting.LineBlockFormatter.insertSpanAfterBlock (LineBlockFormatter.kt:201)
  at org.wordpress.aztec.formatting.LineBlockFormatter.applyHorizontalRule (LineBlockFormatter.kt:121)
  at org.wordpress.aztec.AztecText.toggleFormatting (AztecText.kt:1459)
  at org.wordpress.aztec.toolbar.AztecToolbar.onToolbarAction (AztecToolbar.kt:615)
  at org.wordpress.aztec.toolbar.AztecToolbar.setupToolbarItems$lambda$18$lambda$17 (AztecToolbar.kt:799)
  at android.view.View.performClick (View.java:8508)
Exception java.lang.IllegalArgumentException:
  at android.text.method.WordIterator.checkOffsetIsValid (WordIterator.java:384)
  at android.text.method.WordIterator.isBoundary (WordIterator.java:94)
  at android.widget.Editor$SelectionHandleView.positionAtCursorOffset (Editor.java:5470)
  at android.widget.Editor$HandleView.invalidate (Editor.java:4580)
  at android.widget.Editor$SelectionModifierCursorController.invalidateHandles (Editor.java:6151)
  at android.widget.Editor.invalidateHandlesAndActionMode (Editor.java:1985)
  at android.widget.TextView.spanChange (TextView.java:9941)
  at android.widget.TextView$ChangeWatcher.onSpanAdded (TextView.java:12570)
  at android.text.SpannableStringBuilder.sendSpanAdded (SpannableStringBuilder.java:1283)
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:775)
  at android.text.SpannableStringBuilder.setSpan (SpannableStringBuilder.java:674)
  at androidx.emoji2.text.SpannableBuilder.setSpan (SpannableBuilder.java:135)
  at org.wordpress.aztec.watchers.SuggestionWatcher.reapplyCarriedOverInlineSpans (SuggestionWatcher.kt:147)
  at org.wordpress.aztec.watchers.SuggestionWatcher.onTextChanged (SuggestionWatcher.kt:82)
  at android.widget.TextView.sendOnTextChanged (TextView.java:9769)
  at android.widget.TextView.handleTextChanged (TextView.java:9866)
  at android.widget.TextView$ChangeWatcher.onTextChanged (TextView.java:12538)
  at android.text.SpannableStringBuilder.sendTextChanged (SpannableStringBuilder.java:1263)
  at android.text.SpannableStringBuilder.replace (SpannableStringBuilder.java:575)
  at androidx.emoji2.text.SpannableBuilder.replace (SpannableBuilder.java:308)
  at android.text.SpannableStringBuilder.insert (SpannableStringBuilder.java:224)
  at androidx.emoji2.text.SpannableBuilder.insert (SpannableBuilder.java:316)
  at androidx.emoji2.text.SpannableBuilder.insert (SpannableBuilder.java:48)
  at org.wordpress.aztec.formatting.IndentFormatter.indent (IndentFormatter.kt:49)
  at org.wordpress.aztec.formatting.BlockFormatter.indent (BlockFormatter.kt:75)
  at org.wordpress.aztec.AztecText.indent (AztecText.kt:1211)
  at org.wordpress.aztec.toolbar.AztecToolbar.onToolbarAction (AztecToolbar.kt:655)
  at org.wordpress.aztec.toolbar.AztecToolbar.setupToolbarItems$lambda$18$lambda$17 (AztecToolbar.kt:799)
  at android.view.View.performClick (View.java:6614)

- Clamp AztecText selection to valid range to avoid framework crashes on invalid offsets

- Guard span restore ranges in SuggestionWatcher and LineBlockFormatter

- Add regression test for outdent-at-start crash
@Twinsen81 Twinsen81 requested a review from zwarm December 30, 2025 17:50
@zwarm zwarm self-assigned this Dec 30, 2025
Copy link
Contributor

@zwarm zwarm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Twinsen81 . LGTM! I really like the clamping at the AztecText level and the defensive guards in the formatters should prevent the SpannableStringBuilder crashes. Nice work!

@Twinsen81 Twinsen81 merged commit 7c17677 into trunk Dec 30, 2025
12 of 14 checks passed
@Twinsen81 Twinsen81 deleted the dodroid-604-spannablestringbuilder-crashes-in-aztec branch December 30, 2025 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants