18

Not able to track crash in the project, I got this error in play store pre-launch section, it showing on click of EditText, it got the error. but not getting any crash on a real device.

Issue: java.lang.IndexOutOfBoundsException: setSpan (4 ... 4) ends beyond length 0

Fatal Exception: java.lang.IndexOutOfBoundsException: setSpan (4 ... 4) ends beyond length 0
       at android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java:1096)
       at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:671)
       at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:664)
       at android.text.Selection.setSelection(Selection.java:76)
       at android.text.Selection.setSelection(Selection.java:87)
       at android.widget.EditText.setSelection(EditText.java:98)
       at android.widget.EditText.performAccessibilityActionInternal(EditText.java:138)
       at android.view.View.performAccessibilityAction(View.java:8892)
       at android.view.AccessibilityInteractionController.performAccessibilityActionUiThread(AccessibilityInteractionController.java:668)
       at android.view.AccessibilityInteractionController.-wrap6(AccessibilityInteractionController.java)
       at android.view.AccessibilityInteractionController$PrivateHandler.handleMessage(AccessibilityInteractionController.java:1194)
       at android.os.Handler.dispatchMessage(Handler.java:102)
       at android.os.Looper.loop(Looper.java:148)
       at android.app.ActivityThread.main(ActivityThread.java:5459)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:728)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
Android
  • 1,420
  • 4
  • 13
  • 23
  • *setSpan (4 ... 4) ends beyond length 0 You check the text length before call the setSpan method. – Mridul Das Feb 20 '19 at 05:53
  • 1
    I am not using setSpan method, only used methods for edittext are """ editText.isFocused(); editText.requestFocus(); editText.setText(""); """ – Ravi Prakash Yadav Feb 20 '19 at 13:24
  • 4
    I have exactly this problem in one of my apps, already for months. Only shows up in prelaunch report, never in production. And I don't use setSpan either. – Mario Huizinga Mar 14 '19 at 09:50
  • same here. For me it's also caused by `performAccessibilityAction`. Looks like a bug in the SDK to me – Fabian Streitel Mar 28 '19 at 20:01
  • I'm seeing the exact same problem, only it's happening on a (real) rooted device running Android 6.0.1. It's never been fixed. – FractalBob Nov 19 '20 at 05:51

3 Answers3

13

I was seeing his same error, both in the pre-launch report and in FireBase test lab. I spent several hours looking into this and I am sure it is a bug that only affects SDK <= 23 and is triggered by AccessibilityNodeInfo#performAction(ACTION_SET_TEXT). It seems this method completely ignores the maxLength attribute which in turn causes the IndexOutOfBoundsException.

You can duplicate this same exception with the following code, making sure the string "1234" is longer than your maxLength:

 Bundle arguments = new Bundle();
 arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "1234");
 mEditText.performAccessibilityAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);

So, what to do about it?

One option is to just ignore it as it is likely to affect a very small subset of users. that is unless you think you might have many users using accessibility functions to enter text and they are also using older SDKs (<= 23).

Another option would be to set your minSDKVersion in your build.gradle to 24. This might hurt if you have a lot of users using SDK <= 23

A third option would be to use this very ugly workaround I came up with:

if(Build.VERSION.SDK_INT <= 23){
        ViewGroup rootView = findViewById(R.id.your_layout_that_contains_edittexts);
        ArrayList<View> views = rootView.getTouchables();
        for(View view : views){
            if(view instanceof EditText){
                EditText mEditText = (EditText)view;
                mEditText.setAccessibilityDelegate(new View.AccessibilityDelegate() {
                    @Override
                    public boolean performAccessibilityAction(View host, int action, Bundle arguments) {

                        if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
                            //do something here to make sure index out of bounds does not occur
                            int maxLength = 0;
                            for(InputFilter filter : mEditText.getFilters()){
                                if(filter instanceof InputFilter.LengthFilter)  {
                                    maxLength = ((InputFilter.LengthFilter)filter).getMax();
                                }
                            }

                            Set<String> keys  = arguments.keySet();
                            for(String key : keys){
                                if(arguments.get(key) instanceof  CharSequence){
                                    if(arguments.get(key) != null) {
                                        arguments.putCharSequence(key, ((CharSequence) arguments.get(key)).subSequence(0, maxLength));
                                        mEditText.setText(arguments.getCharSequence(key));
                                    }
                                }
                            }
                        }
                        return true;
                    }
                });

            }
        }

    }

OK, so I did say it was ugly, but it does work. Replace your_layout_that_contains_edittexts with the id of a layout that contains all your EditTexts.

Basically, if SDK <= 23, it cycles through all the EditTexts in the ViewGroup and for each one, overrides the performAccessibilityAction via AccessibilityDelegate and ensures that the text being entered never exceeds the EditText's maxLength value.

jwitt98
  • 1,194
  • 1
  • 16
  • 30
  • 3
    +1 for the suggestion on how to reproduce! For future reference, sometimes you need even a longer string there to crash as "1234" might not be long enough – Noya Mar 13 '20 at 13:07
6

Inspired by @smitty1's solution I made this one. I handle it in a custom EditText instead of checking all EditTexts in a view:

open class MaxLengthEditText : AppCompatAutoCompleteTextView {

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    /*
     * On Android API versions <= 23 the app crashes if a text that is longer than the max length of the EditText
     * is set using an accessibility action. To avoid this we cut the text down to the maximum allowed length.
     */
    override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
        if (Build.VERSION.SDK_INT <= 23 && action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
            filters.forEach { filter ->
                if (filter is InputFilter.LengthFilter) {
                    val maxLength = filter.max
                    arguments?.keySet()?.forEach { key ->
                        if (arguments[key] is CharSequence) {
                            val shorterText = 
arguments.getCharSequence(key)?.subSequence(0, maxLength)
                            setText(shorterText)
                            return true
                        }
                    }
                }
            }
        }

        return super.performAccessibilityAction(action, arguments)
    }
}
Anders Ullnæss
  • 818
  • 9
  • 8
0

In my case EditText did not have a maxLength set. However we are using some custom InputFilter. Here how it looks like:

private val textFilter = object : InputFilter {

    override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
        return when {
            good -> null // just allow change as is
            bad -> "" // ignore change totally[*].
            else -> magic // return a calculated value
        }
    }
}

Problem[*] was ignoring the value totally by returning empty String which was causing crash on Marshmellow and below devices. Here is how I have solved this:

private val textFilter = object : InputFilter {

    override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
        return when {
            good -> null // just allow change as is
            bad -> (dest.toString() + source.subSequence(start, end)).subSequence(dstart, dstart + (end - start)) // ignore change totally[*].
            else -> magic // return a calculated value
        }
    }
}

Key Point is not to return anything smaller than the change (end-start) (on M and below devices).


I still thank previous answers which helped me to identify the root cause.

guness
  • 6,336
  • 7
  • 59
  • 88