Chromium Code Reviews| Index: content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
| diff --git a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
| index e075d8e4d400dbef2f502c3f262b3455eed249a6..9a596088a15e4fabb2104f9857870955cfdec081 100644 |
| --- a/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
| +++ b/content/public/android/java/src/org/chromium/content/browser/input/ImeAdapter.java |
| @@ -4,6 +4,9 @@ |
| package org.chromium.content.browser.input; |
| +import android.annotation.TargetApi; |
| +import android.graphics.Matrix; |
| +import android.os.Build; |
| import android.os.Handler; |
| import android.os.ResultReceiver; |
| import android.os.SystemClock; |
| @@ -15,14 +18,22 @@ import android.text.style.UnderlineSpan; |
| import android.view.KeyCharacterMap; |
| import android.view.KeyEvent; |
| import android.view.View; |
| +import android.view.inputmethod.CursorAnchorInfo; |
| import android.view.inputmethod.EditorInfo; |
| +import android.view.inputmethod.InputConnection; |
| +import android.view.inputmethod.InputMethodManager; |
| import org.chromium.base.CalledByNative; |
| import org.chromium.base.JNINamespace; |
| import org.chromium.base.VisibleForTesting; |
| +import org.chromium.cc.SelectionBoundType; |
| import org.chromium.ui.picker.InputDialogContainer; |
| import java.lang.CharSequence; |
| +import java.util.Arrays; |
| + |
| +import javax.annotation.Nonnull; |
| +import javax.annotation.Nullable; |
| /** |
| * Adapts and plumbs android IME service onto the chrome text input API. |
| @@ -143,6 +154,302 @@ public class ImeAdapter { |
| private int mTextInputFlags; |
| private String mLastComposeText; |
| + /** |
| + * Minimum data set that is required to construct {@link CursorAnchorInfo} object. |
|
jdduke (slow)
2014/11/12 19:05:06
This is a non-trivial code addition, would it make
yukawa
2014/12/05 07:42:19
Sounds reasonable. Now the logic here is complete
|
| + */ |
| + private static final class CursorAnchorInfoSource { |
| + /** |
| + * Special sequence ID that is never returned from {@link #getSequenceNo()}. |
| + */ |
| + public static int INVALID_SEQUENCE_NO = 0; |
| + |
| + private boolean mHasFrameInfo = false; |
| + private String mText; |
| + private int mSelectionStart; |
| + private int mSelectionEnd; |
| + private int mCompositionStart; |
| + private int mCompositionEnd; |
| + private float[] mCompositionCharacterBounds; |
| + private boolean mHasInsertionMarker; |
| + private float mInsertionMarkerHorizontal; |
| + private float mInsertionMarkerTop; |
| + private float mInsertionMarkerBottom; |
| + private float mScale; |
| + private float mTranslationX; |
| + private float mTranslationY; |
| + private int mSequenceNo = INVALID_SEQUENCE_NO + 1; |
| + |
| + |
| + /** |
| + * Resets the internal state. |
| + */ |
| + public void reset() { |
| + mHasFrameInfo = false; |
| + mText = null; |
| + mSelectionStart = -1; |
| + mSelectionEnd = -1; |
| + mCompositionStart = -1; |
| + mCompositionEnd = -1; |
| + mCompositionCharacterBounds = null; |
| + mHasInsertionMarker = false; |
| + mInsertionMarkerHorizontal = Float.NaN; |
| + mInsertionMarkerTop = Float.NaN; |
| + mInsertionMarkerBottom = Float.NaN; |
| + mScale = 1.0f; |
| + mTranslationX = 0.0f; |
| + mTranslationY = 0.0f; |
| + } |
| + |
| + /** |
| + * @return {@code true} if |
| + * {@link #setFrameInfo(float, float, float, int, float, float, float)} was called after |
| + * the object was instantiated or reinitialized by {@link #reset()}. |
| + */ |
|
jdduke (slow)
2015/01/21 19:16:42
Do these accessors need public visibility? Where a
|
| + public boolean hasFrameInfo() { |
| + return mHasFrameInfo; |
| + } |
| + |
| + /** |
| + * @return Current composing text (if any). {@code null}. |
| + */ |
| + @Nullable |
| + public CharSequence getComposingText() { |
| + if (mText == null) { |
| + return null; |
| + } |
| + if (0 <= mCompositionStart && mCompositionStart <= mText.length()) { |
| + return mText.subSequence(mCompositionStart, mCompositionEnd); |
| + } |
| + return ""; |
| + } |
| + |
| + /** |
| + * @return Selection start index (inclusive) that was specified in |
| + * {@link #setTextAndSelection(String, int, int, int, int)}. |
| + */ |
| + public int getSelectionStart() { |
| + return mSelectionStart; |
| + } |
| + |
| + /** |
| + * @return Selection end index (exclusive) that was specified in |
| + * {@link #setTextAndSelection(String, int, int, int, int)}. |
| + */ |
| + public int getSelectionEnd() { |
| + return mSelectionEnd; |
| + } |
| + |
| + /** |
| + * @return Composing text start index (inclusive) that was specified in |
| + * {@link #setTextAndSelection(String, int, int, int, int)}. |
| + */ |
| + public int getCompositionStart() { |
| + return mCompositionStart; |
| + } |
| + |
| + /** |
| + * @return Array of local coordinates for each character bounds in the composing text. |
| + */ |
| + @Nullable |
| + public float[] getCompositionCharacterBounds() { |
| + return mCompositionCharacterBounds; |
| + } |
| + |
| + /** |
| + * @return {@code true} if an insertion marker is displayed. |
| + */ |
| + public boolean hasInsertionMarker() { |
| + return mHasInsertionMarker; |
| + } |
| + |
| + /** |
| + * @return Horizontal position of the text insertion marker, in local coordinates. |
| + */ |
| + public float getInsertionMarkerHorizontal() { |
| + return mInsertionMarkerHorizontal; |
| + } |
| + |
| + /** |
| + * @return Top position of the text insertion marker, in local coordinates. |
| + */ |
| + public float getInsertionMarkerTop() { |
| + return mInsertionMarkerTop; |
| + } |
| + |
| + /** |
| + * @return Bottom position of the text insertion marker, in local coordinates. |
| + */ |
| + public float getInsertionMarkerBottom() { |
| + return mInsertionMarkerBottom; |
| + } |
| + |
| + /** |
| + * @return Scaling factor from CSS (document) coordinates to Physical (screen) coordinates. |
| + */ |
| + public float getScale() { |
| + return mScale; |
| + } |
| + |
| + /** |
| + * @return Horizontal offset from CSS (document) coordinates to View-local Physical (screen) |
| + * coordinates. To obtain the display coordinates, add an offset returned from |
| + * {@link View#getLocationOnScreen(int[])}. |
| + */ |
| + public float getTranslationX() { |
| + return mTranslationX; |
| + } |
| + |
| + /** |
| + * @return Vertical offset from CSS (document) coordinates to View-local Physical (screen) |
| + * coordinates. To obtain the display coordinates, add an offset returned from |
| + * {@link View#getLocationOnScreen(int[])}. |
| + */ |
| + public float getTranslationY() { |
| + return mTranslationY; |
| + } |
| + |
| + /** |
| + * @return The sequence number that will be updated when any of the fields is changed. |
| + * Never returns {@link #INVALID_SEQUENCE_NO}. |
| + */ |
| + public int getSequenceNo() { |
| + return mSequenceNo; |
| + } |
| + |
| + private void incrementSequenceNo() { |
| + ++mSequenceNo; |
| + if (mSequenceNo == INVALID_SEQUENCE_NO) { |
| + ++mSequenceNo; |
| + } |
| + } |
| + |
| + /** |
| + * Sets positional information of composing text as an array of character bounds. |
| + * @param compositionCharacterBounds Array of character bounds in local coordinates. |
| + */ |
| + public void setCompositionCharacterBounds(float[] compositionCharacterBounds) { |
| + if (!Arrays.equals(compositionCharacterBounds, mCompositionCharacterBounds)) { |
| + incrementSequenceNo(); |
| + } |
| + mCompositionCharacterBounds = compositionCharacterBounds; |
| + } |
| + |
| + /** |
| + * Sets text in the focused text area, selection range, and the composing text range. |
| + * @param text Text in the focused text field. |
| + * @param selectionStart Index where the text selection starts. {@code -1} if there is no |
| + * selection. |
| + * @param selectionEnd Index where the text selection ends. {@code -1} if there is no |
| + * selection. |
| + * @param compositionStart Index where the text composition starts. {@code -1} if there is |
| + * no selection. |
| + * @param compositionEnd Index where the text composition ends. {@code -1} if there is no |
| + * selection. |
| + */ |
| + public void setTextAndSelection(String text, int selectionStart, int selectionEnd, |
| + int compositionStart, int compositionEnd) { |
| + if (text != mText || selectionStart != mSelectionStart || selectionEnd != mSelectionEnd |
| + || compositionStart != mCompositionStart || compositionEnd != mCompositionEnd) { |
| + incrementSequenceNo(); |
| + } |
| + mText = text; |
| + mSelectionStart = selectionStart; |
| + mSelectionEnd = selectionEnd; |
| + mCompositionStart = compositionStart; |
| + mCompositionEnd = compositionEnd; |
| + } |
| + |
| + /** |
| + * Sets coordinates system parameters and selection marker information. |
| + * @param scale Scaling factor from CSS (document) coordinates to Physical (screen) |
| + * coordinates. |
| + * @param translationX Horizontal offset from CSS (document) coordinates to |
| + * View-local Physical (screen) coordinates. |
| + * @param translationY Vertical offset from CSS (document) coordinates to View-local |
| + * Physical (screen) coordinates. |
| + * @param frameMetadataSelectionStartType Type code that is defined in |
| + * {@link SelectionBoundType} |
| + * @param frameMetadataSelectionStartEdgeTopX X coordinate of the top of the first selection |
| + * marker. |
| + * @param frameMetadataSelectionStartEdgeTopY Y coordinate of the top of the first selection |
| + * marker. |
| + * @param frameMetadataSelectionStartEdgeBottomY Y coordinate of the bottom of the first |
| + * selection marker. |
| + */ |
| + public void setFrameInfo(float scale, float translationX, float translationY, |
| + int frameMetadataSelectionStartType, float frameMetadataSelectionStartEdgeTopX, |
| + float frameMetadataSelectionStartEdgeTopY, |
| + float frameMetadataSelectionStartEdgeBottomY) { |
| + // We are interested only in zero width selection bounds here because non-zero width |
| + // selection bounds cannot be represented in CursorAnchorInfo API in Android Framework |
| + // as of API Level 21. Actually supporting non-zero width selection bounds in |
| + // CursorAnchorInfo API was once considered in the design phase of that API, but the |
| + // idea was abandoned because the IME is still able to retrieve the same information |
| + // from the following parameters in |
| + // CursorAnchorInfo: |
| + // - CursorAnchorInfo#getCharacterBounds and |
| + // - CursorAnchorInfo#getSelection{Start, End}. |
| + final boolean hasInsertionMarker = |
| + frameMetadataSelectionStartType == SelectionBoundType.SELECTION_BOUND_CENTER; |
| + if (scale != mScale || translationX != mTranslationX || translationY != mTranslationY) { |
| + incrementSequenceNo(); |
| + } else if (hasInsertionMarker) { |
| + if (!mHasInsertionMarker |
| + || frameMetadataSelectionStartEdgeTopX != mInsertionMarkerHorizontal |
| + || frameMetadataSelectionStartEdgeTopY != mInsertionMarkerTop |
| + || frameMetadataSelectionStartEdgeBottomY != mInsertionMarkerBottom) { |
| + incrementSequenceNo(); |
| + } |
| + } else if (mHasInsertionMarker) { |
| + incrementSequenceNo(); |
| + } |
| + mHasInsertionMarker = hasInsertionMarker; |
| + if (mHasInsertionMarker) { |
| + mInsertionMarkerHorizontal = frameMetadataSelectionStartEdgeTopX; |
| + mInsertionMarkerTop = frameMetadataSelectionStartEdgeTopY; |
| + mInsertionMarkerBottom = frameMetadataSelectionStartEdgeBottomY; |
| + } |
| + mScale = scale; |
| + mTranslationX = translationX; |
| + mTranslationY = translationY; |
| + mHasFrameInfo = true; |
| + } |
| + } |
| + private CursorAnchorInfoSource mCursorAnchorInfoSource = new CursorAnchorInfoSource(); |
| + |
| + private static class CursorAnchorInfoBuilderWrapper { |
| + |
| + public static boolean isSupported() { |
| + return false; |
| + } |
| + |
| + public static CursorAnchorInfoBuilderWrapper create() { |
| + return new CursorAnchorInfoBuilderWrapper(); |
| + } |
| + |
| + protected CursorAnchorInfoBuilderWrapper() { |
| + } |
| + |
| + /** |
| + * Calls {@link InputConnection#updateCursorAnchorInfo(View, CursorAnchorInfo)} iff the API |
| + * is available and the current IME actually requested it. |
| + * @param inputMethodManagerWrapper Compatibility wrapper of {@link InputConnection}. |
| + * @param source Data source from which {@link CursorAnchorInfo} is created. |
| + * @param view {@link View} object that should be passed to |
| + * {@link InputConnection#updateCursorAnchorInfo(View, CursorAnchorInfo)}. |
| + */ |
| + @SuppressWarnings("unused") |
| + public void send(@Nonnull InputMethodManagerWrapper inputMethodManagerWrapper, |
|
jdduke (slow)
2015/01/21 19:16:41
I don't quite understand this function?
|
| + @Nonnull CursorAnchorInfoSource source, @Nonnull View view) { |
| + // This is a stub for platforms that do not support CursorAnchorInfo. |
| + } |
| + } |
| + |
| + private final CursorAnchorInfoBuilderWrapper mCursorAnchorInfoBuilder = |
| + CursorAnchorInfoBuilderWrapper.create(); |
| + private boolean mCursorAnchorInfoMonitorEnabled = false; |
| + private boolean mOneshotUpdateCursorAnchorInfoEnabled = false; |
| + |
| @VisibleForTesting |
| int mLastSyntheticKeyCode; |
| @@ -621,6 +928,75 @@ public class ImeAdapter { |
| return true; |
| } |
| + /** |
| + * @return Whether the {@link CursorAnchorInfo} is available or not on this device. |
| + */ |
| + public boolean isCursorAnchorInfoSupported() { |
| + return CursorAnchorInfoBuilderWrapper.isSupported(); |
| + } |
| + |
| + /** |
| + * Start or stop calling |
| + * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}. |
| + * @param enabled {@code true} if {@link InputConnection#requestCursorUpdates(int)} is called |
| + * with {@link InputConnection#CURSOR_UPDATE_MONITOR} bit. |
| + */ |
| + public void setCursorAnchorInfoMonitorEnabled(boolean enabled) { |
| + mCursorAnchorInfoMonitorEnabled = enabled; |
| + } |
| + |
| + /** |
| + * Make sure {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} is called |
| + * at least once immediately when the latest position becomes available. |
| + * @param view The view to be passed to |
| + * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} if the latest |
| + * position is already available. Otherwise ignored. |
| + */ |
| + public void scheduleOneshotUpdateCursorAnchorInfo(View view) { |
| + if (!mCursorAnchorInfoSource.hasFrameInfo()) { |
|
jdduke (slow)
2015/01/21 19:16:41
Where is this method called?
|
| + // If there is no frame info, send the data later when it becomes available. |
| + mOneshotUpdateCursorAnchorInfoEnabled = true; |
| + return; |
| + } |
| + mCursorAnchorInfoBuilder.send(mInputMethodManagerWrapper, mCursorAnchorInfoSource, |
| + mViewEmbedder.getAttachedView()); |
| + mOneshotUpdateCursorAnchorInfoEnabled = false; |
| + } |
| + |
| + /** |
| + * Update the parameters that are to be passed to the IME through |
| + * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}. |
| + * @param text Text in the focused text field. |
| + * @param selectionStart Index where the text selection starts. {@code -1} if there is no |
| + * selection. |
| + * @param selectionEnd Index where the text selection ends. {@code -1} if there is no |
| + * selection. |
| + * @param compositionStart Index where the text composition starts. {@code -1} if there is no |
| + * selection. |
| + * @param compositionEnd Index where the text composition ends. {@code -1} if there is no |
| + * selection. |
| + */ |
| + public void updateTextAndSelection(String text, int selectionStart, int selectionEnd, |
| + int compositionStart, int compositionEnd) { |
| + mCursorAnchorInfoSource.setTextAndSelection(text, selectionStart, selectionEnd, |
| + compositionStart, compositionEnd); |
| + } |
| + |
| + public void onUpdateFrameInfo(float scale, float translationX, float translationY, |
| + int frameMetadataSelectionStartType, float frameMetadataSelectionStartEdgeTopX, |
| + float frameMetadataSelectionStartEdgeTopY, |
| + float frameMetadataSelectionStartEdgeBottomY) { |
| + mCursorAnchorInfoSource.setFrameInfo(scale, translationX, translationY, |
| + frameMetadataSelectionStartType, frameMetadataSelectionStartEdgeTopX, |
| + frameMetadataSelectionStartEdgeTopY, frameMetadataSelectionStartEdgeBottomY); |
| + if (!mCursorAnchorInfoMonitorEnabled && !mOneshotUpdateCursorAnchorInfoEnabled) { |
| + return; |
| + } |
| + mCursorAnchorInfoBuilder.send(mInputMethodManagerWrapper, mCursorAnchorInfoSource, |
| + mViewEmbedder.getAttachedView()); |
| + mOneshotUpdateCursorAnchorInfoEnabled = false; |
| + } |
| + |
| // Calls from C++ to Java |
| @CalledByNative |
| @@ -699,7 +1075,7 @@ public class ImeAdapter { |
| @CalledByNative |
| private void setCharacterBounds(float[] characterBounds) { |
| - // TODO(yukawa): Implement this. |
| + mCursorAnchorInfoSource.setCompositionCharacterBounds(characterBounds); |
| } |
| @CalledByNative |
| @@ -710,6 +1086,7 @@ public class ImeAdapter { |
| } |
| mNativeImeAdapterAndroid = 0; |
| mTextInputType = 0; |
| + mCursorAnchorInfoSource.reset(); |
| } |
| private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid, |