Таинственный indexOutOfBoundsException, включающий показатель MeasureLimit в Android - PullRequest
6 голосов
/ 08 января 2020

Мой код работает идеально в 99% случаев, но если я наберу sh достаточно длинную клавиатуру, я иногда могу заставить ее выдать исключение IndexOutOfBounds. Я не могу сказать вам точно, как воспроизвести это, потому что я не могу воспроизвести это сам без буквальных минут затирания клавиатуры.

Границы соответствуют ограничениям символов на моих различных editTexts, но я не могу работать что заставляет операцию пытаться сделать что-то за пределами.

Ошибка была вызвана (косвенно) методами pushDigitToNextEditText, moveCursorIfNeeded и pushDigitToFront, но она также произошла, когда мой код на трассировке стека отсутствовал, только android исходный код. (Как это возможно?)

Мне удалось найти очень мало об этом особом исключении outOfBounds, и я почти готов сдаться. Я не могу поверить, что реальный пользователь злоупотребил бы текстовыми полями редактирования так, как я должен, чтобы заставить его выдать ошибку, но кто знает?

По той же причине я не могу сказать вам, как воспроизвести это, я не знаю, каков минимальный объем необходимого кода. Вот и все. Я добавил одну из трассировок стека, но могу дать вам больше. У меня много

