My code works perfectly 99% of the time, but if I mash the keyboard long enough, I can sometimes get it to throw an IndexOutOfBounds exception. I can't tell you exactly how to reproduce it, because I can't reproduce it myself without literal minutes of keyboard mashing.
The bounds match the character limits on my different editTexts, but I can't work out what is causing an operation to try to do something outside of bounds.
The error has been triggered (indirectly) by the pushDigitToNextEditText, moveCursorIfNecessary, and pushDigitToFront methods, but it has also occurred with none of my code on the stacktrace, just android source code. (How is that possible?)
I've been able to find very little about this particular kind of outOfBounds exception, and I'm just about ready to give up. I can't believe an actual user would abuse the edit text boxes the way I have to to get it to throw the error, but who knows?
For the same reason I can't tell you how to reproduce it, I don't know what the minimum amount of code necessary is. So here's all of it. I added one of the stack traces, but I can give you more. I have lots.
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")
}
}
}
Layout
<?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>
Stack trace
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