Fix crashes onKeyPress Android

Summary:
There appear to be two different types of crashes related to the recent addition of `onKeyPress` on Android introduce in `0.53`. This PR addresses the cause of both of them.

Firstly, it seems possible to get an `indexOutOfBoundsException` with some 3rd-party keyboards as observed in https://github.com/facebook/react-native/issues/17974 & https://github.com/facebook/react-native/issues/17922. I have simplified the backspace determining logic slightly, and also put in an explicit check for zero case so it is not possible to get an indexOutOfBoundsException & it should make sense in the context of the onKeyPress logic.

Secondly, it appears that `EditText#onCreateInputConnection` can return null. In this case, if we set `null` to be the target of our subclass of `ReactEditTextInputConnectionWrapper`, we will see the crashes as seen [here](https://github.com/facebook/react-native/issues/17974#issuecomment-368471737), whereby any of methods executed in the `InputConnection` interface can result in a crash. It's hard to reason about the state when `null` is returned from `onCreateInputConnection`, however I would might reason that any soft keyboard input cannot update the `EditText` with a `null` `input connection`, as there is no way of interfacing with the `EditText`. I'm am not sure, if there is a later point where we might return/set this input connection at a later point? As without the `InputConnection` onKeyPress will not work. But for now, this will fix this crash at least.

I have not managed to reproduce these crashes myself yet, but users have confirmed that the `indexOutOfBounds` exception is fixed with the 'zero' case and has been confirmed on the respective issues https://github.com/facebook/react-native/issues/17974#issuecomment-368471737.

For the `null` inputConnection target case, I have verified that explicitly setting the target as null in the constructor of `onCreateInputConnection` results in the same stack trace as the one linked. Here is also a [reference](https://github.com/stripe/stripe-android/pull/392/files#diff-6cc1685c98457d07fd4e2dd83f54d5bb) to the same issue closed with the same fix for another project on github.

It is also important to verify that the behavior of `onKeyPress` still functions the same after this change, which can be verified by running the RNTesterProject and the `KeyboardEvents` section in `InputText`.
The cases to check that I think are important to check are:
- Cursor at beginning of input & backspace
- Return key & return key at beginning of input
- Select text then press delete
- Selection then press a key
- Space key
- Different keyboard types

This should not be a breaking change.

 [ANDROID] [BUGFIX] [TextInput] - Fixes crashes with TextInput introduced in 0.53.
Closes https://github.com/facebook/react-native/pull/18114

Differential Revision: D7099570

Pulled By: hramos

fbshipit-source-id: 75b2dc468c1ed398a33eb00487c6aa14ae04e5c2
This commit is contained in:
Josh Hargreaves 2018-02-27 09:59:34 -08:00 committed by Facebook Github Bot
parent a759a44358
commit b60a727adb
2 changed files with 11 additions and 8 deletions

View File

@ -172,14 +172,16 @@ public class ReactEditText extends EditText {
@Override @Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) { public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
ReactContext reactContext = (ReactContext) getContext(); ReactContext reactContext = (ReactContext) getContext();
ReactEditTextInputConnectionWrapper inputConnectionWrapper = InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
new ReactEditTextInputConnectionWrapper(super.onCreateInputConnection(outAttrs), reactContext, this); if (inputConnection != null) {
inputConnection = new ReactEditTextInputConnectionWrapper(inputConnection, reactContext, this);
}
if (isMultiline() && getBlurOnSubmit()) { if (isMultiline() && getBlurOnSubmit()) {
// Remove IME_FLAG_NO_ENTER_ACTION to keep the original IME_OPTION // Remove IME_FLAG_NO_ENTER_ACTION to keep the original IME_OPTION
outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
} }
return inputConnectionWrapper; return inputConnection;
} }
@Override @Override

View File

@ -92,14 +92,15 @@ class ReactEditTextInputConnectionWrapper extends InputConnectionWrapper {
int previousSelectionEnd = mEditText.getSelectionEnd(); int previousSelectionEnd = mEditText.getSelectionEnd();
String key; String key;
boolean consumed = super.setComposingText(text, newCursorPosition); boolean consumed = super.setComposingText(text, newCursorPosition);
int currentSelectionStart = mEditText.getSelectionStart();
boolean noPreviousSelection = previousSelectionStart == previousSelectionEnd; boolean noPreviousSelection = previousSelectionStart == previousSelectionEnd;
boolean cursorDidNotMove = mEditText.getSelectionStart() == previousSelectionStart; boolean cursorDidNotMove = currentSelectionStart == previousSelectionStart;
boolean cursorMovedBackwards = mEditText.getSelectionStart() < previousSelectionStart; boolean cursorMovedBackwardsOrAtBeginningOfInput =
if ((noPreviousSelection && cursorMovedBackwards) (currentSelectionStart < previousSelectionStart) || currentSelectionStart <= 0;
|| !noPreviousSelection && cursorDidNotMove) { if (cursorMovedBackwardsOrAtBeginningOfInput || (!noPreviousSelection && cursorDidNotMove)) {
key = BACKSPACE_KEY_VALUE; key = BACKSPACE_KEY_VALUE;
} else { } else {
key = String.valueOf(mEditText.getText().charAt(mEditText.getSelectionStart() - 1)); key = String.valueOf(mEditText.getText().charAt(currentSelectionStart - 1));
} }
dispatchKeyEventOrEnqueue(key); dispatchKeyEventOrEnqueue(key);
return consumed; return consumed;