package com.example.phonenumberdemo

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
import com.example.phonenumberdemo.databinding.ActivityMainBinding
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setUpPhoneEditTexts()
    }

    private var digitDeletedFlag = false
    private var userIsNotMakingChanges = false
    private var prevEditText1CursorPosition: Int? = null
    private var prevEditText2CursorPosition: Int? = null

    private fun setUpPhoneEditTexts() {
        phoneEditText1.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(p0: Editable?) {
                if (userIsNotMakingChanges) {
                    return
                }
                if (phoneEditText1.text.length == 3) {
                    val lastDigit = chopOffLastDigit(1)
                    pushDigitToNextEditText(lastDigit, 2)
                    moveCursorIfNecessary(1)
                }
                if (digitDeletedFlag) {
                    pullDigitFromNextEditText(1)
                    digitDeletedFlag = false
                }
                prevEditText1CursorPosition = null
            }

            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                if (p3 < p2 && !userIsNotMakingChanges) {
                    digitDeletedFlag = true
                }
                prevEditText1CursorPosition = phoneEditText1.selectionStart
            }

            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
        })

        phoneEditText2.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(p0: Editable?) {
                if (userIsNotMakingChanges) {
                    return
                }
                if (phoneEditText2.selectionEnd == 0) {
                    moveCursorBackTo(1)
                }
                if (phoneEditText2.text.length == 4) {
                    val lastDigit = chopOffLastDigit(2)
                    pushDigitToNextEditText(lastDigit, 3)
                    moveCursorIfNecessary(2)
                }
                if (digitDeletedFlag) {
                    pullDigitFromNextEditText(2)
                    digitDeletedFlag = false
                }
                prevEditText2CursorPosition = null
            }

            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                if (p3 < p2 && !userIsNotMakingChanges) {
                    digitDeletedFlag = true
                }
                prevEditText2CursorPosition = phoneEditText2.selectionStart
            }

            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
        })

        phoneEditText3.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(p0: Editable?) {
                if (userIsNotMakingChanges) {
                    return
                }
                if (phoneEditText3.selectionEnd == 0) {
                    moveCursorBackTo(2)
                }
                if (phoneEditText3.text.length == 5) {
                    chopOffLastDigit(3)
                }
            }

            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
        })
    }

    private fun chopOffLastDigit(editTextNum: Int): Char {
        val editText = getEditText(editTextNum)
        val maxLength = getMaxLength(editTextNum)
        val lastDigit = editText.text[maxLength]

        userIsNotMakingChanges = true
        editText.text.delete(maxLength, maxLength + 1)
        userIsNotMakingChanges = false

        return lastDigit
    }

    private fun pushDigitToNextEditText(digit: Char, nextEditTextNum: Int) {
        val nextEditText = getEditText(nextEditTextNum)
        val nextNextEditTextNum = getNextEditTextNum(nextEditTextNum)
        val maxLength = getMaxLength(nextEditTextNum)

        userIsNotMakingChanges = true
        var bumpedDigit: Char? = null
        if (nextEditText.text.length == maxLength) {
            bumpedDigit = nextEditText.text[maxLength - 1]
            nextEditText.text.delete(maxLength - 1, maxLength)
        }
        nextEditText.text.insert(0, digit.toString())
        if (bumpedDigit != null && nextNextEditTextNum != null) {
            pushDigitToNextEditText(bumpedDigit, nextNextEditTextNum)
        }
        userIsNotMakingChanges = false
    }

    private fun moveCursorBackTo(prevEditTextNum: Int) {
        val prevEditText = getEditText(prevEditTextNum)
        prevEditText.requestFocus()
        prevEditText.setSelection(prevEditText.length())
    }

    private fun moveCursorIfNecessary(editTextNum: Int) {
        if (editTextNum == 1 && prevEditText1CursorPosition!! > 1) {
            phoneEditText2.requestFocus()
            phoneEditText2.setSelection(prevEditText1CursorPosition!! - 2)
        } else if (editTextNum == 2 && prevEditText2CursorPosition!! > 2) {
            phoneEditText3.requestFocus()
            phoneEditText3.setSelection(prevEditText2CursorPosition!! - 3)
        }
    }

    private fun pullDigitFromNextEditText(editTextNum: Int) {
        val editText = getEditText(editTextNum)
        val maxLength = getMaxLength(editTextNum)
        val nextEditTextNum = getNextEditTextNum(editTextNum)
        val nextEditText = getNextEditText(editTextNum)

        if (editText.text.length != maxLength - 1) {
            return
        }
        if (nextEditText?.text.isNullOrEmpty()) {
            return
        }

        userIsNotMakingChanges = true
        val pulledDigit = nextEditText!!.text[0]
        editText.text.append(pulledDigit)
        if (editText.selectionEnd == editText.text.length) {
            editText.setSelection(editText.selectionEnd - 1)
        }
        nextEditText.text.delete(0, 1)
        nextEditText.setText(nextEditText.text.toString())
        pullDigitFromNextEditText(nextEditTextNum!!)
        userIsNotMakingChanges = false
    }

    private fun getEditText(editTextNum: Int): EditText {
        return when (editTextNum) {
            1 -> phoneEditText1
            2 -> phoneEditText2
            3 -> phoneEditText3
            else -> error("invalid editTextNum")
        }
    }

    private fun getMaxLength(editTextNum: Int): Int {
        return when (editTextNum) {
            1 -> 2
            2 -> 3
            3 -> 4
            else -> error("invalid editTextNum")
        }
    }

    private fun getNextEditTextNum(editTextNum: Int): Int? {
        return when (editTextNum) {
            1 -> 2
            2 -> 3
            3 -> null
            else -> error("invalid editTextNum")
        }
    }

    private fun getNextEditText(editTextNum: Int): EditText? {
        return when (editTextNum) {
            1 -> phoneEditText2
            2 -> phoneEditText3
            3 -> null
            else -> error("invalid editTextNum")
        }
    }
}

