import android.os.Handler;
import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.CharacterStyle;
+import android.text.style.UnderlineSpan;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
-import com.google.common.annotations.VisibleForTesting;
-
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.ui.picker.InputDialogContainer;
+
+import java.lang.CharSequence;
/**
* Adapts and plumbs android IME service onto the chrome text input API.
*/
@JNINamespace("content")
public class ImeAdapter {
+
/**
* Interface for the delegate that needs to be notified of IME changes.
*/
public interface ImeAdapterDelegate {
/**
- * @param isFinish whether the event is occurring because input is finished.
+ * Called to notify the delegate about synthetic/real key events before sending to renderer.
+ */
+ void onImeEvent();
+
+ /**
+ * Called when a request to hide the keyboard is sent to InputMethodManager.
*/
- void onImeEvent(boolean isFinish);
- void onSetFieldValue();
void onDismissInput();
+
+ /**
+ * @return View that the keyboard should be attached to.
+ */
View getAttachedView();
+
+ /**
+ * @return Object that should be called for all keyboard show and hide requests.
+ */
ResultReceiver getNewShowKeyboardReceiver();
}
private class DelayedDismissInput implements Runnable {
- private final long mNativeImeAdapter;
+ private long mNativeImeAdapter;
DelayedDismissInput(long nativeImeAdapter) {
mNativeImeAdapter = nativeImeAdapter;
}
+ // http://crbug.com/413744
+ void detach() {
+ mNativeImeAdapter = 0;
+ }
+
@Override
public void run() {
- attach(mNativeImeAdapter, sTextInputTypeNone, AdapterInputConnection.INVALID_SELECTION,
- AdapterInputConnection.INVALID_SELECTION);
+ if (mNativeImeAdapter != 0) {
+ attach(mNativeImeAdapter, sTextInputTypeNone, sTextInputFlagNone);
+ }
dismissInput(true);
}
}
static int sTextInputTypeTel;
static int sTextInputTypeNumber;
static int sTextInputTypeContentEditable;
+ static int sTextInputFlagNone = 0;
+ static int sTextInputFlagAutocompleteOn;
+ static int sTextInputFlagAutocompleteOff;
+ static int sTextInputFlagAutocorrectOn;
+ static int sTextInputFlagAutocorrectOff;
+ static int sTextInputFlagSpellcheckOn;
+ static int sTextInputFlagSpellcheckOff;
static int sModifierShift;
static int sModifierAlt;
static int sModifierCtrl;
static int sModifierCapsLockOn;
static int sModifierNumLockOn;
+ static char[] sSingleCharArray = new char[1];
+ static KeyCharacterMap sKeyCharacterMap;
private long mNativeImeAdapterAndroid;
private InputMethodManagerWrapper mInputMethodManagerWrapper;
private final Handler mHandler;
private DelayedDismissInput mDismissInput = null;
private int mTextInputType;
- private int mInitialSelectionStart;
- private int mInitialSelectionEnd;
+ private int mTextInputFlags;
+ private String mLastComposeText;
+
+ @VisibleForTesting
+ int mLastSyntheticKeyCode;
@VisibleForTesting
boolean mIsShowWithoutHideOutstanding = false;
* Default factory for AdapterInputConnection classes.
*/
public static class AdapterInputConnectionFactory {
- public AdapterInputConnection get(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
- return new AdapterInputConnection(view, imeAdapter, outAttrs);
+ public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
+ Editable editable, EditorInfo outAttrs) {
+ return new AdapterInputConnection(view, imeAdapter, editable, outAttrs);
}
}
+ /**
+ * Overrides the InputMethodManagerWrapper that ImeAdapter uses to make calls to
+ * InputMethodManager.
+ * @param immw InputMethodManagerWrapper that should be used to call InputMethodManager.
+ */
@VisibleForTesting
public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) {
mInputMethodManagerWrapper = immw;
*/
void setInputConnection(AdapterInputConnection inputConnection) {
mInputConnection = inputConnection;
+ mLastComposeText = null;
}
/**
- * Should be only used by AdapterInputConnection.
+ * Should be used only by AdapterInputConnection.
* @return The input type of currently focused element.
*/
int getTextInputType() {
}
/**
- * Should be only used by AdapterInputConnection.
- * @return The starting index of the initial text selection.
+ * Should be used only by AdapterInputConnection.
+ * @return The input flags of the currently focused element.
*/
- int getInitialSelectionStart() {
- return mInitialSelectionStart;
+ int getTextInputFlags() {
+ return mTextInputFlags;
}
/**
- * Should be only used by AdapterInputConnection.
- * @return The ending index of the initial text selection.
+ * @return Constant representing that a focused node is not an input field.
*/
- int getInitialSelectionEnd() {
- return mInitialSelectionEnd;
- }
-
public static int getTextInputTypeNone() {
return sTextInputTypeNone;
}
return modifiers;
}
- public boolean isActive() {
- return mInputConnection != null && mInputConnection.isActive();
- }
-
- private boolean isFor(int nativeImeAdapter, int textInputType) {
- return mNativeImeAdapterAndroid == nativeImeAdapter &&
- mTextInputType == textInputType;
- }
-
- public void attachAndShowIfNeeded(int nativeImeAdapter, int textInputType,
- int selectionStart, int selectionEnd, boolean showIfNeeded) {
+ /**
+ * Shows or hides the keyboard based on passed parameters.
+ * @param nativeImeAdapter Pointer to the ImeAdapterAndroid object that is sending the update.
+ * @param textInputType Text input type for the currently focused field in renderer.
+ * @param showIfNeeded Whether the keyboard should be shown if it is currently hidden.
+ */
+ public void updateKeyboardVisibility(long nativeImeAdapter, int textInputType,
+ int textInputFlags, boolean showIfNeeded) {
mHandler.removeCallbacks(mDismissInput);
// If current input type is none and showIfNeeded is false, IME should not be shown
return;
}
- if (!isFor(nativeImeAdapter, textInputType)) {
+ if (mNativeImeAdapterAndroid != nativeImeAdapter || mTextInputType != textInputType) {
// Set a delayed task to perform unfocus. This avoids hiding the keyboard when tabbing
// through text inputs or when JS rapidly changes focus to another text element.
if (textInputType == sTextInputTypeNone) {
return;
}
- attach(nativeImeAdapter, textInputType, selectionStart, selectionEnd);
+ attach(nativeImeAdapter, textInputType, textInputFlags);
mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView());
if (showIfNeeded) {
}
}
- public void attach(long nativeImeAdapter, int textInputType, int selectionStart,
- int selectionEnd) {
+ public void attach(long nativeImeAdapter, int textInputType, int textInputFlags) {
if (mNativeImeAdapterAndroid != 0) {
nativeResetImeAdapter(mNativeImeAdapterAndroid);
}
mNativeImeAdapterAndroid = nativeImeAdapter;
mTextInputType = textInputType;
- mInitialSelectionStart = selectionStart;
- mInitialSelectionEnd = selectionEnd;
+ mTextInputFlags = textInputFlags;
+ mLastComposeText = null;
if (nativeImeAdapter != 0) {
nativeAttachImeAdapter(mNativeImeAdapterAndroid);
}
+ if (mTextInputType == sTextInputTypeNone) {
+ dismissInput(false);
+ }
}
/**
* @param nativeImeAdapter The pointer to the native ImeAdapter object.
*/
public void attach(long nativeImeAdapter) {
- if (mNativeImeAdapterAndroid != 0) {
- nativeResetImeAdapter(mNativeImeAdapterAndroid);
- }
- mNativeImeAdapterAndroid = nativeImeAdapter;
- if (nativeImeAdapter != 0) {
- nativeAttachImeAdapter(mNativeImeAdapterAndroid);
- }
+ attach(nativeImeAdapter, sTextInputTypeNone, sTextInputFlagNone);
}
private void showKeyboard() {
return isTextInputType(mTextInputType);
}
+ /**
+ * @return true if the selected text is of password.
+ */
+ public boolean isSelectionPassword() {
+ return mTextInputType == sTextInputTypePassword;
+ }
+
public boolean dispatchKeyEvent(KeyEvent event) {
return translateAndSendNativeEvents(event);
}
else return COMPOSITION_KEY_CODE;
}
+ /**
+ * @return Android KeyEvent for a single unicode character. Only one KeyEvent is returned
+ * even if the system determined that various modifier keys (like Shift) would also have
+ * been pressed.
+ */
+ private static KeyEvent androidKeyEventForCharacter(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) { // No known key sequence will create that character.
+ return null;
+ }
+
+ for (int i = 0; i < events.length; ++i) {
+ if (events[i].getAction() == KeyEvent.ACTION_DOWN &&
+ !KeyEvent.isModifierKey(events[i].getKeyCode())) {
+ return events[i];
+ }
+ }
+
+ return null; // No printing characters were found.
+ }
+
+ @VisibleForTesting
+ public static KeyEvent getTypedKeyEventGuess(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 androidKeyEventForCharacter(newtext.charAt(0));
+ } else {
+ return null;
+ }
+ }
+
+ // 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 androidKeyEventForCharacter(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 new KeyEvent(KeyEvent.ACTION_DOWN, 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 null;
+ }
+
void sendKeyEventWithKeyCode(int keyCode, int flags) {
- long eventTime = System.currentTimeMillis();
+ long eventTime = SystemClock.uptimeMillis();
+ mLastSyntheticKeyCode = keyCode;
translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags));
- translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), eventTime,
+ translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
KeyEvent.ACTION_UP, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags));
}
// Calls from Java to C++
+ // TODO: Add performance tracing to more complicated functions.
- boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition,
+ boolean checkCompositionQueueAndCallNative(CharSequence text, int newCursorPosition,
boolean isCommit) {
if (mNativeImeAdapterAndroid == 0) return false;
+ mViewEmbedder.onImeEvent();
- // Committing an empty string finishes the current composition.
- boolean isFinish = text.isEmpty();
- mViewEmbedder.onImeEvent(isFinish);
- int keyCode = shouldSendKeyEventWithKeyCode(text);
- long timeStampMs = System.currentTimeMillis();
+ String textStr = text.toString();
+ int keyCode = shouldSendKeyEventWithKeyCode(textStr);
+ long timeStampMs = SystemClock.uptimeMillis();
if (keyCode != COMPOSITION_KEY_CODE) {
sendKeyEventWithKeyCode(keyCode,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
} else {
- nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
- timeStampMs, keyCode, 0);
+ KeyEvent keyEvent = getTypedKeyEventGuess(mLastComposeText, textStr);
+ int modifiers = 0;
+ if (keyEvent != null) {
+ keyCode = keyEvent.getKeyCode();
+ modifiers = getModifiers(keyEvent.getMetaState());
+ } else if (!textStr.equals(mLastComposeText)) {
+ keyCode = KeyEvent.KEYCODE_UNKNOWN;
+ } else {
+ keyCode = -1;
+ }
+
+ // If this is a commit with no previous composition, then treat it as a native
+ // KeyDown/KeyUp pair with no composition rather than a synthetic pair with
+ // composition below.
+ if (keyCode > 0 && isCommit && mLastComposeText == null) {
+ mLastSyntheticKeyCode = keyCode;
+ return translateAndSendNativeEvents(keyEvent) &&
+ translateAndSendNativeEvents(KeyEvent.changeAction(
+ keyEvent, KeyEvent.ACTION_UP));
+ }
+
+ // 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
+
+ if (keyCode >= 0) {
+ nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
+ timeStampMs, keyCode, modifiers, 0);
+ }
+
if (isCommit) {
- nativeCommitText(mNativeImeAdapterAndroid, text);
+ nativeCommitText(mNativeImeAdapterAndroid, textStr);
+ textStr = null;
} else {
- nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition);
+ nativeSetComposingText(mNativeImeAdapterAndroid, text, textStr, newCursorPosition);
+ }
+
+ if (keyCode >= 0) {
+ nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
+ timeStampMs, keyCode, modifiers, 0);
}
- nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
- timeStampMs, keyCode, 0);
+
+ mLastSyntheticKeyCode = keyCode;
}
+ mLastComposeText = textStr;
return true;
}
void finishComposingText() {
+ mLastComposeText = null;
if (mNativeImeAdapterAndroid == 0) return;
nativeFinishComposingText(mNativeImeAdapterAndroid);
}
// event.
return false;
}
- mViewEmbedder.onImeEvent(false);
+ mViewEmbedder.onImeEvent();
return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
- event.isSystem(), event.getUnicodeChar());
+ /*isSystemKey=*/false, event.getUnicodeChar());
}
- boolean sendSyntheticKeyEvent(
- int eventType, long timestampMs, int keyCode, int unicodeChar) {
+ boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int modifiers,
+ int unicodeChar) {
if (mNativeImeAdapterAndroid == 0) return false;
nativeSendSyntheticKeyEvent(
- mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
+ mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, modifiers, unicodeChar);
return true;
}
+ /**
+ * Send a request to the native counterpart to delete a given range of characters.
+ * @param beforeLength Number of characters to extend the selection by before the existing
+ * selection.
+ * @param afterLength Number of characters to extend the selection by after the existing
+ * selection.
+ * @return Whether the native counterpart of ImeAdapter received the call.
+ */
boolean deleteSurroundingText(int beforeLength, int afterLength) {
+ mViewEmbedder.onImeEvent();
if (mNativeImeAdapterAndroid == 0) return false;
nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength);
return true;
}
+ /**
+ * Send a request to the native counterpart to set the selection to given range.
+ * @param start Selection start index.
+ * @param end Selection end index.
+ * @return Whether the native counterpart of ImeAdapter received the call.
+ */
boolean setEditableSelectionOffsets(int start, int end) {
if (mNativeImeAdapterAndroid == 0) return false;
nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
}
/**
- * 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 end The end of the composition.
* @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;
nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
+ mLastComposeText = text != null ? text.toString() : null;
return true;
}
}
@CalledByNative
+ private static void initializeTextInputFlags(
+ int textInputFlagAutocompleteOn, int textInputFlagAutocompleteOff,
+ int textInputFlagAutocorrectOn, int textInputFlagAutocorrectOff,
+ int textInputFlagSpellcheckOn, int textInputFlagSpellcheckOff) {
+ sTextInputFlagAutocompleteOn = textInputFlagAutocompleteOn;
+ sTextInputFlagAutocompleteOff = textInputFlagAutocompleteOff;
+ sTextInputFlagAutocorrectOn = textInputFlagAutocorrectOn;
+ sTextInputFlagAutocorrectOff = textInputFlagAutocorrectOff;
+ sTextInputFlagSpellcheckOn = textInputFlagSpellcheckOn;
+ sTextInputFlagSpellcheckOff = textInputFlagSpellcheckOff;
+ }
+
+ @CalledByNative
private void focusedNodeChanged(boolean isEditable) {
if (mInputConnection != null && isEditable) mInputConnection.restartInput();
}
@CalledByNative
+ private void populateUnderlinesFromSpans(CharSequence text, long underlines) {
+ if (!(text instanceof SpannableString)) return;
+
+ SpannableString spannableString = ((SpannableString) text);
+ CharacterStyle spans[] =
+ spannableString.getSpans(0, text.length(), CharacterStyle.class);
+ for (CharacterStyle span : spans) {
+ if (span instanceof BackgroundColorSpan) {
+ nativeAppendBackgroundColorSpan(underlines, spannableString.getSpanStart(span),
+ spannableString.getSpanEnd(span),
+ ((BackgroundColorSpan) span).getBackgroundColor());
+ } else if (span instanceof UnderlineSpan) {
+ nativeAppendUnderlineSpan(underlines, spannableString.getSpanStart(span),
+ spannableString.getSpanEnd(span));
+ }
+ }
+ }
+
+ @CalledByNative
private void cancelComposition() {
if (mInputConnection != null) mInputConnection.restartInput();
+ mLastComposeText = null;
}
@CalledByNative
void detach() {
- if (mDismissInput != null) mHandler.removeCallbacks(mDismissInput);
+ if (mDismissInput != null) {
+ mHandler.removeCallbacks(mDismissInput);
+ mDismissInput.detach();
+ }
mNativeImeAdapterAndroid = 0;
mTextInputType = 0;
}
private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid,
- int eventType, long timestampMs, int keyCode, int unicodeChar);
+ int eventType, long timestampMs, int keyCode, int modifiers, int unicodeChar);
private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
int unicodeChar);
- private native void nativeSetComposingText(long nativeImeAdapterAndroid, String text,
- int newCursorPosition);
+ private static native void nativeAppendUnderlineSpan(long underlinePtr, int start, int end);
+
+ private static native void nativeAppendBackgroundColorSpan(long underlinePtr, int start,
+ int end, int backgroundColor);
+
+ private native void nativeSetComposingText(long nativeImeAdapterAndroid, CharSequence text,
+ String textStr, int newCursorPosition);
- private native void nativeCommitText(long nativeImeAdapterAndroid, String text);
+ private native void nativeCommitText(long nativeImeAdapterAndroid, String textStr);
private native void nativeFinishComposingText(long nativeImeAdapterAndroid);