Skip to content

Commit 243aecc

Browse files
kirillzyuskofacebook-github-bot
authored andcommittedFeb 3, 2025·
fix: avoid ConcurrentModificationException (#49109)
Summary: Fixes a `ConcurrentModificationException` when iterating over `TextWatcher` `mListeners` array. If you open Android open source code (`TextView` class), then we can see that Android iterates with `for/n` loop (not `for/:`): ```java void sendAfterTextChanged(Editable text) { if (mListeners != null) { final ArrayList<TextWatcher> list = mListeners; final int count = list.size(); for (int i = 0; i < count; i++) { list.get(i).afterTextChanged(text); } } notifyListeningManagersAfterTextChanged(); hideErrorIfUnchanged(); } ``` <hr> We can catch the `ConcurrentModificationException` with old code, when for example we have 3 listeners: - 0 is `EmojiTextWatcher` (seems like it's added by OS); - 1 is `OnlyChangeIfRequiredMaskedTextChangedListener` (added by `react-native-text-input-mask`); - 2 is a listener that attached by `react-native-keyboard-controller`. On every afterTextChanged [input-mask-android](https://github.com/RedMadRobot/input-mask-android/tree/df452edc0c52a37e5082adcfc3d05d77b5aa34e8) [removes](https://github.com/RedMadRobot/input-mask-android/blob/df452edc0c52a37e5082adcfc3d05d77b5aa34e8/inputmask/src/main/kotlin/com/redmadrobot/inputmask/MaskedTextChangedListener.kt#L212) the listener and [adds](https://github.com/RedMadRobot/input-mask-android/blob/df452edc0c52a37e5082adcfc3d05d77b5aa34e8/inputmask/src/main/kotlin/com/redmadrobot/inputmask/MaskedTextChangedListener.kt#L231) it back. The oversimplified version of the code can be next: ```java public class MyClass { public static void main(String args[]) { ArrayList<Integer> mListeners = new ArrayList<>(); mListeners.add(0); mListeners.add(1); mListeners.add(2); Iterator<Integer> iterator = mListeners.iterator(); while (iterator.hasNext()) { Integer listener = iterator.next(); // Check if the listener is equal to 1 // 1 is OnlyChangeIfRequiredMaskedTextChangedListener and we simulate the behavior of this class if (listener == 1) { int i = mListeners.indexOf(listener); if (i >= 0) { mListeners.remove(i); } // Add the removed element at the end mListeners.add(listener); } } // Print the modified list System.out.println(mListeners); } } ``` Key points are: - if we have only [0, 1] listener, then it works well and `ConcurrentModificationException` will not be thrown, because we modify last element; - if we have `[0, 1, 2]` then exception will be thrown. So in this PR I decided to re-work code to match what Android has. With `for/n` approach `ConcurrentModificationException` will not be thrown, because we don't check array immutability in this case. More information also can be found here: kirillzyusko/react-native-keyboard-controller#324 ## Changelog: [ANDROID] [CHANGED] - avoid `ConcurrentModificationException` when iterating over `mListeners` `TextWatcher` array Pull Request resolved: #49109 Reviewed By: cortinico Differential Revision: D69050984 Pulled By: javache fbshipit-source-id: 9c6a7a428467fa5e546d70549dfcc91d6b2e58d2
1 parent 880fd92 commit 243aecc

File tree

1 file changed

+3
-3
lines changed
  • packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput

1 file changed

+3
-3
lines changed
 

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@
8080
import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan;
8181
import com.facebook.react.views.text.internal.span.ReactUnderlineSpan;
8282
import com.facebook.react.views.text.internal.span.TextInlineImageSpan;
83-
import java.util.ArrayList;
8483
import java.util.Objects;
84+
import java.util.concurrent.CopyOnWriteArrayList;
8585

8686
/**
8787
* A wrapper around the EditText that lets us better control what happens when an EditText gets
@@ -111,7 +111,7 @@ public class ReactEditText extends AppCompatEditText {
111111
/** A count of events sent to JS or C++. */
112112
protected int mNativeEventCount;
113113

114-
private @Nullable ArrayList<TextWatcher> mListeners;
114+
private @Nullable CopyOnWriteArrayList<TextWatcher> mListeners;
115115
private @Nullable TextWatcherDelegator mTextWatcherDelegator;
116116
private int mStagedInputType;
117117
protected boolean mContainsImages;
@@ -390,7 +390,7 @@ private boolean requestFocusProgramatically() {
390390
@Override
391391
public void addTextChangedListener(TextWatcher watcher) {
392392
if (mListeners == null) {
393-
mListeners = new ArrayList<>();
393+
mListeners = new CopyOnWriteArrayList<>();
394394
super.addTextChangedListener(getTextWatcherDelegator());
395395
}
396396

0 commit comments

Comments
 (0)
Please sign in to comment.