Макет

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">

        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Enter your phone number to create an account or to log in."
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="32dp"
                android:textAlignment="center"
                android:layout_marginLeft="16dp"
                android:layout_marginRight="16dp" android:textSize="24sp" android:id="@+id/text_block"/>


        <TextView
                android:id="@+id/phoneTextView1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" android:text="(5"
                android:textSize="32sp" app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="8dp"
                android:layout_marginLeft="8dp"
                android:textColor="@color/colorPrimaryDark"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintEnd_toStartOf="@+id/phoneEditText1"
                app:layout_constraintHorizontal_chainStyle="packed" android:layout_marginTop="32dp"
                app:layout_constraintTop_toBottomOf="@+id/text_block"/>
        <EditText
                android:id="@+id/phoneEditText1"
                android:layout_width="43sp"
                android:layout_height="wrap_content"
                android:inputType="number"
                android:maxLength="3"
                android:textColor="@color/colorPrimaryDark"
                app:layout_constraintStart_toEndOf="@+id/phoneTextView1"
                app:layout_constraintTop_toTopOf="@+id/phoneTextView1"
                app:layout_constraintBottom_toBottomOf="@+id/phoneTextView1" android:textSize="32sp"
                app:layout_constraintHorizontal_bias="0.5" app:layout_constraintEnd_toStartOf="@+id/phoneTextView2"/>
        <TextView
                android:id="@+id/phoneTextView2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" android:text=")"
                app:layout_constraintStart_toEndOf="@+id/phoneEditText1"
                android:textSize="32sp"
                android:textColor="@color/colorPrimaryDark"
                app:layout_constraintBottom_toBottomOf="@+id/phoneTextView1"
                app:layout_constraintTop_toTopOf="@+id/phoneTextView1" app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintEnd_toStartOf="@+id/phoneEditText2"/>
        <EditText
                android:id="@+id/phoneEditText2"
                android:layout_width="63sp"
                android:layout_height="wrap_content"
                android:inputType="number"
                android:maxLength="4"
                android:ems="10"
                app:layout_constraintStart_toEndOf="@+id/phoneTextView2"
                app:layout_constraintBottom_toBottomOf="@+id/phoneTextView1"
                android:textColor="@color/colorPrimaryDark"
                app:layout_constraintTop_toTopOf="@+id/phoneTextView1" android:textSize="32sp"
                app:layout_constraintHorizontal_bias="0.5" app:layout_constraintEnd_toStartOf="@+id/phoneTextView3"/>
        <TextView
                android:id="@+id/phoneTextView3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" android:text="-"
                app:layout_constraintStart_toEndOf="@+id/phoneEditText2"
                app:layout_constraintBottom_toBottomOf="@+id/phoneTextView1"
                app:layout_constraintTop_toTopOf="@+id/phoneTextView1"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="32sp" app:layout_constraintEnd_toStartOf="@+id/phoneEditText3"
                app:layout_constraintHorizontal_bias="0.5"/>
        <EditText
                android:id="@+id/phoneEditText3"
                android:layout_width="84sp"
                android:layout_height="wrap_content"
                android:inputType="number"
                android:maxLength="5"
                android:ems="10"
                app:layout_constraintStart_toEndOf="@+id/phoneTextView3"
                app:layout_constraintBottom_toBottomOf="@+id/phoneTextView1"
                app:layout_constraintTop_toTopOf="@+id/phoneTextView1" android:textSize="32sp"
                android:textColor="@color/colorPrimaryDark"
                app:layout_constraintHorizontal_bias="0.5" app:layout_constraintEnd_toEndOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Трассировка стека

2020-01-07 20:56:27.344 16142-16142/com.example.phonenumberdemo E/MessageQueue-JNI: java.lang.IndexOutOfBoundsException: measureLimit (4) is out of start (0) and limit (3) bounds
        at android.text.TextLine.handleRun(TextLine.java:1106)
        at android.text.TextLine.measureRun(TextLine.java:528)
        at android.text.TextLine.measure(TextLine.java:326)
        at android.text.Layout.getHorizontal(Layout.java:1184)
        at android.text.Layout.getHorizontal(Layout.java:1162)
        at android.text.Layout.getPrimaryHorizontal(Layout.java:1133)
        at android.text.Layout.getPrimaryHorizontal(Layout.java:1123)
        at android.widget.Editor$SelectionHandleView.getHorizontal(Editor.java:5562)
        at android.widget.Editor$SelectionHandleView.getHorizontal(Editor.java:5553)
        at android.widget.Editor$HandleView.getCursorHorizontalPosition(Editor.java:4706)
        at android.widget.Editor$HandleView.positionAtCursorOffset(Editor.java:4685)
        at android.widget.Editor$SelectionHandleView.positionAtCursorOffset(Editor.java:5466)
        at android.widget.Editor$HandleView.show(Editor.java:4596)
        at android.widget.Editor$SelectionModifierCursorController.initHandles(Editor.java:5788)
        at android.widget.Editor$SelectionModifierCursorController.show(Editor.java:5760)
        at android.widget.Editor$SelectionModifierCursorController.enterDrag(Editor.java:5800)
        at android.widget.Editor.selectCurrentWordAndStartDrag(Editor.java:2187)
        at android.widget.Editor.access$7800(Editor.java:144)
        at android.widget.Editor$SelectionModifierCursorController.onTouchEvent(Editor.java:5848)
        at android.widget.Editor.onTouchEvent(Editor.java:1475)
        at android.widget.TextView.onTouchEvent(TextView.java:10056)
        at android.view.View.dispatchTouchEvent(View.java:12513)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440)
        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
        at android.app.Activity.dispatchTouchEvent(Activity.java:3400)
        at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
        at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:398)
        at android.view.View.dispatchPointerEvent(View.java:12752)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5106)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4909)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4585)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4642)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7092)
