blob: 0dc13ed95d8e0d83b6b02969c7c76234bcb62276 [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.annotation.SuppressLint;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.text.SpannableString;
import android.text.TextUtils;
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.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import org.chromium.base.Log;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.blink_public.web.WebInputEventModifier;
import org.chromium.blink_public.web.WebInputEventType;
import org.chromium.blink_public.web.WebTextInputMode;
import org.chromium.content.browser.ViewUtils;
import org.chromium.content.browser.picker.InputDialogContainer;
import org.chromium.content_public.browser.ImeEventObserver;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ime.TextInputType;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* 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 {
private static final String TAG = "cr_Ime";
private static final boolean DEBUG_LOGS = false;
public static final int COMPOSITION_KEY_CODE = 229;
private static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000;
private long mNativeImeAdapterAndroid;
private InputMethodManagerWrapper mInputMethodManagerWrapper;
private ChromiumBaseInputConnection mInputConnection;
private ChromiumBaseInputConnection.Factory mInputConnectionFactory;
// NOTE: This object will not be released by Android framework until the matching
// ResultReceiver in the InputMethodService (IME app) gets gc'ed.
private ShowKeyboardResultReceiver mShowKeyboardResultReceiver;
private final WebContents mWebContents;
private View mContainerView;
// This holds the information necessary for constructing CursorAnchorInfo, and notifies to
// InputMethodManager on appropriate timing, depending on how IME requested the information
// via InputConnection. The update request is per InputConnection, hence for each time it is
// re-created, the monitoring status will be reset.
private final CursorAnchorInfoController mCursorAnchorInfoController;
private final List<ImeEventObserver> mEventObservers = new ArrayList<>();
private int mTextInputType = TextInputType.NONE;
private int mTextInputFlags;
private int mTextInputMode = WebTextInputMode.DEFAULT;
private boolean mNodeEditable;
private boolean mNodePassword;
// Viewport rect before the OSK was brought up.
// Used to tell View#onSizeChanged to focus a form element.
private final Rect mFocusPreOSKViewportRect = new Rect();
// Keep the current configuration to detect the change when onConfigurationChanged() is called.
private Configuration mCurrentConfig;
private int mLastSelectionStart;
private int mLastSelectionEnd;
private String mLastText;
private int mLastCompositionStart;
private int mLastCompositionEnd;
private boolean mRestartInputOnNextStateUpdate;
// True if ImeAdapter is connected to render process.
private boolean mIsConnected;
/**
* {@ResultReceiver} passed in InputMethodManager#showSoftInput}. We need this to scroll to the
* editable node at the right timing, which is after input method window shows up.
*/
// TODO(crbug.com/635567): Fix this properly.
@SuppressLint("ParcelCreator")
private static class ShowKeyboardResultReceiver extends ResultReceiver {
// Unfortunately, the memory life cycle of ResultReceiver object, once passed in
// showSoftInput(), is in the control of Android's input method framework and IME app,
// so we use a weakref to avoid tying ImeAdapter's lifetime to that of ResultReceiver
// object.
private final WeakReference<ImeAdapter> mImeAdapter;
public ShowKeyboardResultReceiver(ImeAdapter imeAdapter, Handler handler) {
super(handler);
mImeAdapter = new WeakReference<>(imeAdapter);
}
@Override
public void onReceiveResult(int resultCode, Bundle resultData) {
ImeAdapter imeAdapter = mImeAdapter.get();
if (imeAdapter == null) return;
imeAdapter.onShowKeyboardReceiveResult(resultCode);
}
}
/**
* @param webContents WebContents instance with which this ImeAdapter is associated.
* @param containerView {@link View} instance which input events are posted on.
* @param wrapper InputMethodManagerWrapper that should receive all the call directed to
* InputMethodManager.
*/
public ImeAdapter(
WebContents webContents, View containerView, InputMethodManagerWrapper wrapper) {
mWebContents = webContents;
mContainerView = containerView;
mInputMethodManagerWrapper = wrapper;
// Deep copy newConfig so that we can notice the difference.
mCurrentConfig = new Configuration(mContainerView.getResources().getConfiguration());
// CursorAnchroInfo is supported only after L.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mCursorAnchorInfoController = CursorAnchorInfoController.create(wrapper,
new CursorAnchorInfoController.ComposingTextDelegate() {
@Override
public CharSequence getText() {
return mLastText;
}
@Override
public int getSelectionStart() {
return mLastSelectionStart;
}
@Override
public int getSelectionEnd() {
return mLastSelectionEnd;
}
@Override
public int getComposingTextStart() {
return mLastCompositionStart;
}
@Override
public int getComposingTextEnd() {
return mLastCompositionEnd;
}
});
} else {
mCursorAnchorInfoController = null;
}
mNativeImeAdapterAndroid = nativeInit(webContents);
}
/**
* Set the container view.
* @param containerView {@link View} which this ImeAdapter works on.
*/
public void setContainerView(View containerView) {
mContainerView = containerView;
}
public void addEventObserver(ImeEventObserver eventObserver) {
mEventObservers.add(eventObserver);
}
private void createInputConnectionFactory() {
if (mInputConnectionFactory != null) return;
mInputConnectionFactory = new ThreadedInputConnectionFactory(mInputMethodManagerWrapper);
}
// Tells if the ImeAdapter in valid state (i.e. not in destroyed state), and is
// connected to render process. The former check guards against the call via
// ThreadedInputConnection from Android framework after ImeAdapter.destroy() is called.
private boolean isValid() {
return mNativeImeAdapterAndroid != 0 && mIsConnected;
}
/**
* @see View#onCreateInputConnection(EditorInfo)
* @param allowKeyboardLearning Whether to allow keyboard (IME) app to do personalized learning.
*/
public ChromiumBaseInputConnection onCreateInputConnection(
EditorInfo outAttrs, boolean allowKeyboardLearning) {
// InputMethodService evaluates fullscreen mode even when the new input connection is
// null. This makes sure IME doesn't enter fullscreen mode or open custom UI.
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
// TODO(changwan): Replace with EditorInfoCompat#IME_FLAG_NO_PERSONALIZED_LEARNING or
// EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING as soon as either is
// available in all build config types.
if (!allowKeyboardLearning) outAttrs.imeOptions |= IME_FLAG_NO_PERSONALIZED_LEARNING;
// Without this line, some third-party IMEs will try to compose text even when
// not on an editable node. Even when we return null here, key events can still go
// through ImeAdapter#dispatchKeyEvent().
if (mTextInputType == TextInputType.NONE) {
setInputConnection(null);
if (DEBUG_LOGS) Log.i(TAG, "onCreateInputConnection returns null.");
return null;
}
if (mInputConnectionFactory == null) return null;
setInputConnection(mInputConnectionFactory.initializeAndGet(mContainerView, this,
mTextInputType, mTextInputFlags, mTextInputMode, mLastSelectionStart,
mLastSelectionEnd, outAttrs));
if (DEBUG_LOGS) Log.i(TAG, "onCreateInputConnection: " + mInputConnection);
if (mCursorAnchorInfoController != null) {
mCursorAnchorInfoController.onRequestCursorUpdates(false /* not an immediate request */,
false /* disable monitoring */, mContainerView);
}
if (isValid()) {
nativeRequestCursorUpdate(mNativeImeAdapterAndroid,
false /* not an immediate request */, false /* disable monitoring */);
}
return mInputConnection;
}
private void setInputConnection(ChromiumBaseInputConnection inputConnection) {
if (mInputConnection == inputConnection) return;
// The previous input connection might be waiting for state update.
if (mInputConnection != null) mInputConnection.unblockOnUiThread();
mInputConnection = inputConnection;
}
/**
* Overrides the InputMethodManagerWrapper that ImeAdapter uses to make calls to
* InputMethodManager.
* @param immw InputMethodManagerWrapper that should be used to call InputMethodManager.
*/
@VisibleForTesting
public void setInputMethodManagerWrapperForTest(InputMethodManagerWrapper immw) {
mInputMethodManagerWrapper = immw;
if (mCursorAnchorInfoController != null) {
mCursorAnchorInfoController.setInputMethodManagerWrapperForTest(immw);
}
}
@VisibleForTesting
void setInputConnectionFactory(ChromiumBaseInputConnection.Factory factory) {
mInputConnectionFactory = factory;
}
@VisibleForTesting
ChromiumBaseInputConnection.Factory getInputConnectionFactoryForTest() {
return mInputConnectionFactory;
}
/**
* Get the current input connection for testing purposes.
*/
@VisibleForTesting
public ChromiumBaseInputConnection getInputConnectionForTest() {
return mInputConnection;
}
private static int getModifiers(int metaState) {
int modifiers = 0;
if ((metaState & KeyEvent.META_SHIFT_ON) != 0) {
modifiers |= WebInputEventModifier.SHIFT_KEY;
}
if ((metaState & KeyEvent.META_ALT_ON) != 0) {
modifiers |= WebInputEventModifier.ALT_KEY;
}
if ((metaState & KeyEvent.META_CTRL_ON) != 0) {
modifiers |= WebInputEventModifier.CONTROL_KEY;
}
if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) {
modifiers |= WebInputEventModifier.CAPS_LOCK_ON;
}
if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) {
modifiers |= WebInputEventModifier.NUM_LOCK_ON;
}
return modifiers;
}
/**
* Updates internal representation of the text being edited and its selection and composition
* properties.
*
* @param textInputType Text input type for the currently focused field in renderer.
* @param textInputFlags Text input flags.
* @param textInputMode Text input mode.
* @param showIfNeeded Whether the keyboard should be shown if it is currently hidden.
* @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.
* @param replyToRequest True when the update was requested by IME.
*/
@CalledByNative
private void updateState(int textInputType, int textInputFlags, int textInputMode,
boolean showIfNeeded, String text, int selectionStart, int selectionEnd,
int compositionStart, int compositionEnd, boolean replyToRequest) {
TraceEvent.begin("ImeAdapter.updateState");
try {
if (DEBUG_LOGS) {
Log.i(TAG, "updateState: type [%d->%d], flags [%d], show [%b], ", mTextInputType,
textInputType, textInputFlags, showIfNeeded);
}
boolean needsRestart = false;
if (mRestartInputOnNextStateUpdate) {
needsRestart = true;
mRestartInputOnNextStateUpdate = false;
}
mTextInputFlags = textInputFlags;
if (mTextInputMode != textInputMode) {
mTextInputMode = textInputMode;
needsRestart = true;
}
if (mTextInputType != textInputType) {
mTextInputType = textInputType;
needsRestart = true;
boolean editable = textInputType != TextInputType.NONE;
boolean password = textInputType == TextInputType.PASSWORD;
if (mNodeEditable != editable || mNodePassword != password) {
for (ImeEventObserver observer : mEventObservers) {
observer.onNodeAttributeUpdated(editable, password);
}
mNodeEditable = editable;
mNodePassword = password;
}
}
if (mCursorAnchorInfoController != null
&& (!TextUtils.equals(mLastText, text) || mLastSelectionStart != selectionStart
|| mLastSelectionEnd != selectionEnd
|| mLastCompositionStart != compositionStart
|| mLastCompositionEnd != compositionEnd)) {
mCursorAnchorInfoController.invalidateLastCursorAnchorInfo();
}
mLastText = text;
mLastSelectionStart = selectionStart;
mLastSelectionEnd = selectionEnd;
mLastCompositionStart = compositionStart;
mLastCompositionEnd = compositionEnd;
if (textInputType == TextInputType.NONE) {
hideKeyboard();
} else {
if (needsRestart) restartInput();
// There is no API for us to get notified of user's dismissal of keyboard.
// Therefore, we should try to show keyboard even when text input type hasn't
// changed.
if (showIfNeeded) showSoftKeyboard();
}
if (mInputConnection != null) {
boolean singleLine = mTextInputType != TextInputType.TEXT_AREA
&& mTextInputType != TextInputType.CONTENT_EDITABLE;
mInputConnection.updateStateOnUiThread(text, selectionStart, selectionEnd,
compositionStart, compositionEnd, singleLine, replyToRequest);
}
} finally {
TraceEvent.end("ImeAdapter.updateState");
}
}
/**
* Show soft keyboard only if it is the current keyboard configuration.
*/
private void showSoftKeyboard() {
if (DEBUG_LOGS) Log.i(TAG, "showSoftKeyboard");
mInputMethodManagerWrapper.showSoftInput(mContainerView, 0, getNewShowKeyboardReceiver());
if (mContainerView.getResources().getConfiguration().keyboard
!= Configuration.KEYBOARD_NOKEYS) {
mWebContents.scrollFocusedEditableNodeIntoView();
}
}
/**
* Call this when we get result from ResultReceiver passed in calling showSoftInput().
* @param resultCode The result of showSoftInput() as defined in InputMethodManager.
*/
public void onShowKeyboardReceiveResult(int resultCode) {
if (resultCode == InputMethodManager.RESULT_SHOWN) {
// If OSK is newly shown, delay the form focus until
// the onSizeChanged (in order to adjust relative to the
// new size).
// TODO(jdduke): We should not assume that onSizeChanged will
// always be called, crbug.com/294908.
mContainerView.getWindowVisibleDisplayFrame(mFocusPreOSKViewportRect);
} else if (ViewUtils.hasFocus(mContainerView)
&& resultCode == InputMethodManager.RESULT_UNCHANGED_SHOWN) {
// If the OSK was already there, focus the form immediately.
mWebContents.scrollFocusedEditableNodeIntoView();
}
}
public Rect getFocusPreOSKViewportRect() {
return mFocusPreOSKViewportRect;
}
@VisibleForTesting
public ResultReceiver getNewShowKeyboardReceiver() {
if (mShowKeyboardResultReceiver == null) {
// Note: the returned object will get leaked by Android framework.
mShowKeyboardResultReceiver = new ShowKeyboardResultReceiver(this, new Handler());
}
return mShowKeyboardResultReceiver;
}
/**
* Hide soft keyboard.
*/
private void hideKeyboard() {
if (DEBUG_LOGS) Log.i(TAG, "hideKeyboard");
View view = mContainerView;
if (mInputMethodManagerWrapper.isActive(view)) {
// NOTE: we should not set ResultReceiver here. Otherwise, IMM will own ContentViewCore
// and ImeAdapter even after input method goes away and result gets received.
mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0, null);
}
// Detach input connection by returning null from onCreateInputConnection().
if (mTextInputType == TextInputType.NONE && mInputConnection != null) {
ChromiumBaseInputConnection inputConnection = mInputConnection;
restartInput(); // resets mInputConnection
// crbug.com/666982: Restart input may not happen if view is detached from window, but
// we need to unblock in any case. We want to call this after restartInput() to
// ensure that there is no additional IME operation in the queue.
inputConnection.unblockOnUiThread();
}
}
/**
* Call this when keyboard configuration has changed.
*/
public void onKeyboardConfigurationChanged(Configuration newConfig) {
// If configuration unchanged, do nothing.
if (mCurrentConfig.keyboard == newConfig.keyboard
&& mCurrentConfig.keyboardHidden == newConfig.keyboardHidden
&& mCurrentConfig.hardKeyboardHidden == newConfig.hardKeyboardHidden) {
return;
}
// Deep copy newConfig so that we can notice the difference.
mCurrentConfig = new Configuration(newConfig);
if (DEBUG_LOGS) {
Log.i(TAG, "onKeyboardConfigurationChanged: mTextInputType [%d]", mTextInputType);
}
if (mTextInputType != TextInputType.NONE) {
restartInput();
// By default, we show soft keyboard on keyboard changes. This is useful
// when the user switches from hardware keyboard to software keyboard.
// TODO(changwan): check if we can skip this for hardware keyboard configurations.
showSoftKeyboard();
}
}
/**
* Call this when window's focus has changed.
* @param gainFocus True if we're gaining focus.
*/
public void onWindowFocusChanged(boolean gainFocus) {
if (mInputConnectionFactory != null) {
mInputConnectionFactory.onWindowFocusChanged(gainFocus);
}
}
/**
* Call this when view is attached to window.
*/
public void onViewAttachedToWindow() {
if (mInputConnectionFactory != null) {
mInputConnectionFactory.onViewAttachedToWindow();
}
}
/**
* Call this when view is detached from window.
*/
public void onViewDetachedFromWindow() {
resetAndHideKeyboard();
if (mInputConnectionFactory != null) {
mInputConnectionFactory.onViewDetachedFromWindow();
}
}
/**
* Call this when view's focus has changed.
* @param gainFocus True if we're gaining focus.
* @param hideKeyboardOnBlur True if we should hide soft keyboard when losing focus.
*/
public void onViewFocusChanged(boolean gainFocus, boolean hideKeyboardOnBlur) {
if (DEBUG_LOGS) Log.i(TAG, "onViewFocusChanged: gainFocus [%b]", gainFocus);
if (!gainFocus && hideKeyboardOnBlur) resetAndHideKeyboard();
if (mInputConnectionFactory != null) {
mInputConnectionFactory.onViewFocusChanged(gainFocus);
}
}
@VisibleForTesting
void setInputTypeForTest(int textInputType) {
mTextInputType = textInputType;
}
private static boolean isTextInputType(int type) {
return type != TextInputType.NONE && !InputDialogContainer.isDialogInputType(type);
}
public boolean hasTextInputType() {
return isTextInputType(mTextInputType);
}
/**
* See {@link View#dispatchKeyEvent(KeyEvent)}
*/
public boolean dispatchKeyEvent(KeyEvent event) {
if (DEBUG_LOGS) {
Log.i(TAG, "dispatchKeyEvent: action [%d], keycode [%d]", event.getAction(),
event.getKeyCode());
}
if (mInputConnection != null) return mInputConnection.sendKeyEventOnUiThread(event);
return sendKeyEvent(event);
}
/**
* Resets IME adapter and hides keyboard. Note that this will also unblock input connection.
*/
public void resetAndHideKeyboard() {
if (DEBUG_LOGS) Log.i(TAG, "resetAndHideKeyboard");
mTextInputType = TextInputType.NONE;
mTextInputFlags = 0;
mTextInputMode = WebTextInputMode.DEFAULT;
mRestartInputOnNextStateUpdate = false;
// This will trigger unblocking if necessary.
hideKeyboard();
}
@CalledByNative
private void destroy() {
resetAndHideKeyboard();
mNativeImeAdapterAndroid = 0;
mIsConnected = false;
if (mCursorAnchorInfoController != null) {
mCursorAnchorInfoController.focusedNodeChanged(false);
}
}
/**
* Update selection to input method manager.
*
* @param selectionStart The selection start.
* @param selectionEnd The selection end.
* @param compositionStart The composition start.
* @param compositionEnd The composition end.
*/
void updateSelection(
int selectionStart, int selectionEnd, int compositionStart, int compositionEnd) {
mInputMethodManagerWrapper.updateSelection(
mContainerView, selectionStart, selectionEnd, compositionStart, compositionEnd);
}
/**
* Update extracted text to input method manager.
*/
void updateExtractedText(int token, ExtractedText extractedText) {
mInputMethodManagerWrapper.updateExtractedText(mContainerView, token, extractedText);
}
/**
* Restart input (finish composition and change EditorInfo, such as input type).
*/
void restartInput() {
// This will eventually cause input method manager to call View#onCreateInputConnection().
mInputMethodManagerWrapper.restartInput(mContainerView);
if (mInputConnection != null) mInputConnection.onRestartInputOnUiThread();
}
/**
* @see BaseInputConnection#performContextMenuAction(int)
*/
boolean performContextMenuAction(int id) {
if (DEBUG_LOGS) Log.i(TAG, "performContextMenuAction: id [%d]", id);
switch (id) {
case android.R.id.selectAll:
mWebContents.selectAll();
return true;
case android.R.id.cut:
mWebContents.cut();
return true;
case android.R.id.copy:
mWebContents.copy();
return true;
case android.R.id.paste:
mWebContents.paste();
return true;
default:
return false;
}
}
boolean performEditorAction(int actionCode) {
if (!isValid()) return false;
if (actionCode == EditorInfo.IME_ACTION_NEXT) {
sendSyntheticKeyPress(KeyEvent.KEYCODE_TAB,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
| KeyEvent.FLAG_EDITOR_ACTION);
} else {
sendSyntheticKeyPress(KeyEvent.KEYCODE_ENTER,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
| KeyEvent.FLAG_EDITOR_ACTION);
}
return true;
}
void notifyUserAction() {
mInputMethodManagerWrapper.notifyUserAction();
}
@VisibleForTesting
protected void sendSyntheticKeyPress(int keyCode, int flags) {
long eventTime = SystemClock.uptimeMillis();
sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags));
sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_UP, keyCode, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags));
}
private void onImeEvent() {
for (ImeEventObserver observer : mEventObservers) observer.onImeEvent();
if (mNodeEditable) mWebContents.dismissTextHandles();
}
boolean sendCompositionToNative(
CharSequence text, int newCursorPosition, boolean isCommit, int unicodeFromKeyEvent) {
if (!isValid()) return false;
// One WebView app detects Enter in JS by looking at KeyDown (http://crbug/577967).
if (TextUtils.equals(text, "\n")) {
sendSyntheticKeyPress(KeyEvent.KEYCODE_ENTER,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
return true;
}
onImeEvent();
long timestampMs = SystemClock.uptimeMillis();
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.RAW_KEY_DOWN, 0,
timestampMs, COMPOSITION_KEY_CODE, 0, false, unicodeFromKeyEvent);
if (isCommit) {
nativeCommitText(mNativeImeAdapterAndroid, text, text.toString(), newCursorPosition);
} else {
nativeSetComposingText(
mNativeImeAdapterAndroid, text, text.toString(), newCursorPosition);
}
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.KEY_UP, 0, timestampMs,
COMPOSITION_KEY_CODE, 0, false, unicodeFromKeyEvent);
return true;
}
@VisibleForTesting
boolean finishComposingText() {
if (!isValid()) return false;
nativeFinishComposingText(mNativeImeAdapterAndroid);
return true;
}
boolean sendKeyEvent(KeyEvent event) {
if (!isValid()) return false;
int action = event.getAction();
int type;
if (action == KeyEvent.ACTION_DOWN) {
type = WebInputEventType.KEY_DOWN;
} else if (action == KeyEvent.ACTION_UP) {
type = WebInputEventType.KEY_UP;
} else {
// In theory, KeyEvent.ACTION_MULTIPLE is a valid value, but in practice
// this seems to have been quietly deprecated and we've never observed
// a case where it's sent (holding down physical keyboard key also
// sends ACTION_DOWN), so it's fine to silently drop it.
return false;
}
onImeEvent();
return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, type,
getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
event.getScanCode(), /*isSystemKey=*/false, event.getUnicodeChar());
}
/**
* 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) {
onImeEvent();
if (!isValid()) return false;
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.RAW_KEY_DOWN, 0,
SystemClock.uptimeMillis(), COMPOSITION_KEY_CODE, 0, false, 0);
nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength);
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.KEY_UP, 0,
SystemClock.uptimeMillis(), COMPOSITION_KEY_CODE, 0, false, 0);
return true;
}
/**
* Send a request to the native counterpart to delete a given range of characters.
* @param beforeLength Number of code points to extend the selection by before the existing
* selection.
* @param afterLength Number of code points to extend the selection by after the existing
* selection.
* @return Whether the native counterpart of ImeAdapter received the call.
*/
boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
onImeEvent();
if (!isValid()) return false;
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.RAW_KEY_DOWN, 0,
SystemClock.uptimeMillis(), COMPOSITION_KEY_CODE, 0, false, 0);
nativeDeleteSurroundingTextInCodePoints(
mNativeImeAdapterAndroid, beforeLength, afterLength);
nativeSendKeyEvent(mNativeImeAdapterAndroid, null, WebInputEventType.KEY_UP, 0,
SystemClock.uptimeMillis(), COMPOSITION_KEY_CODE, 0, false, 0);
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 (!isValid()) 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(int start, int end) {
if (!isValid()) return false;
if (start <= end) {
nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
} else {
nativeSetComposingRegion(mNativeImeAdapterAndroid, end, start);
}
return true;
}
@CalledByNative
private void focusedNodeChanged(boolean isEditable) {
if (DEBUG_LOGS) Log.i(TAG, "focusedNodeChanged: isEditable [%b]", isEditable);
// Update controller before the connection is restarted.
if (mCursorAnchorInfoController != null) {
mCursorAnchorInfoController.focusedNodeChanged(isEditable);
}
if (mTextInputType != TextInputType.NONE && mInputConnection != null && isEditable) {
mRestartInputOnNextStateUpdate = true;
}
}
/**
* Send a request to the native counterpart to give the latest text input state update.
*/
boolean requestTextInputStateUpdate() {
if (!isValid()) return false;
// You won't get state update anyways.
if (mInputConnection == null) return false;
return nativeRequestTextInputStateUpdate(mNativeImeAdapterAndroid);
}
/**
* Notified when IME requested Chrome to change the cursor update mode.
*/
public boolean onRequestCursorUpdates(int cursorUpdateMode) {
final boolean immediateRequest =
(cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0;
final boolean monitorRequest =
(cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0;
if (isValid()) {
nativeRequestCursorUpdate(mNativeImeAdapterAndroid, immediateRequest, monitorRequest);
}
if (mCursorAnchorInfoController == null) return false;
return mCursorAnchorInfoController.onRequestCursorUpdates(
immediateRequest, monitorRequest, mContainerView);
}
/**
* Notified when a frame has been produced by the renderer and all the associated metadata.
* @param scaleFactor device scale factor.
* @param contentOffsetYPix Y offset below the browser controls.
* @param hasInsertionMarker Whether the insertion marker is visible or not.
* @param insertionMarkerHorizontal X coordinates (in view-local DIP pixels) of the insertion
* marker if it exists. Will be ignored otherwise.
* @param insertionMarkerTop Y coordinates (in view-local DIP pixels) of the top of the
* insertion marker if it exists. Will be ignored otherwise.
* @param insertionMarkerBottom Y coordinates (in view-local DIP pixels) of the bottom of
* the insertion marker if it exists. Will be ignored otherwise.
*/
@CalledByNative
private void updateFrameInfo(float scaleFactor, float contentOffsetYPix,
boolean hasInsertionMarker, boolean isInsertionMarkerVisible,
float insertionMarkerHorizontal, float insertionMarkerTop,
float insertionMarkerBottom) {
if (mCursorAnchorInfoController == null) return;
mCursorAnchorInfoController.onUpdateFrameInfo(scaleFactor, contentOffsetYPix,
hasInsertionMarker, isInsertionMarkerVisible, insertionMarkerHorizontal,
insertionMarkerTop, insertionMarkerBottom, mContainerView);
}
@CalledByNative
private void populateUnderlinesFromSpans(CharSequence text, long underlines) {
if (DEBUG_LOGS) {
Log.i(TAG, "populateUnderlinesFromSpans: text [%s], underlines [%d]", text, 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 (DEBUG_LOGS) Log.i(TAG, "cancelComposition");
if (mInputConnection != null) restartInput();
}
@CalledByNative
private void setCharacterBounds(float[] characterBounds) {
if (mCursorAnchorInfoController == null) return;
mCursorAnchorInfoController.setCompositionCharacterBounds(characterBounds, mContainerView);
}
@CalledByNative
private void onConnectedToRenderProcess() {
if (DEBUG_LOGS) Log.i(TAG, "onConnectedToRenderProcess");
mIsConnected = true;
createInputConnectionFactory();
resetAndHideKeyboard();
}
private native long nativeInit(WebContents webContents);
private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
int type, int modifiers, long timestampMs, int keyCode, int scanCode,
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, CharSequence text, String textStr, int newCursorPosition);
private native void nativeFinishComposingText(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 nativeDeleteSurroundingTextInCodePoints(
long nativeImeAdapterAndroid, int before, int after);
private native boolean nativeRequestTextInputStateUpdate(long nativeImeAdapterAndroid);
private native void nativeRequestCursorUpdate(long nativeImeAdapterAndroid,
boolean immediateRequest, boolean monitorRequest);
}