| Index: content/public/android/java/src/org/chromium/content/browser/ImeAdapter.java
|
| diff --git a/content/public/android/java/src/org/chromium/content/browser/ImeAdapter.java b/content/public/android/java/src/org/chromium/content/browser/ImeAdapter.java
|
| index 2904ea46e5588c8120cb23c388f9f8fd8e96c3da..249f6ae9cc87c3448c68c292029a4cddead24fd0 100644
|
| --- a/content/public/android/java/src/org/chromium/content/browser/ImeAdapter.java
|
| +++ b/content/public/android/java/src/org/chromium/content/browser/ImeAdapter.java
|
| @@ -4,15 +4,279 @@
|
|
|
| package org.chromium.content.browser;
|
|
|
| +import android.content.Context;
|
| +import android.os.Handler;
|
| +import android.os.ResultReceiver;
|
| +import android.text.Editable;
|
| +import android.text.InputType;
|
| +import android.text.Selection;
|
| +import android.view.KeyEvent;
|
| +import android.view.View;
|
| +import android.view.inputmethod.BaseInputConnection;
|
| +import android.view.inputmethod.EditorInfo;
|
| +import android.view.inputmethod.ExtractedText;
|
| +import android.view.inputmethod.ExtractedTextRequest;
|
| +import android.view.inputmethod.InputMethodManager;
|
| +
|
| import org.chromium.base.CalledByNative;
|
| import org.chromium.base.JNINamespace;
|
| import org.chromium.content.app.AppResource;
|
|
|
| +// We have to adapt and plumb android IME service and chrome text input API.
|
| +// ImeAdapter provides an interface in both ways native <-> java:
|
| +// 1. InputConnectionAdapter notifies native code of text composition state and
|
| +// dispatch key events from java -> WebKit.
|
| +// 2. Native ImeAdapter notifies java side to clear composition text.
|
| +//
|
| +// The basic flow is:
|
| +// 1. Intercept dispatchKeyEventPreIme() to record the current key event, but do
|
| +// nothing else.
|
| +// 2. When InputConnectionAdapter gets called with composition or result text:
|
| +// a) If a key event has been recorded in dispatchKeyEventPreIme() and we
|
| +// receive a result text with single character, then we probably need to
|
| +// send the result text as a Char event rather than a ConfirmComposition
|
| +// event. So we need to dispatch the recorded key event followed by a
|
| +// synthetic Char event.
|
| +// b) If we receive a composition text or a result text with more than one
|
| +// characters, then no matter if we recorded a key event or not in
|
| +// dispatchKeyEventPreIme(), we just need to dispatch a synthetic key
|
| +// event with special keycode 229, and then dispatch the composition or
|
| +// result text.
|
| +// 3. Intercept dispatchKeyEvent() method for key events not handled by IME, we
|
| +// need to dispatch them to webkit and check webkit's reply. Then inject a
|
| +// new key event for further processing if webkit didn't handle it.
|
| @JNINamespace("content")
|
| class ImeAdapter {
|
| + interface ViewEmbedder {
|
| + /**
|
| + * @param isFinish whether the event is occuring because input is finished.
|
| + */
|
| + public void onImeEvent(boolean isFinish);
|
| + public void onSetFieldValue();
|
| + public void onDismissInput();
|
| + public View getAttachedView();
|
| + public ResultReceiver getNewShowKeyboardReceiver();
|
| + }
|
| +
|
| + static final int COMPOSITION_KEY_CODE = 229;
|
| +
|
| + static int sEventTypeRawKeyDown;
|
| + static int sEventTypeKeyUp;
|
| + static int sEventTypeChar;
|
| + static int sTextInputTypeNone;
|
| + static int sTextInputTypeText;
|
| + static int sTextInputTypeTextArea;
|
| + static int sTextInputTypePassword;
|
| + static int sTextInputTypeSearch;
|
| + static int sTextInputTypeUrl;
|
| + static int sTextInputTypeEmail;
|
| + static int sTextInputTypeTel;
|
| + static int sTextInputTypeNumber;
|
| + static int sTextInputTypeWeek;
|
| + static int sTextInputTypeContentEditable;
|
| + static int sModifierShift;
|
| + static int sModifierAlt;
|
| + static int sModifierCtrl;
|
| + static int sModifierCapsLockOn;
|
| + static int sModifierNumLockOn;
|
| +
|
| +
|
| + @CalledByNative
|
| + static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp,
|
| + int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl,
|
| + int modifierCapsLockOn, int modifierNumLockOn) {
|
| + sEventTypeRawKeyDown = eventTypeRawKeyDown;
|
| + sEventTypeKeyUp = eventTypeKeyUp;
|
| + sEventTypeChar = eventTypeChar;
|
| + sModifierShift = modifierShift;
|
| + sModifierAlt = modifierAlt;
|
| + sModifierCtrl = modifierCtrl;
|
| + sModifierCapsLockOn = modifierCapsLockOn;
|
| + sModifierNumLockOn = modifierNumLockOn;
|
| + }
|
| +
|
| + @CalledByNative
|
| + static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText,
|
| + int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch,
|
| + int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel,
|
| + int textInputTypeNumber, int textInputTypeDate, int textInputTypeDateTime,
|
| + int textInputTypeDateTimeLocal, int textInputTypeMonth, int textInputTypeTime,
|
| + int textInputTypeWeek, int textInputTypeContentEditable) {
|
| + sTextInputTypeNone = textInputTypeNone;
|
| + sTextInputTypeText = textInputTypeText;
|
| + sTextInputTypeTextArea = textInputTypeTextArea;
|
| + sTextInputTypePassword = textInputTypePassword;
|
| + sTextInputTypeSearch = textInputTypeSearch;
|
| + sTextInputTypeUrl = textInputTypeUrl;
|
| + sTextInputTypeEmail = textInputTypeEmail;
|
| + sTextInputTypeTel = textInputTypeTel;
|
| + sTextInputTypeNumber = textInputTypeNumber;
|
| + sTextInputTypeWeek = textInputTypeWeek;
|
| + sTextInputTypeContentEditable = textInputTypeContentEditable;
|
| + InputDialogContainer.initializeInputTypes(textInputTypeDate, textInputTypeDateTime,
|
| + textInputTypeDateTimeLocal, textInputTypeMonth, textInputTypeTime);
|
| + }
|
|
|
| private int mNativeImeAdapterAndroid;
|
| private int mTextInputType;
|
| + private int mPreImeEventCount;
|
| +
|
| + private Context mContext;
|
| + private SelectionHandleController mSelectionHandleController;
|
| + private InsertionHandleController mInsertionHandleController;
|
| + private AdapterInputConnection mInputConnection;
|
| + private ViewEmbedder mViewEmbedder;
|
| + private Handler mHandler;
|
| + private InputDialogContainer mInputDialogContainer;
|
| +
|
| + private class DelayedDismissInput implements Runnable {
|
| + private int mNativeImeAdapter;
|
| +
|
| + DelayedDismissInput(int nativeImeAdapter) {
|
| + mNativeImeAdapter = nativeImeAdapter;
|
| + }
|
| +
|
| + @Override
|
| + public void run() {
|
| + attach(mNativeImeAdapter, sTextInputTypeNone);
|
| + dismissInput(true);
|
| + }
|
| + };
|
| +
|
| + private DelayedDismissInput mDismissInput = null;
|
| +
|
| + // Delay introduced to avoid hiding the keyboard if new show requests are received.
|
| + // The time required by the unfocus-focus events triggered by tab has been measured in soju:
|
| + // Mean: 18.633 ms, Standard deviation: 7.9837 ms.
|
| + // The value here should be higher enough to cover these cases, but not too high to avoid
|
| + // letting the user perceiving important delays.
|
| + private static final int INPUT_DISMISS_DELAY = 150;
|
| +
|
| + ImeAdapter(Context context, SelectionHandleController selectionHandleController,
|
| + InsertionHandleController insertionHandleController, ViewEmbedder embedder) {
|
| + mPreImeEventCount = 0;
|
| + mContext = context;
|
| + mSelectionHandleController = selectionHandleController;
|
| + mInsertionHandleController = insertionHandleController;
|
| + mViewEmbedder = embedder;
|
| + mHandler = new Handler();
|
| + mInputDialogContainer = new InputDialogContainer(context,
|
| + new InputDialogContainer.InputActionDelegate() {
|
| + public void clearFocus() {
|
| + nativeClearFocus(mNativeImeAdapterAndroid);
|
| + }
|
| + public void replaceText(String text) {
|
| + nativeReplaceText(mNativeImeAdapterAndroid, text);
|
| + mViewEmbedder.onSetFieldValue();
|
| + }
|
| + });
|
| + }
|
| +
|
| + boolean isFor(int nativeImeAdapter, int textInputType) {
|
| + return mNativeImeAdapterAndroid == nativeImeAdapter &&
|
| + mTextInputType == textInputType;
|
| + }
|
| +
|
| + void attachAndShowIfNeeded(int nativeImeAdapter, int textInputType,
|
| + String text, boolean showIfNeeded) {
|
| + mHandler.removeCallbacks(mDismissInput);
|
| +
|
| + // If current input type is none and showIfNeeded is false, IME should not be shown
|
| + // and input type should remain as none.
|
| + if (mTextInputType == sTextInputTypeNone && !showIfNeeded) {
|
| + return;
|
| + }
|
| +
|
| + if (!isFor(nativeImeAdapter, 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) {
|
| + mDismissInput = new DelayedDismissInput(nativeImeAdapter);
|
| + mHandler.postDelayed(mDismissInput, INPUT_DISMISS_DELAY);
|
| + return;
|
| + }
|
| +
|
| + int previousType = mTextInputType;
|
| + attach(nativeImeAdapter, textInputType);
|
| +
|
| + InputMethodManager manager = (InputMethodManager)
|
| + mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
| +
|
| + if (hasTextInputType()) {
|
| + manager.restartInput(mViewEmbedder.getAttachedView());
|
| + // If type has changed from dialog to text, show even if showIfNeeded is not true.
|
| + if (showIfNeeded || mInputDialogContainer.isDialogShowing()) {
|
| + showKeyboard();
|
| + }
|
| + } else if (hasDialogInputType()) {
|
| + // If type has changed from text to dialog, show even if showIfNeeded is not true.
|
| + if (showIfNeeded || isTextInputType(previousType)) {
|
| + // Make sure the keyboard is dismissed before displaying the dialog.
|
| + dismissInput(false);
|
| + mInsertionHandleController.hideAndDisallowAutomaticShowing();
|
| + mInputDialogContainer.showDialog(text, textInputType);
|
| + }
|
| + }
|
| + } else if (hasInputType()) {
|
| + if (!mInputDialogContainer.isDialogShowing() && showIfNeeded) {
|
| + if (hasDialogInputType()) {
|
| + mInsertionHandleController.hideAndDisallowAutomaticShowing();
|
| + mInputDialogContainer.showDialog(text, textInputType);
|
| + } else {
|
| + showKeyboard();
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + void attach(int nativeImeAdapter, int textInputType) {
|
| + mNativeImeAdapterAndroid = nativeImeAdapter;
|
| + mTextInputType = textInputType;
|
| + nativeAttachImeAdapter(mNativeImeAdapterAndroid);
|
| + }
|
| +
|
| + /**
|
| + * Attaches the imeAdapter to its native counterpart. This is needed to start forwarding
|
| + * keyboard events to WebKit.
|
| + * @param nativeImeAdapter The pointer to the native ImeAdapter object.
|
| + */
|
| + void attach(int nativeImeAdapter) {
|
| + mNativeImeAdapterAndroid = nativeImeAdapter;
|
| + if (nativeImeAdapter != 0) {
|
| + nativeAttachImeAdapter(mNativeImeAdapterAndroid);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Used to check whether the native counterpart of the ImeAdapter has been attached yet.
|
| + * @return Whether native ImeAdapter has been attached and its pointer is currently nonzero.
|
| + */
|
| + boolean isNativeImeAdapterAttached() {
|
| + return mNativeImeAdapterAndroid != 0;
|
| + }
|
| +
|
| + private void showKeyboard() {
|
| + mInputDialogContainer.dismissDialog();
|
| + InputMethodManager manager = (InputMethodManager)
|
| + mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
| + manager.showSoftInput(mViewEmbedder.getAttachedView(), 0,
|
| + mViewEmbedder.getNewShowKeyboardReceiver());
|
| + }
|
| +
|
| + private void dismissInput(boolean unzoomIfNeeded) {
|
| + hideKeyboard(unzoomIfNeeded);
|
| + mViewEmbedder.onDismissInput();
|
| + }
|
| +
|
| + private void hideKeyboard(boolean unzoomIfNeeded) {
|
| + InputMethodManager manager = (InputMethodManager)
|
| + mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
| + View view = mViewEmbedder.getAttachedView();
|
| + if (manager.isActive(view)) {
|
| + manager.hideSoftInputFromWindow(view.getWindowToken(), 0,
|
| + unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null);
|
| + }
|
| + }
|
|
|
| @CalledByNative
|
| void detach() {
|
| @@ -20,6 +284,170 @@ class ImeAdapter {
|
| mTextInputType = 0;
|
| }
|
|
|
| + boolean hasInputType() {
|
| + return mTextInputType != sTextInputTypeNone;
|
| + }
|
| +
|
| + static boolean isTextInputType(int type) {
|
| + return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type);
|
| + }
|
| +
|
| + boolean hasTextInputType() {
|
| + return isTextInputType(mTextInputType);
|
| + }
|
| +
|
| + boolean hasDialogInputType() {
|
| + return InputDialogContainer.isDialogInputType(mTextInputType);
|
| + }
|
| +
|
| + void dispatchKeyEventPreIme(KeyEvent event) {
|
| + // We only register that a key was pressed, but we don't actually intercept
|
| + // it.
|
| + ++mPreImeEventCount;
|
| + }
|
| +
|
| + boolean dispatchKeyEvent(KeyEvent event) {
|
| + mPreImeEventCount = 0;
|
| + return translateAndSendNativeEvents(event);
|
| + }
|
| +
|
| + void commitText() {
|
| + cancelComposition();
|
| + if (mNativeImeAdapterAndroid != 0) {
|
| + nativeCommitText(mNativeImeAdapterAndroid, "");
|
| + }
|
| + }
|
| +
|
| + @SuppressWarnings("unused")
|
| + @CalledByNative
|
| + private void cancelComposition() {
|
| + if (mInputConnection != null) {
|
| + mInputConnection.cancelComposition();
|
| + }
|
| + }
|
| +
|
| + private boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition,
|
| + boolean isCommit) {
|
| + if (mNativeImeAdapterAndroid == 0) {
|
| + return false;
|
| + }
|
| +
|
| + // Committing an empty string finishes the current composition.
|
| + boolean isFinish = text.isEmpty();
|
| + if (!isFinish) {
|
| + mSelectionHandleController.hideAndDisallowAutomaticShowing();
|
| + mInsertionHandleController.hideAndDisallowAutomaticShowing();
|
| + }
|
| + mViewEmbedder.onImeEvent(isFinish);
|
| + boolean hasSingleChar = mPreImeEventCount == 1 && text.length() == 1;
|
| + int keyCode = hasSingleChar ? text.codePointAt(0) : COMPOSITION_KEY_CODE;
|
| + int keyChar = hasSingleChar ? text.codePointAt(0) : 0;
|
| + long timeStampMs = System.currentTimeMillis();
|
| + nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
|
| + timeStampMs, keyCode, keyChar);
|
| + if (hasSingleChar) {
|
| + nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeChar,
|
| + timeStampMs, text.codePointAt(0), text.codePointAt(0));
|
| + } else {
|
| + if (isCommit) {
|
| + nativeCommitText(mNativeImeAdapterAndroid, text);
|
| + } else {
|
| + nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition);
|
| + }
|
| + }
|
| + nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
|
| + timeStampMs, keyCode, keyChar);
|
| + mPreImeEventCount = 0;
|
| + return true;
|
| + }
|
| +
|
| + private boolean translateAndSendNativeEvents(KeyEvent event) {
|
| + if (mNativeImeAdapterAndroid == 0) {
|
| + return false;
|
| + }
|
| + int action = event.getAction();
|
| + if (action != KeyEvent.ACTION_DOWN &&
|
| + action != KeyEvent.ACTION_UP) {
|
| + // action == KeyEvent.ACTION_MULTIPLE
|
| + // TODO(bulach): confirm the actual behavior. Apparently:
|
| + // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a
|
| + // composition key down (229) followed by a commit text with the
|
| + // string from event.getUnicodeChars().
|
| + // Otherwise, we'd need to send an event with a
|
| + // WebInputEvent::IsAutoRepeat modifier. We also need to verify when
|
| + // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN,
|
| + // and if that's the case, we'll need to review when to send the Char
|
| + // event.
|
| + return false;
|
| + }
|
| + mViewEmbedder.onImeEvent(false);
|
| + return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
|
| + getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
|
| + event.isSystem(), event.getUnicodeChar());
|
| + }
|
| +
|
| + private void setInputConnection(AdapterInputConnection inputConnection) {
|
| + mInputConnection = inputConnection;
|
| + }
|
| +
|
| + private static int getModifiers(int metaState) {
|
| + int modifiers = 0;
|
| + if ((metaState & KeyEvent.META_SHIFT_ON) != 0) {
|
| + modifiers |= sModifierShift;
|
| + }
|
| + if ((metaState & KeyEvent.META_ALT_ON) != 0) {
|
| + modifiers |= sModifierAlt;
|
| + }
|
| + if ((metaState & KeyEvent.META_CTRL_ON) != 0) {
|
| + modifiers |= sModifierCtrl;
|
| + }
|
| + if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) {
|
| + modifiers |= sModifierCapsLockOn;
|
| + }
|
| + if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) {
|
| + modifiers |= sModifierNumLockOn;
|
| + }
|
| + return modifiers;
|
| + }
|
| +
|
| + boolean isActive() {
|
| + return mInputConnection != null && mInputConnection.isActive();
|
| + }
|
| +
|
| + private boolean sendSyntheticKeyEvent(
|
| + int eventType, long timestampMs, int keyCode, int unicodeChar) {
|
| + if (mNativeImeAdapterAndroid == 0) {
|
| + return false;
|
| + }
|
| + nativeSendSyntheticKeyEvent(
|
| + mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
|
| + return true;
|
| + }
|
| +
|
| + private boolean deleteSurroundingText(int leftLength, int rightLength) {
|
| + if (mNativeImeAdapterAndroid == 0) {
|
| + return false;
|
| + }
|
| + nativeDeleteSurroundingText(mNativeImeAdapterAndroid, leftLength, rightLength);
|
| + return true;
|
| + }
|
| +
|
| + private boolean setEditableSelectionOffsets(int start, int end) {
|
| + if (mNativeImeAdapterAndroid == 0) {
|
| + return false;
|
| + }
|
| + nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
|
| + return true;
|
| + }
|
| +
|
| + private boolean setComposingRegion(int start, int end) {
|
| + if (mNativeImeAdapterAndroid == 0) {
|
| + return false;
|
| + }
|
| + nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
|
| + return true;
|
| + }
|
| +
|
| boolean unselect() {
|
| if (mNativeImeAdapterAndroid == 0) {
|
| return false;
|
| @@ -60,9 +488,357 @@ class ImeAdapter {
|
| return true;
|
| }
|
|
|
| + // This InputConnection is created by ContentView.onCreateInputConnection.
|
| + // It then adapts android's IME to chrome's RenderWidgetHostView using the
|
| + // native ImeAdapterAndroid via the outer class ImeAdapter.
|
| + static public class AdapterInputConnection extends BaseInputConnection {
|
| + private View mInternalView;
|
| + private ImeAdapter mImeAdapter;
|
| + private Editable mEditable;
|
| + private boolean mSingleLine;
|
| + private int numBatchEdits;
|
| + private boolean shouldUpdateImeSelection;
|
| +
|
| + // Factory function.
|
| + static public AdapterInputConnection getInstance(View view, ImeAdapter imeAdapter,
|
| + EditorInfo outAttrs) {
|
| + return new AdapterInputConnection(view, imeAdapter, outAttrs);
|
| + }
|
| +
|
| + /**
|
| + * Updates the AdapterInputConnection's internal representation of the text
|
| + * being edited and its selection and composition properties. The resulting
|
| + * Editable is accessible through the getEditable() method.
|
| + * If the text has not changed, this also calls updateSelection on the InputMethodManager.
|
| + * @param text The String contents of the field being edited
|
| + * @param selectionStart The character offset of the selection start, or the caret
|
| + * position if there is no selection
|
| + * @param selectionEnd The character offset of the selection end, or the caret
|
| + * position if there is no selection
|
| + * @param compositionStart The character offset of the composition start, or -1
|
| + * if there is no composition
|
| + * @param compositionEnd The character offset of the composition end, or -1
|
| + * if there is no selection
|
| + */
|
| + public void setEditableText(String text, int selectionStart, int selectionEnd,
|
| + int compositionStart, int compositionEnd) {
|
| +
|
| + if (mEditable == null) {
|
| + mEditable = Editable.Factory.getInstance().newEditable("");
|
| + }
|
| +
|
| + int prevSelectionStart = Selection.getSelectionStart(mEditable);
|
| + int prevSelectionEnd = Selection.getSelectionEnd(mEditable);
|
| + int prevEditableLength = mEditable.length();
|
| + int prevCompositionStart = getComposingSpanStart(mEditable);
|
| + int prevCompositionEnd = getComposingSpanEnd(mEditable);
|
| + String prevText = mEditable.toString();
|
| +
|
| + selectionStart = Math.min(selectionStart, text.length());
|
| + selectionEnd = Math.min(selectionEnd, text.length());
|
| + compositionStart = Math.min(compositionStart, text.length());
|
| + compositionEnd = Math.min(compositionEnd, text.length());
|
| +
|
| + boolean textUnchanged = prevText.equals(text);
|
| +
|
| + if (textUnchanged
|
| + && prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
|
| + && prevCompositionStart == compositionStart
|
| + && prevCompositionEnd == compositionEnd) {
|
| + // Nothing has changed; don't need to do anything
|
| + return;
|
| + }
|
| +
|
| + // When a programmatic change has been made to the editable field, both the start
|
| + // and end positions for the composition will equal zero. In this case we cancel the
|
| + // active composition in the editor as this no longer is relevant.
|
| + if (textUnchanged && compositionStart == 0 && compositionEnd == 0) {
|
| + cancelComposition();
|
| + }
|
| +
|
| + if (!textUnchanged) {
|
| + mEditable.replace(0, mEditable.length(), text);
|
| + }
|
| + Selection.setSelection(mEditable, selectionStart, selectionEnd);
|
| + super.setComposingRegion(compositionStart, compositionEnd);
|
| +
|
| + if (textUnchanged || prevText.equals("")) {
|
| + // updateSelection should be called when a manual selection change occurs.
|
| + // Should not be called if text is being entered else issues can occur
|
| + // e.g. backspace to undo autocorrection will not work with the default OSK.
|
| + getInputMethodManager().updateSelection(mInternalView,
|
| + selectionStart, selectionEnd, compositionStart, compositionEnd);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public Editable getEditable() {
|
| + if (mEditable == null) {
|
| + mEditable = Editable.Factory.getInstance().newEditable("");
|
| + Selection.setSelection(mEditable, 0);
|
| + }
|
| + return mEditable;
|
| + }
|
| +
|
| + @Override
|
| + public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
| + super.setComposingText(text, newCursorPosition);
|
| + return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
|
| + newCursorPosition, false);
|
| + }
|
| +
|
| + @Override
|
| + public boolean commitText(CharSequence text, int newCursorPosition) {
|
| + super.commitText(text, newCursorPosition);
|
| + shouldUpdateImeSelection = true;
|
| + return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
|
| + newCursorPosition, true);
|
| + }
|
| +
|
| + @Override
|
| + public boolean performEditorAction(int actionCode) {
|
| + switch (actionCode) {
|
| + case EditorInfo.IME_ACTION_NEXT:
|
| + cancelComposition();
|
| + // Send TAB key event
|
| + long timeStampMs = System.currentTimeMillis();
|
| + mImeAdapter.sendSyntheticKeyEvent(
|
| + sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
|
| + return true;
|
| + case EditorInfo.IME_ACTION_GO:
|
| + case EditorInfo.IME_ACTION_SEARCH:
|
| + mImeAdapter.dismissInput(true);
|
| + break;
|
| + }
|
| +
|
| + return super.performEditorAction(actionCode);
|
| + }
|
| +
|
| + @Override
|
| + public boolean performContextMenuAction(int id) {
|
| + switch (id) {
|
| + case android.R.id.selectAll:
|
| + return mImeAdapter.selectAll();
|
| + case android.R.id.cut:
|
| + return mImeAdapter.cut();
|
| + case android.R.id.copy:
|
| + return mImeAdapter.copy();
|
| + case android.R.id.paste:
|
| + return mImeAdapter.paste();
|
| + default:
|
| + return false;
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
| + ExtractedText et = new ExtractedText();
|
| + if (mEditable == null) {
|
| + et.text = "";
|
| + } else {
|
| + et.text = mEditable.toString();
|
| + et.partialEndOffset = mEditable.length();
|
| + et.selectionStart = Selection.getSelectionStart(mEditable);
|
| + et.selectionEnd = Selection.getSelectionEnd(mEditable);
|
| + }
|
| + et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
|
| + return et;
|
| + }
|
| +
|
| + @Override
|
| + public boolean deleteSurroundingText(int leftLength, int rightLength) {
|
| + if (!super.deleteSurroundingText(leftLength, rightLength)) {
|
| + return false;
|
| + }
|
| + shouldUpdateImeSelection = true;
|
| + return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
|
| + }
|
| +
|
| + @Override
|
| + public boolean sendKeyEvent(KeyEvent event) {
|
| + mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing();
|
| + mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing();
|
| +
|
| + // If this is a key-up, and backspace/del or if the key has a character representation,
|
| + // need to update the underlying Editable (i.e. the local representation of the text
|
| + // being edited).
|
| + // Note that setEditableText will still be called asynchronously as usual, but
|
| + // it'll be a no-op as the internal state will be already updated.
|
| + if (event.getAction() == KeyEvent.ACTION_UP) {
|
| + if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
|
| + super.deleteSurroundingText(1, 0);
|
| + } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
|
| + super.deleteSurroundingText(0, 1);
|
| + } else {
|
| + int unicodeChar = event.getUnicodeChar();
|
| + if (unicodeChar != 0) {
|
| + Editable editable = getEditable();
|
| + int selectionStart = Selection.getSelectionStart(editable);
|
| + int selectionEnd = Selection.getSelectionEnd(editable);
|
| + if (selectionStart > selectionEnd) {
|
| + int temp = selectionStart;
|
| + selectionStart = selectionEnd;
|
| + selectionEnd = temp;
|
| + }
|
| + editable.replace(selectionStart, selectionEnd,
|
| + Character.toString((char)unicodeChar));
|
| + }
|
| + }
|
| + }
|
| +
|
| + return super.sendKeyEvent(event);
|
| + }
|
| +
|
| + @Override
|
| + public boolean finishComposingText() {
|
| + if (mEditable == null
|
| + || (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable))) {
|
| + return true;
|
| + }
|
| + super.finishComposingText();
|
| + return commitText("", 0);
|
| + }
|
| +
|
| + @Override
|
| + public boolean setSelection(int start, int end) {
|
| + if (start < 0 || end < 0) return true;
|
| + super.setSelection(start, end);
|
| + return mImeAdapter.setEditableSelectionOffsets(start, end);
|
| + }
|
| +
|
| + /**
|
| + * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there
|
| + * is no longer a current composition. Note this differs from finishComposingText, which
|
| + * is called by the IME when it wants to end a composition.
|
| + */
|
| + void cancelComposition() {
|
| + getInputMethodManager().restartInput(mInternalView);
|
| + }
|
| +
|
| + @Override
|
| + public boolean setComposingRegion(int start, int end) {
|
| + int a = Math.min(start, end);
|
| + int b = Math.max(start, end);
|
| + super.setComposingRegion(a, b);
|
| + return mImeAdapter.setComposingRegion(a, b);
|
| + }
|
| +
|
| + boolean isActive() {
|
| + return getInputMethodManager().isActive();
|
| + }
|
| +
|
| + private InputMethodManager getInputMethodManager() {
|
| + return (InputMethodManager) mInternalView.getContext()
|
| + .getSystemService(Context.INPUT_METHOD_SERVICE);
|
| + }
|
| +
|
| + private void updateImeSelection() {
|
| + if (mEditable != null) {
|
| + getInputMethodManager().updateSelection(mInternalView,
|
| + Selection.getSelectionStart(mEditable),
|
| + Selection.getSelectionEnd(mEditable),
|
| + getComposingSpanStart(mEditable),
|
| + getComposingSpanEnd(mEditable));
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public boolean beginBatchEdit() {
|
| + ++numBatchEdits;
|
| + return false;
|
| + }
|
| +
|
| + @Override
|
| + public boolean endBatchEdit() {
|
| + if (--numBatchEdits == 0 && shouldUpdateImeSelection) {
|
| + updateImeSelection();
|
| + shouldUpdateImeSelection = false;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + private AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
|
| + super(view, true);
|
| + mInternalView = view;
|
| + mImeAdapter = imeAdapter;
|
| + mImeAdapter.setInputConnection(this);
|
| + mSingleLine = true;
|
| + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
| + outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
|
| + | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
|
| + if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeText) {
|
| + // Normal text field
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
|
| + } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTextArea ||
|
| + imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeContentEditable) {
|
| + // TextArea or contenteditable.
|
| + outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
|
| + | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
|
| + | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
|
| + mSingleLine = false;
|
| + } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypePassword) {
|
| + // Password
|
| + outAttrs.inputType = InputType.TYPE_CLASS_TEXT
|
| + | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
|
| + } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeSearch) {
|
| + // Search
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
|
| + } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeUrl) {
|
| + // Url
|
| + // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
|
| + // exclude it for now.
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
|
| + } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeEmail) {
|
| + // Email
|
| + outAttrs.inputType = InputType.TYPE_CLASS_TEXT
|
| + | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
|
| + } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTel) {
|
| + // Telephone
|
| + // Number and telephone do not have both a Tab key and an
|
| + // action in default OSK, so set the action to NEXT
|
| + outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
|
| + } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeNumber) {
|
| + // Number
|
| + outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
|
| + | InputType.TYPE_NUMBER_VARIATION_NORMAL;
|
| + outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
|
| + }
|
| + }
|
| + }
|
| +
|
| + private native boolean nativeSendSyntheticKeyEvent(int nativeImeAdapterAndroid,
|
| + int eventType, long timestampMs, int keyCode, int unicodeChar);
|
| +
|
| + private native boolean nativeSendKeyEvent(int nativeImeAdapterAndroid, KeyEvent event,
|
| + int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
|
| + int unicodeChar);
|
| +
|
| + private native void nativeSetComposingText(int nativeImeAdapterAndroid, String text,
|
| + int newCursorPosition);
|
| +
|
| + private native void nativeCommitText(int nativeImeAdapterAndroid, String text);
|
| +
|
| + private native void nativeAttachImeAdapter(int nativeImeAdapterAndroid);
|
| +
|
| + private native void nativeReplaceText(int nativeImeAdapterAndroid, String text);
|
| +
|
| + private native void nativeClearFocus(int nativeImeAdapterAndroid);
|
| +
|
| + private native void nativeSetEditableSelectionOffsets(int nativeImeAdapterAndroid,
|
| + int start, int end);
|
| +
|
| + private native void nativeSetComposingRegion(int nativeImeAdapterAndroid, int start, int end);
|
| +
|
| + private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid,
|
| + int before, int after);
|
| +
|
| private native void nativeUnselect(int nativeImeAdapterAndroid);
|
| private native void nativeSelectAll(int nativeImeAdapterAndroid);
|
| private native void nativeCut(int nativeImeAdapterAndroid);
|
| private native void nativeCopy(int nativeImeAdapterAndroid);
|
| private native void nativePaste(int nativeImeAdapterAndroid);
|
| -}
|
| +}
|
|
|