2020-01-07 20:56:27.344 16142-16142/com.example.phonenumberdemo D/AndroidRuntime: Shutting down VM
2020-01-07 20:56:27.351 16142-16142/com.example.phonenumberdemo E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.phonenumberdemo, PID: 16142
    java.lang.IndexOutOfBoundsException: measureLimit (4) is out of start (0) and limit (3) bounds
        at android.text.TextLine.handleRun(TextLine.java:1106)
        at android.text.TextLine.measureRun(TextLine.java:528)
        at android.text.TextLine.measure(TextLine.java:326)
        at android.text.Layout.getHorizontal(Layout.java:1184)
        at android.text.Layout.getHorizontal(Layout.java:1162)
        at android.text.Layout.getPrimaryHorizontal(Layout.java:1133)
        at android.text.Layout.getPrimaryHorizontal(Layout.java:1123)
        at android.widget.Editor$SelectionHandleView.getHorizontal(Editor.java:5562)
        at android.widget.Editor$SelectionHandleView.getHorizontal(Editor.java:5553)
        at android.widget.Editor$HandleView.getCursorHorizontalPosition(Editor.java:4706)
        at android.widget.Editor$HandleView.positionAtCursorOffset(Editor.java:4685)
        at android.widget.Editor$SelectionHandleView.positionAtCursorOffset(Editor.java:5466)
        at android.widget.Editor$HandleView.show(Editor.java:4596)
        at android.widget.Editor$SelectionModifierCursorController.initHandles(Editor.java:5788)
        at android.widget.Editor$SelectionModifierCursorController.show(Editor.java:5760)
        at android.widget.Editor$SelectionModifierCursorController.enterDrag(Editor.java:5800)
        at android.widget.Editor.selectCurrentWordAndStartDrag(Editor.java:2187)
        at android.widget.Editor.access$7800(Editor.java:144)
        at android.widget.Editor$SelectionModifierCursorController.onTouchEvent(Editor.java:5848)
        at android.widget.Editor.onTouchEvent(Editor.java:1475)
        at android.widget.TextView.onTouchEvent(TextView.java:10056)
        at android.view.View.dispatchTouchEvent(View.java:12513)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
        at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
        at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440)
        at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
        at android.app.Activity.dispatchTouchEvent(Activity.java:3400)
        at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
        at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:398)
        at android.view.View.dispatchPointerEvent(View.java:12752)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5106)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4909)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4585)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4642)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
2020-01-07 20:56:27.351 16142-16142/com.example.phonenumberdemo E/AndroidRuntime:     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
        at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7092)
        at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7061)
        at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7022)
        at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7195)
        at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:186)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:326)
        at android.os.Looper.loop(Looper.java:160)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
2020-01-07 20:56:26.272 16142-16166/com.example.phonenumberdemo D/TextClassifierService: No configured system TextClassifierService
2020-01-07 20:56:27.373 16142-16142/com.example.phonenumberdemo I/Process: Sending signal. PID: 16142 SIG: 9
...