I think this is most clearly implemented using an InputFilter instead of TextWatcher. Then you don’t have to worry about putting the cursor in the right spot if you reject the change.
Figure out what the text would look like after the change and whether it's acceptable. Reject it if it's not.
fun CharSequence.hasConsecutiveChars(count: Int) {
var cur: Char? = null
var curCount = 0
for (c in this) {
if (c != cur) {
cur = c
curCount = 0
}
if (++curCount >= count) return true
}
return false
}
val filter = InputFilter { source, start, end, dest, dstart, dend ->
val afterChange = source.replaceRange(start..end, dest.subSequence(dstart..dend))
if (afterChange.hasConsecutiveChars(4)) "" else null
}
editText.setInputFilters(filter)