blob: a9d2d54ee00362008b644e88ea638638f1e532d6 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser.input;
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 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.
* 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. When InputConnectionAdapter gets called with composition or result text:
* If we receive a composition text or a result text, then we just need to
* dispatch a synthetic key event with special keycode 229, and then dispatch
* the composition or result text.
* 2. 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.
*
* Note that the native peer object does not take any strong reference onto the
* instance of this java object, hence it is up to the client of this class (e.g.
* the ViewEmbedder implementor) to hold a strong reference to it for the required
* lifetime of the object.
*/
@JNINamespace("content")
public class ImeAdapter {
/**
* Interface for the delegate that needs to be notified of IME changes.
*/
public interface ImeAdapterDelegate {
/**
* 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 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 long mNativeImeAdapter;
DelayedDismissInput(long nativeImeAdapter) {
mNativeImeAdapter = nativeImeAdapter;
}
// http://crbug.com/413744
void detach() {
mNativeImeAdapter = 0;
}
@Override
public void run() {
if (mNativeImeAdapter != 0) {
attach(mNativeImeAdapter, sTextInputTypeNone, sTextInputFlagNone);
}
dismissInput(true);
}
}
private static final int COMPOSITION_KEY_CODE = 229;
// 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;
// All the constants that are retrieved from the C++ code.
// They get set through initializeWebInputEvents and initializeTextInputTypes calls.
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 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 AdapterInputConnection mInputConnection;
private final ImeAdapterDelegate mViewEmbedder;
private final Handler mHandler;
private DelayedDismissInput mDismissInput = null;
private int mTextInputType;
private int mTextInputFlags;
private String mLastComposeText;
@VisibleForTesting
int mLastSyntheticKeyCode;
@VisibleForTesting
boolean mIsShowWithoutHideOutstanding = false;
/**
* @param wrapper InputMethodManagerWrapper that should receive all the call directed to
* InputMethodManager.
* @param embedder The view that is used for callbacks from ImeAdapter.
*/
public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) {
mInputMethodManagerWrapper = wrapper;
mViewEmbedder = embedder;
mHandler = new Handler();
}
/**
* Default factory for AdapterInputConnection classes.
*/
public static class AdapterInputConnectionFactory {
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;
}
/**
* Should be only used by AdapterInputConnection.
* @return InputMethodManagerWrapper that should receive all the calls directed to
* InputMethodManager.
*/
InputMethodManagerWrapper getInputMethodManagerWrapper() {
return mInputMethodManagerWrapper;
}
/**
* Set the current active InputConnection when a new InputConnection is constructed.
* @param inputConnection The input connection that is currently used with IME.
*/
void setInputConnection(AdapterInputConnection inputConnection) {
mInputConnection = inputConnection;
mLastComposeText = null;
}
/**
* Should be used only by AdapterInputConnection.
* @return The input type of currently focused element.
*/
int getTextInputType() {
return mTextInputType;
}
/**
* Should be used only by AdapterInputConnection.
* @return The input flags of the currently focused element.
*/
int getTextInputFlags() {
return mTextInputFlags;
}
/**
* @return Constant representing that a focused node is not an input field.
*/
public static int getTextInputTypeNone() {
return sTextInputTypeNone;
}
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;
}
/**
* 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
// and input type should remain as none.
if (mTextInputType == sTextInputTypeNone && !showIfNeeded) {
return;
}
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) {
mDismissInput = new DelayedDismissInput(nativeImeAdapter);
mHandler.postDelayed(mDismissInput, INPUT_DISMISS_DELAY);
return;
}
attach(nativeImeAdapter, textInputType, textInputFlags);
mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView());
if (showIfNeeded) {
showKeyboard();
}
} else if (hasInputType() && showIfNeeded) {
showKeyboard();
}
}
public void attach(long nativeImeAdapter, int textInputType, int textInputFlags) {
if (mNativeImeAdapterAndroid != 0) {
nativeResetImeAdapter(mNativeImeAdapterAndroid);
}
mNativeImeAdapterAndroid = nativeImeAdapter;
mTextInputType = textInputType;
mTextInputFlags = textInputFlags;
mLastComposeText = null;
if (nativeImeAdapter != 0) {
nativeAttachImeAdapter(mNativeImeAdapterAndroid);
}
if (mTextInputType == sTextInputTypeNone) {
dismissInput(false);
}
}
/**
* 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.
*/
public void attach(long nativeImeAdapter) {
attach(nativeImeAdapter, sTextInputTypeNone, sTextInputFlagNone);
}
private void showKeyboard() {
mIsShowWithoutHideOutstanding = true;
mInputMethodManagerWrapper.showSoftInput(mViewEmbedder.getAttachedView(), 0,
mViewEmbedder.getNewShowKeyboardReceiver());
}
private void dismissInput(boolean unzoomIfNeeded) {
mIsShowWithoutHideOutstanding = false;
View view = mViewEmbedder.getAttachedView();
if (mInputMethodManagerWrapper.isActive(view)) {
mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0,
unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null);
}
mViewEmbedder.onDismissInput();
}
private boolean hasInputType() {
return mTextInputType != sTextInputTypeNone;
}
private static boolean isTextInputType(int type) {
return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type);
}
public boolean hasTextInputType() {
return isTextInputType(mTextInputType);
}
/**
* @return true if the selected text is of password.
*/
public boolean isSelectionPassword() {
return mTextInputType == sTextInputTypePassword;
}
public boolean dispatchKeyEvent(KeyEvent event) {
// Physical keyboards have their events come through here instead of
// AdapterInputConnection.
if (mInputConnection != null) {
return mInputConnection.sendKeyEvent(event);
}
return translateAndSendNativeEvents(event, 0);
}
private int shouldSendKeyEventWithKeyCode(String text) {
if (text.length() != 1) return COMPOSITION_KEY_CODE;
if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER;
else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB;
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 = SystemClock.uptimeMillis();
mLastSyntheticKeyCode = keyCode;
translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags), 0);
translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
KeyEvent.ACTION_UP, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags), 0);
}
// Calls from Java to C++
// TODO: Add performance tracing to more complicated functions.
boolean checkCompositionQueueAndCallNative(CharSequence text, int newCursorPosition,
boolean isCommit) {
if (mNativeImeAdapterAndroid == 0) return false;
mViewEmbedder.onImeEvent();
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 {
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 single-character 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 && textStr.length() == 1) {
mLastSyntheticKeyCode = keyCode;
return translateAndSendNativeEvents(keyEvent, 0)
&& translateAndSendNativeEvents(
KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP), 0);
}
// FIXME: Use WebTextInputFlags.AutocompleteOff. We need this hack to enable merge into
// into Beta to fix http://crbug.com/422685 .
final int textInputFlagAutocompleteOff = 1 << 1;
// If we do not have autocomplete=off, then always send compose events rather than a
// guessed keyCode. This addresses http://crbug.com/422685 .
if ((mTextInputFlags & textInputFlagAutocompleteOff) == 0) {
keyCode = COMPOSITION_KEY_CODE;
modifiers = 0;
}
// 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, textStr);
textStr = null;
} else {
nativeSetComposingText(mNativeImeAdapterAndroid, text, textStr, newCursorPosition);
}
if (keyCode >= 0) {
nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
timeStampMs, keyCode, modifiers, 0);
}
mLastSyntheticKeyCode = keyCode;
}
mLastComposeText = textStr;
return true;
}
void finishComposingText() {
mLastComposeText = null;
if (mNativeImeAdapterAndroid == 0) return;
nativeFinishComposingText(mNativeImeAdapterAndroid);
}
boolean translateAndSendNativeEvents(KeyEvent event, int accentChar) {
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();
int unicodeChar = AdapterInputConnection.maybeAddAccentToCharacter(
accentChar, event.getUnicodeChar());
return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
/*isSystemKey=*/false, unicodeChar);
}
boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int modifiers,
int unicodeChar) {
if (mNativeImeAdapterAndroid == 0) return false;
nativeSendSyntheticKeyEvent(
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);
return true;
}
/**
* 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(CharSequence text, int start, int end) {
if (mNativeImeAdapterAndroid == 0) return false;
nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
mLastComposeText = text != null ? text.toString() : null;
return true;
}
/**
* Send a request to the native counterpart to unselect text.
* @return Whether the native counterpart of ImeAdapter received the call.
*/
public boolean unselect() {
if (mNativeImeAdapterAndroid == 0) return false;
nativeUnselect(mNativeImeAdapterAndroid);
return true;
}
/**
* Send a request to the native counterpart of ImeAdapter to select all the text.
* @return Whether the native counterpart of ImeAdapter received the call.
*/
public boolean selectAll() {
if (mNativeImeAdapterAndroid == 0) return false;
nativeSelectAll(mNativeImeAdapterAndroid);
return true;
}
/**
* Send a request to the native counterpart of ImeAdapter to cut the selected text.
* @return Whether the native counterpart of ImeAdapter received the call.
*/
public boolean cut() {
if (mNativeImeAdapterAndroid == 0) return false;
nativeCut(mNativeImeAdapterAndroid);
return true;
}
/**
* Send a request to the native counterpart of ImeAdapter to copy the selected text.
* @return Whether the native counterpart of ImeAdapter received the call.
*/
public boolean copy() {
if (mNativeImeAdapterAndroid == 0) return false;
nativeCopy(mNativeImeAdapterAndroid);
return true;
}
/**
* Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard.
* @return Whether the native counterpart of ImeAdapter received the call.
*/
public boolean paste() {
if (mNativeImeAdapterAndroid == 0) return false;
nativePaste(mNativeImeAdapterAndroid);
return true;
}
// Calls from C++ to Java
@CalledByNative
private 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
private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText,
int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch,
int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel,
int textInputTypeNumber, int textInputTypeContentEditable) {
sTextInputTypeNone = textInputTypeNone;
sTextInputTypeText = textInputTypeText;
sTextInputTypeTextArea = textInputTypeTextArea;
sTextInputTypePassword = textInputTypePassword;
sTextInputTypeSearch = textInputTypeSearch;
sTextInputTypeUrl = textInputTypeUrl;
sTextInputTypeEmail = textInputTypeEmail;
sTextInputTypeTel = textInputTypeTel;
sTextInputTypeNumber = textInputTypeNumber;
sTextInputTypeContentEditable = textInputTypeContentEditable;
}
@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
private void setCharacterBounds(float[] characterBounds) {
// TODO(yukawa): Implement this.
}
@CalledByNative
void detach() {
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 modifiers, int unicodeChar);
private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
int unicodeChar);
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 textStr);
private native void nativeFinishComposingText(long nativeImeAdapterAndroid);
private native void nativeAttachImeAdapter(long nativeImeAdapterAndroid);
private native void nativeSetEditableSelectionOffsets(long nativeImeAdapterAndroid,
int start, int end);
private native void nativeSetComposingRegion(long nativeImeAdapterAndroid, int start, int end);
private native void nativeDeleteSurroundingText(long nativeImeAdapterAndroid,
int before, int after);
private native void nativeUnselect(long nativeImeAdapterAndroid);
private native void nativeSelectAll(long nativeImeAdapterAndroid);
private native void nativeCut(long nativeImeAdapterAndroid);
private native void nativeCopy(long nativeImeAdapterAndroid);
private native void nativePaste(long nativeImeAdapterAndroid);
private native void nativeResetImeAdapter(long nativeImeAdapterAndroid);
}