Commit 87389c9d authored by bcwhite@chromium.org's avatar bcwhite@chromium.org

Send correct key-codes when doing composition events instead of always 0.

BUG=118639

Review URL: https://codereview.chromium.org/373523002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@285108 0039d316-1c4b-4281-b951-d872f2087c98
parent d33b81c0
...@@ -460,7 +460,12 @@ public class AdapterInputConnection extends BaseInputConnection { ...@@ -460,7 +460,12 @@ public class AdapterInputConnection extends BaseInputConnection {
super.setComposingRegion(a, b); super.setComposingRegion(a, b);
} }
updateSelectionIfRequired(); updateSelectionIfRequired();
return mImeAdapter.setComposingRegion(a, b);
CharSequence regionText = null;
if (b > a) {
regionText = mEditable.subSequence(start, end);
}
return mImeAdapter.setComposingRegion(regionText, a, b);
} }
boolean isActive() { boolean isActive() {
......
...@@ -116,6 +116,8 @@ public class ImeAdapter { ...@@ -116,6 +116,8 @@ public class ImeAdapter {
static int sModifierCtrl; static int sModifierCtrl;
static int sModifierCapsLockOn; static int sModifierCapsLockOn;
static int sModifierNumLockOn; static int sModifierNumLockOn;
static char[] sSingleCharArray = new char[1];
static KeyCharacterMap sKeyCharacterMap;
private long mNativeImeAdapterAndroid; private long mNativeImeAdapterAndroid;
private InputMethodManagerWrapper mInputMethodManagerWrapper; private InputMethodManagerWrapper mInputMethodManagerWrapper;
...@@ -124,6 +126,10 @@ public class ImeAdapter { ...@@ -124,6 +126,10 @@ public class ImeAdapter {
private final Handler mHandler; private final Handler mHandler;
private DelayedDismissInput mDismissInput = null; private DelayedDismissInput mDismissInput = null;
private int mTextInputType; private int mTextInputType;
private String mLastComposeText;
@VisibleForTesting
int mLastSyntheticKeyCode;
@VisibleForTesting @VisibleForTesting
boolean mIsShowWithoutHideOutstanding = false; boolean mIsShowWithoutHideOutstanding = false;
...@@ -317,8 +323,50 @@ public class ImeAdapter { ...@@ -317,8 +323,50 @@ public class ImeAdapter {
else return COMPOSITION_KEY_CODE; else return COMPOSITION_KEY_CODE;
} }
/**
* @return Android keycode for a single unicode character.
*/
private static int androidKeyCodeForCharacter(char chr) {
if (sKeyCharacterMap == null) {
sKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
}
sSingleCharArray[0] = chr;
// TODO: Evaluate cost of this system call.
KeyEvent[] events = sKeyCharacterMap.getEvents(sSingleCharArray);
if (events == null || events.length != 2) // One key-down event and one key-up event.
return KeyEvent.KEYCODE_UNKNOWN;
return events[0].getKeyCode();
}
@VisibleForTesting
public static int getTypedKeycodeGuess(String oldtext, String newtext) {
// Starting typing a new composition should add only a single character. Any composition
// beginning with text longer than that must come from something other than typing so
// return 0.
if (oldtext == null) {
if (newtext.length() == 1) {
return androidKeyCodeForCharacter(newtext.charAt(0));
} else {
return 0;
}
}
// The content has grown in length: assume the last character is the key that caused it.
if (newtext.length() > oldtext.length() && newtext.startsWith(oldtext))
return androidKeyCodeForCharacter(newtext.charAt(newtext.length() - 1));
// The content has shrunk in length: assume that backspace was pressed.
if (oldtext.length() > newtext.length() && oldtext.startsWith(newtext))
return KeyEvent.KEYCODE_DEL;
// The content is unchanged or has undergone a complex change (i.e. not a simple tail
// modification) so return an unknown key-code.
return 0;
}
void sendKeyEventWithKeyCode(int keyCode, int flags) { void sendKeyEventWithKeyCode(int keyCode, int flags) {
long eventTime = SystemClock.uptimeMillis(); long eventTime = SystemClock.uptimeMillis();
mLastSyntheticKeyCode = keyCode;
translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
...@@ -330,6 +378,7 @@ public class ImeAdapter { ...@@ -330,6 +378,7 @@ public class ImeAdapter {
} }
// Calls from Java to C++ // Calls from Java to C++
// TODO: Add performance tracing to more complicated functions.
boolean checkCompositionQueueAndCallNative(CharSequence text, int newCursorPosition, boolean checkCompositionQueueAndCallNative(CharSequence text, int newCursorPosition,
boolean isCommit) { boolean isCommit) {
...@@ -346,13 +395,38 @@ public class ImeAdapter { ...@@ -346,13 +395,38 @@ public class ImeAdapter {
sendKeyEventWithKeyCode(keyCode, sendKeyEventWithKeyCode(keyCode,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
} else { } else {
keyCode = getTypedKeycodeGuess(mLastComposeText, textStr);
mLastComposeText = textStr;
mLastSyntheticKeyCode = keyCode;
// When typing, there is no issue sending KeyDown and KeyUp events around the
// composition event because those key events do nothing (other than call JS
// handlers). Typing does not cause changes outside of a KeyPress event which
// we don't call here. However, if the key-code is a control key such as
// KEYCODE_DEL then there never is an associated KeyPress event and the KeyDown
// event itself causes the action. The net result below is that the Renderer calls
// cancelComposition() and then Android starts anew with setComposingRegion().
// This stopping and restarting of composition could be a source of problems
// with 3rd party keyboards.
//
// An alternative is to *not* call nativeSetComposingText() in the non-commit case
// below. This avoids the restart of composition described above but fails to send
// an update to the composition while in composition which, strictly speaking,
// does not match the spec.
//
// For now, the solution is to endure the restarting of composition and only dive
// into the alternate solution should there be problems in the field. --bcwhite
nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown, nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
timeStampMs, keyCode, 0); timeStampMs, keyCode, 0);
if (isCommit) { if (isCommit) {
nativeCommitText(mNativeImeAdapterAndroid, textStr); nativeCommitText(mNativeImeAdapterAndroid, textStr);
mLastComposeText = null;
} else { } else {
nativeSetComposingText(mNativeImeAdapterAndroid, text, textStr, newCursorPosition); nativeSetComposingText(mNativeImeAdapterAndroid, text, textStr, newCursorPosition);
} }
nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
timeStampMs, keyCode, 0); timeStampMs, keyCode, 0);
} }
...@@ -425,14 +499,15 @@ public class ImeAdapter { ...@@ -425,14 +499,15 @@ public class ImeAdapter {
} }
/** /**
* Send a request to the native counterpart to set compositing region to given indices. * Send a request to the native counterpart to set composing region to given indices.
* @param start The start of the composition. * @param start The start of the composition.
* @param end The end of the composition. * @param end The end of the composition.
* @return Whether the native counterpart of ImeAdapter received the call. * @return Whether the native counterpart of ImeAdapter received the call.
*/ */
boolean setComposingRegion(int start, int end) { boolean setComposingRegion(CharSequence text, int start, int end) {
if (mNativeImeAdapterAndroid == 0) return false; if (mNativeImeAdapterAndroid == 0) return false;
nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end); nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
mLastComposeText = text != null ? text.toString() : null;
return true; return true;
} }
...@@ -546,6 +621,7 @@ public class ImeAdapter { ...@@ -546,6 +621,7 @@ public class ImeAdapter {
@CalledByNative @CalledByNative
private void cancelComposition() { private void cancelComposition() {
if (mInputConnection != null) mInputConnection.restartInput(); if (mInputConnection != null) mInputConnection.restartInput();
mLastComposeText = null;
} }
@CalledByNative @CalledByNative
......
...@@ -262,6 +262,196 @@ public class ImeTest extends ContentShellTestBase { ...@@ -262,6 +262,196 @@ public class ImeTest extends ContentShellTestBase {
waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1); waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 6, "h\nllo ", 2, 2, -1, -1);
} }
@SmallTest
@Feature({"TextInput", "Main"})
public void testGuessedKeycodeFromTyping() throws Throwable {
assertEquals(0, ImeAdapter.getTypedKeycodeGuess(null, ""));
assertEquals(KeyEvent.KEYCODE_X, ImeAdapter.getTypedKeycodeGuess(null, "x"));
assertEquals(0, ImeAdapter.getTypedKeycodeGuess(null, "xyz"));
assertEquals(0, ImeAdapter.getTypedKeycodeGuess("abc", "abc"));
assertEquals(KeyEvent.KEYCODE_DEL, ImeAdapter.getTypedKeycodeGuess("abc", ""));
assertEquals(KeyEvent.KEYCODE_H, ImeAdapter.getTypedKeycodeGuess("", "h"));
assertEquals(KeyEvent.KEYCODE_DEL, ImeAdapter.getTypedKeycodeGuess("h", ""));
assertEquals(KeyEvent.KEYCODE_E, ImeAdapter.getTypedKeycodeGuess("h", "he"));
assertEquals(KeyEvent.KEYCODE_L, ImeAdapter.getTypedKeycodeGuess("he", "hel"));
assertEquals(KeyEvent.KEYCODE_O, ImeAdapter.getTypedKeycodeGuess("hel", "helo"));
assertEquals(KeyEvent.KEYCODE_DEL, ImeAdapter.getTypedKeycodeGuess("helo", "hel"));
assertEquals(KeyEvent.KEYCODE_L, ImeAdapter.getTypedKeycodeGuess("hel", "hell"));
assertEquals(KeyEvent.KEYCODE_L, ImeAdapter.getTypedKeycodeGuess("hell", "helll"));
assertEquals(KeyEvent.KEYCODE_DEL, ImeAdapter.getTypedKeycodeGuess("helll", "hell"));
assertEquals(KeyEvent.KEYCODE_O, ImeAdapter.getTypedKeycodeGuess("hell", "hello"));
assertEquals(KeyEvent.KEYCODE_X, ImeAdapter.getTypedKeycodeGuess("xxx", "xxxx"));
assertEquals(KeyEvent.KEYCODE_X, ImeAdapter.getTypedKeycodeGuess("xxx", "xxxxx"));
assertEquals(KeyEvent.KEYCODE_DEL, ImeAdapter.getTypedKeycodeGuess("xxx", "xx"));
assertEquals(KeyEvent.KEYCODE_DEL, ImeAdapter.getTypedKeycodeGuess("xxx", "x"));
assertEquals(KeyEvent.KEYCODE_Y, ImeAdapter.getTypedKeycodeGuess("xxx", "xxxy"));
assertEquals(KeyEvent.KEYCODE_Y, ImeAdapter.getTypedKeycodeGuess("xxx", "xxxxy"));
assertEquals(0, ImeAdapter.getTypedKeycodeGuess("xxx", "xy"));
assertEquals(0, ImeAdapter.getTypedKeycodeGuess("xxx", "y"));
assertEquals(0, ImeAdapter.getTypedKeycodeGuess("foo", "bar"));
assertEquals(0, ImeAdapter.getTypedKeycodeGuess("foo", "bars"));
assertEquals(0, ImeAdapter.getTypedKeycodeGuess("foo", "ba"));
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testKeyCodesWhileComposingText() throws Throwable {
DOMUtils.focusNode(mContentViewCore, "textarea");
assertWaitForKeyboardStatus(true);
// The calls below are a reflection of what the stock Google Keyboard (Android 4.4) sends
// when the noted key is touched on screen. Exercise care when altering to make sure
// that the test reflects reality. If this test breaks, it's possible that code has
// changed and different calls need to be made instead.
mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
// H
expectUpdateStateCall(mConnection);
setComposingText(mConnection, "h", 1);
assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// O
expectUpdateStateCall(mConnection);
setComposingText(mConnection, "ho", 1);
assertEquals(KeyEvent.KEYCODE_O, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
// DEL
expectUpdateStateCall(mConnection);
setComposingText(mConnection, "h", 1);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
setComposingRegion(mConnection, 0, 1); // DEL calls cancelComposition() then restarts
// I
setComposingText(mConnection, "hi", 1);
assertEquals(KeyEvent.KEYCODE_I, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
// SPACE
commitText(mConnection, "hi", 1);
assertEquals(0, mImeAdapter.mLastSyntheticKeyCode);
commitText(mConnection, " ", 1);
assertEquals(KeyEvent.KEYCODE_SPACE, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0));
// DEL
deleteSurroundingText(mConnection, 1, 0);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
setComposingRegion(mConnection, 0, 2);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
// DEL
setComposingText(mConnection, "h", 1);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// DEL
commitText(mConnection, "", 1);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
// DEL (on empty input)
deleteSurroundingText(mConnection, 1, 0); // BS on empty still sends 1,0
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
}
@SmallTest
@Feature({"TextInput", "Main"})
public void testKeyCodesWhileTypingText() throws Throwable {
DOMUtils.focusNode(mContentViewCore, "textarea");
assertWaitForKeyboardStatus(true);
// The calls below are a reflection of what the Hacker's Keyboard sends when the noted
// key is touched on screen. Exercise care when altering to make sure that the test
// reflects reality.
mConnection = (TestAdapterInputConnection) getAdapterInputConnection();
waitAndVerifyEditableCallback(mConnection.mImeUpdateQueue, 0, "", 0, 0, -1, -1);
// H
expectUpdateStateCall(mConnection);
commitText(mConnection, "h", 1);
assertEquals(KeyEvent.KEYCODE_H, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// O
expectUpdateStateCall(mConnection);
commitText(mConnection, "o", 1);
assertEquals(KeyEvent.KEYCODE_O, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("ho", mConnection.getTextBeforeCursor(9, 0));
// DEL
expectUpdateStateCall(mConnection);
deleteSurroundingText(mConnection, 1, 0);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// I
expectUpdateStateCall(mConnection);
commitText(mConnection, "i", 1);
assertEquals(KeyEvent.KEYCODE_I, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
// SPACE
expectUpdateStateCall(mConnection);
commitText(mConnection, " ", 1);
assertEquals(KeyEvent.KEYCODE_SPACE, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("hi ", mConnection.getTextBeforeCursor(9, 0));
// DEL
expectUpdateStateCall(mConnection);
deleteSurroundingText(mConnection, 1, 0);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("hi", mConnection.getTextBeforeCursor(9, 0));
// DEL
expectUpdateStateCall(mConnection);
deleteSurroundingText(mConnection, 1, 0);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("h", mConnection.getTextBeforeCursor(9, 0));
// DEL
expectUpdateStateCall(mConnection);
deleteSurroundingText(mConnection, 1, 0);
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
assertUpdateStateCall(mConnection, 1000);
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
// DEL (on empty input)
deleteSurroundingText(mConnection, 1, 0); // BS on empty still sends 1,0
assertEquals(KeyEvent.KEYCODE_DEL, mImeAdapter.mLastSyntheticKeyCode);
assertEquals("", mConnection.getTextBeforeCursor(9, 0));
}
@SmallTest @SmallTest
@Feature({"TextInput", "Main"}) @Feature({"TextInput", "Main"})
public void testEnterKeyEventWhileComposingText() throws Throwable { public void testEnterKeyEventWhileComposingText() throws Throwable {
...@@ -327,6 +517,23 @@ public class ImeTest extends ContentShellTestBase { ...@@ -327,6 +517,23 @@ public class ImeTest extends ContentShellTestBase {
text, selectionStart, selectionEnd, compositionStart, compositionEnd); text, selectionStart, selectionEnd, compositionStart, compositionEnd);
} }
private void expectUpdateStateCall(final TestAdapterInputConnection connection) {
connection.mImeUpdateQueue.clear();
}
private void assertUpdateStateCall(final TestAdapterInputConnection connection, int maxms)
throws Exception {
while (connection.mImeUpdateQueue.size() == 0 && maxms > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
// Not really a problem since we're just going to sleep again.
}
maxms -= 50;
}
assertTrue(connection.mImeUpdateQueue.size() > 0);
}
private void assertClipboardContents(final Activity activity, final String expectedContents) private void assertClipboardContents(final Activity activity, final String expectedContents)
throws InterruptedException { throws InterruptedException {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
...@@ -449,6 +656,16 @@ public class ImeTest extends ContentShellTestBase { ...@@ -449,6 +656,16 @@ public class ImeTest extends ContentShellTestBase {
}); });
} }
private void deleteSurroundingText(final AdapterInputConnection connection, final int before,
final int after) {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
connection.deleteSurroundingText(before, after);
}
});
}
private static class TestAdapterInputConnectionFactory extends private static class TestAdapterInputConnectionFactory extends
ImeAdapter.AdapterInputConnectionFactory { ImeAdapter.AdapterInputConnectionFactory {
@Override @Override
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment