/** @see BaseInputConnection#performContextMenuAction(int) */ @Override public boolean performContextMenuAction(int id) { if (DEBUG) Log.w(TAG, "performContextMenuAction [" + 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; } }
/** @see BaseInputConnection#setComposingRegion(int, int) */ @Override public boolean setComposingRegion(int start, int end) { if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); int textLength = mEditable.length(); int a = Math.min(start, end); int b = Math.max(start, end); if (a < 0) a = 0; if (b < 0) b = 0; if (a > textLength) a = textLength; if (b > textLength) b = textLength; CharSequence regionText = null; if (a == b) { removeComposingSpans(mEditable); } else { if (a == 0 && b == mEditable.length()) { regionText = mEditable.subSequence(a, b); // If setting composing region that matches, at least in length, of the entire // editable region then check it for image placeholders. If any are found, // don't continue this operation. // This fixes the problem where, on Android 4.3, pasting an image is followed // by setting the composing region which then causes the image to be deleted. // http://crbug.com/466755 for (int i = a; i < b; ++i) { if (regionText.charAt(i) == '\uFFFC') return true; } } super.setComposingRegion(a, b); } updateSelectionIfRequired(); return mImeAdapter.setComposingRegion(regionText, a, b); }
/** @see BaseInputConnection#setSelection(int, int) */ @Override public boolean setSelection(int start, int end) { if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]"); int textLength = mEditable.length(); if (start < 0 || end < 0 || start > textLength || end > textLength) return true; super.setSelection(start, end); updateSelectionIfRequired(); return mImeAdapter.setEditableSelectionOffsets(start, end); }
/** @see BaseInputConnection#performEditorAction(int) */ @Override public boolean performEditorAction(int actionCode) { if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]"); if (actionCode == EditorInfo.IME_ACTION_NEXT) { restartInput(); // Send TAB key event long timeStampMs = SystemClock.uptimeMillis(); mImeAdapter.sendSyntheticKeyEvent( WebInputEventType.RawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0, 0); } else { mImeAdapter.sendKeyEventWithKeyCode( KeyEvent.KEYCODE_ENTER, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION); } return true; }
/** @see BaseInputConnection#setComposingText(java.lang.CharSequence, int) */ @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]"); if (maybePerformEmptyCompositionWorkaround(text)) return true; mPendingAccent = 0; super.setComposingText(text, newCursorPosition); updateSelectionIfRequired(); return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition, false); }
/** @see BaseInputConnection#finishComposingText() */ @Override public boolean finishComposingText() { if (DEBUG) Log.w(TAG, "finishComposingText"); mPendingAccent = 0; if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) { return true; } super.finishComposingText(); updateSelectionIfRequired(); mImeAdapter.finishComposingText(); return true; }
@SmallTest @Feature({"TextInput", "Main"}) public void testGuessedKeyCodeFromTyping() throws Throwable { assertEquals(-1, getTypedKeycodeGuess(null, "")); assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess(null, "x")); assertEquals(-1, getTypedKeycodeGuess(null, "xyz")); assertEquals(-1, getTypedKeycodeGuess("abc", "abc")); assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("abc", "")); assertEquals(KeyEvent.KEYCODE_H, getTypedKeycodeGuess("", "h")); assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("h", "")); assertEquals(KeyEvent.KEYCODE_E, getTypedKeycodeGuess("h", "he")); assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("he", "hel")); assertEquals(KeyEvent.KEYCODE_O, getTypedKeycodeGuess("hel", "helo")); assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("helo", "hel")); assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("hel", "hell")); assertEquals(KeyEvent.KEYCODE_L, getTypedKeycodeGuess("hell", "helll")); assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("helll", "hell")); assertEquals(KeyEvent.KEYCODE_O, getTypedKeycodeGuess("hell", "hello")); assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess("xxx", "xxxx")); assertEquals(KeyEvent.KEYCODE_X, getTypedKeycodeGuess("xxx", "xxxxx")); assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("xxx", "xx")); assertEquals(KeyEvent.KEYCODE_DEL, getTypedKeycodeGuess("xxx", "x")); assertEquals(KeyEvent.KEYCODE_Y, getTypedKeycodeGuess("xxx", "xxxy")); assertEquals(KeyEvent.KEYCODE_Y, getTypedKeycodeGuess("xxx", "xxxxy")); assertEquals(-1, getTypedKeycodeGuess("xxx", "xy")); assertEquals(-1, getTypedKeycodeGuess("xxx", "y")); assertEquals(-1, getTypedKeycodeGuess("foo", "bar")); assertEquals(-1, getTypedKeycodeGuess("foo", "bars")); assertEquals(-1, getTypedKeycodeGuess("foo", "ba")); // Some characters also require modifiers so we have to check the full event. KeyEvent ev = ImeAdapter.getTypedKeyEventGuess(null, "!"); assertEquals(KeyEvent.KEYCODE_1, ev.getKeyCode()); assertTrue(ev.isShiftPressed()); }
private int getTypedKeycodeGuess(String before, String after) { KeyEvent ev = ImeAdapter.getTypedKeyEventGuess(before, after); if (ev == null) return -1; return ev.getKeyCode(); }
private InputMethodManagerWrapper getInputMethodManagerWrapper() { return mImeAdapter.getInputMethodManagerWrapper(); }
@VisibleForTesting AdapterInputConnection(View view, ImeAdapter imeAdapter, Editable editable, EditorInfo outAttrs) { super(view, true); mInternalView = view; mImeAdapter = imeAdapter; mImeAdapter.setInputConnection(this); mEditable = editable; // The editable passed in might have been in use by a prior keyboard and could have had // prior composition spans set. To avoid keyboard conflicts, remove all composing spans // when taking ownership of an existing Editable. removeComposingSpans(mEditable); mSingleLine = true; outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI; outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; int inputType = imeAdapter.getTextInputType(); int inputFlags = imeAdapter.getTextInputFlags(); if ((inputFlags & WebTextInputFlags.AutocompleteOff) != 0) { outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS; } if (inputType == TextInputType.TEXT) { // Normal text field outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; if ((inputFlags & WebTextInputFlags.AutocorrectOff) == 0) { outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; } } else if (inputType == TextInputType.TEXT_AREA || inputType == TextInputType.CONTENT_EDITABLE) { outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; if ((inputFlags & WebTextInputFlags.AutocorrectOff) == 0) { outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; } outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE; mSingleLine = false; } else if (inputType == TextInputType.PASSWORD) { // Password outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; } else if (inputType == TextInputType.SEARCH) { // Search outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH; } else if (inputType == TextInputType.URL) { // Url outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI; outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; } else if (inputType == TextInputType.EMAIL) { // Email outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; } else if (inputType == TextInputType.TELEPHONE) { // 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 (inputType == TextInputType.NUMBER) { // Number outAttrs.inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_NORMAL | InputType.TYPE_NUMBER_FLAG_DECIMAL; outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; } // Handling of autocapitalize. Blink will send the flag taking into account the element's // type. This is not using AutocapitalizeNone because Android does not autocapitalize by // default and there is no way to express no capitalization. // Autocapitalize is meant as a hint to the virtual keyboard. if ((inputFlags & WebTextInputFlags.AutocapitalizeCharacters) != 0) { outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; } else if ((inputFlags & WebTextInputFlags.AutocapitalizeWords) != 0) { outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; } else if ((inputFlags & WebTextInputFlags.AutocapitalizeSentences) != 0) { outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; } // Content editable doesn't use autocapitalize so we need to set it manually. if (inputType == TextInputType.CONTENT_EDITABLE) { outAttrs.inputType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; } outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); mLastUpdateSelectionStart = outAttrs.initialSelStart; mLastUpdateSelectionEnd = outAttrs.initialSelEnd; Selection.setSelection(mEditable, outAttrs.initialSelStart, outAttrs.initialSelEnd); updateSelectionIfRequired(); }
/** @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent) */ @Override public boolean sendKeyEvent(KeyEvent event) { if (DEBUG) { Log.w( TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "] [" + event.getUnicodeChar() + "]"); } int action = event.getAction(); int keycode = event.getKeyCode(); int unicodeChar = event.getUnicodeChar(); // If this isn't a KeyDown event, no need to update composition state; just pass the key // event through and return. But note that some keys, such as enter, may actually be // handled on ACTION_UP in Blink. if (action != KeyEvent.ACTION_DOWN) { mImeAdapter.translateAndSendNativeEvents(event); return true; } // If this is 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). Some IMEs like Jellybean stock IME and Samsung IME mix in delete // KeyPress events instead of calling deleteSurroundingText. if (keycode == KeyEvent.KEYCODE_DEL) { deleteSurroundingTextImpl(1, 0, true); } else if (keycode == KeyEvent.KEYCODE_FORWARD_DEL) { deleteSurroundingTextImpl(0, 1, true); } else if (keycode == KeyEvent.KEYCODE_ENTER) { // Finish text composition when pressing enter, as that may submit a form field. // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed. finishComposingText(); } else if ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) != 0) { // Store a pending accent character and make it the current composition. int pendingAccent = unicodeChar & KeyCharacterMap.COMBINING_ACCENT_MASK; StringBuilder builder = new StringBuilder(); builder.appendCodePoint(pendingAccent); setComposingText(builder.toString(), 1); mPendingAccent = pendingAccent; return true; } else if (mPendingAccent != 0 && unicodeChar != 0) { int combined = KeyEvent.getDeadChar(mPendingAccent, unicodeChar); if (combined != 0) { StringBuilder builder = new StringBuilder(); builder.appendCodePoint(combined); commitText(builder.toString(), 1); return true; } // Noncombinable character; commit the accent character and fall through to sending // the key event for the character afterwards. finishComposingText(); } replaceSelectionWithUnicodeChar(unicodeChar); mImeAdapter.translateAndSendNativeEvents(event); return true; }
private boolean deleteSurroundingTextImpl( int beforeLength, int afterLength, boolean fromPhysicalKey) { if (DEBUG) { Log.w( TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + " " + fromPhysicalKey + "]"); } if (mPendingAccent != 0) { finishComposingText(); } int originalBeforeLength = beforeLength; int originalAfterLength = afterLength; int selectionStart = Selection.getSelectionStart(mEditable); int selectionEnd = Selection.getSelectionEnd(mEditable); int availableBefore = selectionStart; int availableAfter = mEditable.length() - selectionEnd; beforeLength = Math.min(beforeLength, availableBefore); afterLength = Math.min(afterLength, availableAfter); // Adjust these values even before calling super.deleteSurroundingText() to be consistent // with the super class. if (isIndexBetweenUtf16SurrogatePair(mEditable, selectionStart - beforeLength)) { beforeLength += 1; } if (isIndexBetweenUtf16SurrogatePair(mEditable, selectionEnd + afterLength)) { afterLength += 1; } super.deleteSurroundingText(beforeLength, afterLength); updateSelectionIfRequired(); // If this was called due to a physical key, no need to generate a key event here as // the caller will take care of forwarding the original. if (fromPhysicalKey) { return true; } // For single-char deletion calls |ImeAdapter.sendKeyEventWithKeyCode| with the real key // code. For multi-character deletion, executes deletion by calling // |ImeAdapter.deleteSurroundingText| and sends synthetic key events with a dummy key code. int keyCode = KeyEvent.KEYCODE_UNKNOWN; if (originalBeforeLength == 1 && originalAfterLength == 0) { keyCode = KeyEvent.KEYCODE_DEL; } else if (originalBeforeLength == 0 && originalAfterLength == 1) { keyCode = KeyEvent.KEYCODE_FORWARD_DEL; } boolean result = true; if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { result = mImeAdapter.sendSyntheticKeyEvent( WebInputEventType.RawKeyDown, SystemClock.uptimeMillis(), keyCode, 0, 0); result &= mImeAdapter.deleteSurroundingText(beforeLength, afterLength); result &= mImeAdapter.sendSyntheticKeyEvent( WebInputEventType.KeyUp, SystemClock.uptimeMillis(), keyCode, 0, 0); } else { mImeAdapter.sendKeyEventWithKeyCode( keyCode, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); } return result; }