Skip to content

fix(android): crash indexoutofboundsexecption set span#583

Open
eszlamczyk wants to merge 3 commits into
mainfrom
fix/580/android-crash
Open

fix(android): crash indexoutofboundsexecption set span#583
eszlamczyk wants to merge 3 commits into
mainfrom
fix/580/android-crash

Conversation

@eszlamczyk

Copy link
Copy Markdown
Collaborator

What/Why?

Fixes #580

Long-pressing an EnrichedMarkdownText (commonmark flavor, selectable default) crashes on
Samsung / One UI devices with:

java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0
  at android.text.Selection.setSelection(...)
  at android.widget.TextView.semSetSelection(...)
Root cause.

The component enables setTextIsSelectable(true) but installed a LinkMovementMethod-based movement method, and the two fight over the buffer's Selection spans. LinkLongPressMovementMethod removed the Selection on every ACTION_DOWN (and on UP/CANCEL, plus LinkMovementMethod's own removal for any non-link touch), while the platform Editor - which processes each touch event before the movement method - was concurrently running its long-press word-selection machinery. Instrumentation on a stock emulator showed
getSelectionStart()/getSelectionEnd() returning (-1, -1) for the entire first half of every long-press gesture. AOSP tolerates that state; Samsung's One UI Editor caches the offsets mid-gesture and replays them after select-word via semSetSelectionSelection.setSelection(spannable, -1, -1) → crash. Replaying the cached offsets the way One UI
does reproduces the byte-identical exception on a stock emulator.

Fix.

LinkLongPressMovementMethod now extends ArrowKeyMovementMethod (the method setTextIsSelectable installs natively) and never mutates Selection spans. Link taps are dispatched from touch offsets: the pressed LinkSpan is recorded on ACTION_DOWN and onClick fires on ACTION_UP when the gesture stayed on the same span. Long-press scheduling, touch-slop cancellation, isLinkTouchActive (RNGH interop), spoiler taps, and the empty-space fall-through are unchanged. The same fix is ported to the copy in packages/android-enriched-markdown/ui.

Note

keyboard/d-pad traversal between links (a LinkMovementMethod feature) is not carried over; screen-reader link access is unaffected (handled by accessibilityHelper).

Testing

Repro setup (both paths): the reported crash is in the commonmark-flavor native view, and the example screens use flavor="github" (a different native view), so change the Playground preview to flavor="commonmark" (or remove the flavor prop) first. Then type some **markdown** text into the input so the preview renders it.

On a Samsung device (real crash, One UI) - NOT TESTED

  1. Build the example app APK and install it (locally yarn react-native-example android, or upload the APK to a Remote Test Lab session - pick an Android 13 / One UI device).
  2. In the Playground preview, long-press a word and drag your finger slightly while holding.
  3. Before this PR: the app crashes with IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0 (visible in logcat). After this PR: the word is selected normally with handles and the selection menu.

Without a Samsung device (any emulator - verifies the root cause and the fix)

The crash is Samsung-only, but the state that causes it is created by our code and is observable
anywhere. Add temporary logging around super.onTouchEvent in EnrichedMarkdownText.onTouchEvent:

Log.d("SelDebug", "${MotionEvent.actionToString(event.action)} BEFORE: $selectionStart..$selectionEnd")
val result = super.onTouchEvent(event)
Log.d("SelDebug", "${MotionEvent.actionToString(event.action)} AFTER:  $selectionStart..$selectionEnd")
  1. Run the app, long-press a word in the preview while watching adb logcat -s SelDebug.
  2. Before this PR: ACTION_DOWN flips the offsets to -1..-1 and they stay there for every
    ACTION_MOVE until the long-press timeout selects the word — this is the invalid state
    Samsung's Editor caches and later feeds into semSetSelection(-1, -1).
    After this PR: the offsets never become -1 at any point in the gesture; they keep their
    previous value until the long-press sets the word range.
  3. (Optional, exact-crash proof) Replay the cached offsets the way One UI does — cache
    selectionStart/End after super on ACTION_DOWN, and on a later ACTION_MOVE where the
    buffer has a valid selection, call
    Selection.setSelection(text as Spannable, cachedStart, cachedEnd). Before this PR a single
    long-press throws the byte-identical setSpan (-1 ... -1) exception on a stock emulator;
    after this PR the replay never fires because the cached offsets are never -1.

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes an Android crash on Samsung / One UI devices when long-pressing selectable EnrichedMarkdownText by preventing the movement method from mutating Selection spans during touch/long-press gestures (avoiding (-1, -1) selection offsets being replayed by One UI).

Changes:

  • Reworks LinkLongPressMovementMethod to extend ArrowKeyMovementMethod and dispatch link taps based on touch offsets (ACTION_DOWN/ACTION_UP), without touching Selection.
  • Preserves long-press scheduling/cancellation behavior while letting the platform selection machinery operate normally.
  • Ports the same movement-method change to the duplicated implementation in packages/android-enriched-markdown/ui.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLongPressMovementMethod.kt Switches to ArrowKeyMovementMethod, removes Selection mutations, and implements offset-based link click dispatch with spoiler tap handling.
packages/android-enriched-markdown/ui/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLongPressMovementMethod.kt Ports offset-based link click dispatch on top of ArrowKeyMovementMethod for the UI package copy.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@eszlamczyk
eszlamczyk marked this pull request as ready for review July 23, 2026 12:36
@eszlamczyk
eszlamczyk requested a review from hryhoriiK97 July 23, 2026 12:36

@hryhoriiK97 hryhoriiK97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good! I left a few minor comments.

* relying on LinkMovementMethod's selection-based implementation, and
* [ArrowKeyMovementMethod] keeps text selection behavior identical to a plain
* selectable TextView.
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The crash forensics are already well documented in the PR description and commit. Let's replace with something like this:

/**
 * Movement method that adds link tap / long-press handling on top of
 * [ArrowKeyMovementMethod], the method [setTextIsSelectable] installs.
 *
 * Must never mutate the buffer's Selection spans — the platform Editor
 * manages them during long-press gestures, and removing or overwriting
 * them mid-gesture crashes on some OEM skins.
 * See: https://github.com/software-mansion/react-native-enriched-markdown/issues/580
 */

* relying on LinkMovementMethod's selection-based implementation, and
* [ArrowKeyMovementMethod] keeps text selection behavior identical to a plain
* selectable TextView.
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The same as above 🔝

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.

Android: crash IndexOutOfBoundsException: setSpan (-1 ... -1) on long-press when selectable is enabled on EnrichedMarkdownText

3 participants