Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(5)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java

Issue 1589953005: Support InputMethodManager#updateCursorAnchorInfo for Android 5.0 (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Addressed comments in #38 Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java
diff --git a/content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java b/content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java
new file mode 100644
index 0000000000000000000000000000000000000000..443e7d25a4c660083b3906c8b44f6f471bfc4050
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/input/CursorAnchorInfoController.java
@@ -0,0 +1,280 @@
+// Copyright 2016 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.TargetApi;
+import android.graphics.Matrix;
+import android.os.Build;
+import android.view.View;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.InputConnection;
+
+import org.chromium.base.VisibleForTesting;
+import org.chromium.base.annotations.SuppressFBWarnings;
+import org.chromium.content.browser.RenderCoordinates;
+
+import java.util.Arrays;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * A state machine interface which receives Chromium internal events to determines when to call
+ * {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}. This interface is
+ * also used in unit tests to mock out {@link CursorAnchorInfo}, which is available only in
+ * Android 5.0 (Lollipop) and later.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+final class CursorAnchorInfoController {
+ /**
+ * An interface to mock out {@link View#getLocationOnScreen(int[])} for testing.
+ */
+ public interface ViewDelegate {
+ void getLocationOnScreen(View view, int[] location);
+ }
+
+ /**
+ * An interface to mock out composing text retrieval from ImeAdapter.
+ */
+ public interface ComposingTextDelegate {
+ CharSequence getText();
+ int getSelectionStart();
+ int getSelectionEnd();
+ int getComposingTextStart();
+ int getComposingTextEnd();
+ }
+
+ // Current focus and monitoring states.
+ private boolean mIsEditable;
+ private boolean mHasPendingImmediateRequest;
+ private boolean mMonitorModeEnabled;
+
+ // Parmeter for CursorAnchorInfo, updated by setCompositionCharacterBounds.
+ @Nullable
+ private float[] mCompositionCharacterBounds;
+ // Paremeters for CursorAnchorInfo, updated by onUpdateFrameInfo.
+ private boolean mHasCoordinateInfo;
+ private float mScale;
+ private float mTranslationX;
+ private float mTranslationY;
+ private boolean mHasInsertionMarker;
+ private boolean mIsInsertionMarkerVisible;
+ private float mInsertionMarkerHorizontal;
+ private float mInsertionMarkerTop;
+ private float mInsertionMarkerBottom;
+
+ @Nullable
+ private CursorAnchorInfo mLastCursorAnchorInfo;
+
+ @Nonnull
+ private final Matrix mMatrix = new Matrix();
+ @Nonnull
+ private final int[] mViewOrigin = new int[2];
+ @Nonnull
+ private final CursorAnchorInfo.Builder mCursorAnchorInfoBuilder =
+ new CursorAnchorInfo.Builder();
+
+ @Nullable
+ private InputMethodManagerWrapper mInputMethodManagerWrapper;
+ @Nullable
+ private final ComposingTextDelegate mComposingTextDelegate;
+ @Nonnull
+ private final ViewDelegate mViewDelegate;
+
+ private CursorAnchorInfoController(InputMethodManagerWrapper inputMethodManagerWrapper,
+ ComposingTextDelegate composingTextDelegate, ViewDelegate viewDelegate) {
+ mInputMethodManagerWrapper = inputMethodManagerWrapper;
+ mComposingTextDelegate = composingTextDelegate;
+ mViewDelegate = viewDelegate;
+ }
+
+ public static CursorAnchorInfoController create(
+ InputMethodManagerWrapper inputMethodManagerWrapper,
+ ComposingTextDelegate composingTextDelegate) {
+ return new CursorAnchorInfoController(inputMethodManagerWrapper,
+ composingTextDelegate, new ViewDelegate() {
+ @Override
+ public void getLocationOnScreen(View view, int[] location) {
+ view.getLocationOnScreen(location);
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public void setInputMethodManagerWrapperForTest(
+ InputMethodManagerWrapper inputMethodManagerWrapper) {
+ mInputMethodManagerWrapper = inputMethodManagerWrapper;
+ }
+
+ @VisibleForTesting
+ public static CursorAnchorInfoController createForTest(
+ InputMethodManagerWrapper inputMethodManagerWrapper,
+ ComposingTextDelegate composingTextDelegate,
+ ViewDelegate viewDelegate) {
+ return new CursorAnchorInfoController(inputMethodManagerWrapper, composingTextDelegate,
+ viewDelegate);
+ }
+
+ /**
+ * Called by ImeAdapter when a IME related web content state is changed.
+ */
+ public void invalidateLastCursorAnchorInfo() {
+ if (!mIsEditable) return;
+
+ mLastCursorAnchorInfo = null;
+ }
+
+ /**
+ * 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 (!mIsEditable) return;
+
+ if (!Arrays.equals(compositionCharacterBounds, mCompositionCharacterBounds)) {
+ mLastCursorAnchorInfo = null;
+ mCompositionCharacterBounds = compositionCharacterBounds;
+ }
+ }
+
+ /**
+ * Sets coordinates system parameters and selection marker information.
+ * @param hasInsertionMarker {@code true} if the insertion marker exists.
+ * @param isInsertionMarkerVisible {@code true} if the insertion insertion marker is visible.
+ * @param insertionMarkerHorizontal X coordinate of the top of the first selection marker.
+ * @param insertionMarkerTop Y coordinate of the top of the first selection marker.
+ * @param insertionMarkerBottom Y coordinate of the bottom of the first selection marker.
+ * @param view The attached view.
+ */
+ @SuppressFBWarnings("FE_FLOATING_POINT_EQUALITY")
+ public void onUpdateFrameInfo(@Nonnull RenderCoordinates renderCoordinates,
+ boolean hasInsertionMarker, boolean isInsertionMarkerVisible,
+ float insertionMarkerHorizontal, float insertionMarkerTop,
+ float insertionMarkerBottom, @Nonnull View view) {
+ if (!mIsEditable) return;
+
+ // Reuse {@param #mViewOrigin} to avoid object creation, as this method is supposed to be
+ // called at relatively high rate.
+ mViewDelegate.getLocationOnScreen(view, mViewOrigin);
+
+ // Character bounds and insertion marker locations come in device independent pixels
+ // relative from the top-left corner of the web view content area. (In other words, the
+ // effects of various kinds of zooming and scrolling are already taken into account.)
+ //
+ // We need to prepare parameters that convert such values to physical pixels, in the
+ // screen coordinate. Hence the following values are derived.
+ float scale = renderCoordinates.getDeviceScaleFactor();
+ float translationX = mViewOrigin[0];
+ float translationY = mViewOrigin[1] + renderCoordinates.getContentOffsetYPix();
+
+ if (!mHasCoordinateInfo
+ || scale != mScale
+ || translationX != mTranslationX
+ || translationY != mTranslationY
+ || hasInsertionMarker != mHasInsertionMarker
+ || isInsertionMarkerVisible != mIsInsertionMarkerVisible
+ || insertionMarkerHorizontal != mInsertionMarkerHorizontal
+ || insertionMarkerTop != mInsertionMarkerTop
+ || insertionMarkerBottom != mInsertionMarkerBottom) {
+ mLastCursorAnchorInfo = null;
+ mHasCoordinateInfo = true;
+ mScale = scale;
+ mTranslationX = translationX;
+ mTranslationY = translationY;
+ mHasInsertionMarker = hasInsertionMarker;
+ mIsInsertionMarkerVisible = isInsertionMarkerVisible;
+ mInsertionMarkerHorizontal = insertionMarkerHorizontal;
+ mInsertionMarkerTop = insertionMarkerTop;
+ mInsertionMarkerBottom = insertionMarkerBottom;
+ }
+
+ // Notify to IME if there is a pending request, or if it is in monitor mode and we have
+ // some change in the state.
+ if (mHasPendingImmediateRequest
+ || (mMonitorModeEnabled && mLastCursorAnchorInfo == null)) {
+ updateCursorAnchorInfo(view);
+ }
+ }
+
+ /**
+ * Resets the current state on update monitoring mode to the default (= do nothing.)
+ */
+ public void resetMonitoringState() {
+ mMonitorModeEnabled = false;
+ mHasPendingImmediateRequest = false;
+ }
+
+ public void focusedNodeChanged(boolean isEditable) {
+ mIsEditable = isEditable;
+ mCompositionCharacterBounds = null;
+ mHasCoordinateInfo = false;
+ mLastCursorAnchorInfo = null;
+ }
+
+ public boolean onRequestCursorUpdates(int cursorUpdateMode, View view) {
+ if (!mIsEditable) return false;
+
+ mMonitorModeEnabled = (cursorUpdateMode & InputConnection.CURSOR_UPDATE_MONITOR) != 0;
+ if ((cursorUpdateMode & InputConnection.CURSOR_UPDATE_IMMEDIATE) != 0) {
+ mHasPendingImmediateRequest = true;
+ updateCursorAnchorInfo(view);
+ }
+ return true;
+ }
+
+ /**
+ * Computes the CursorAnchorInfo instance and notify to InputMethodManager if needed.
+ */
+ private void updateCursorAnchorInfo(View view) {
+ if (!mHasCoordinateInfo) return;
+
+ if (mLastCursorAnchorInfo == null) {
+ // Reuse the builder object.
+ mCursorAnchorInfoBuilder.reset();
+
+ CharSequence text = mComposingTextDelegate.getText();
+ int selectionStart = mComposingTextDelegate.getSelectionStart();
+ int selectionEnd = mComposingTextDelegate.getSelectionEnd();
+ int composingTextStart = mComposingTextDelegate.getComposingTextStart();
+ int composingTextEnd = mComposingTextDelegate.getComposingTextEnd();
+ if (text != null && 0 <= composingTextStart && composingTextEnd <= text.length()) {
+ mCursorAnchorInfoBuilder.setComposingText(composingTextStart,
+ text.subSequence(composingTextStart, composingTextEnd));
+ float[] compositionCharacterBounds = mCompositionCharacterBounds;
+ if (compositionCharacterBounds != null) {
+ int numCharacter = compositionCharacterBounds.length / 4;
+ for (int i = 0; i < numCharacter; ++i) {
+ float left = compositionCharacterBounds[i * 4];
+ float top = compositionCharacterBounds[i * 4 + 1];
+ float right = compositionCharacterBounds[i * 4 + 2];
+ float bottom = compositionCharacterBounds[i * 4 + 3];
+ int charIndex = composingTextStart + i;
+ mCursorAnchorInfoBuilder.addCharacterBounds(charIndex, left, top, right,
+ bottom, CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION);
+ }
+ }
+ }
+ mCursorAnchorInfoBuilder.setSelectionRange(selectionStart, selectionEnd);
+ mMatrix.setScale(mScale, mScale);
+ mMatrix.postTranslate(mTranslationX, mTranslationY);
+ mCursorAnchorInfoBuilder.setMatrix(mMatrix);
+ if (mHasInsertionMarker) {
+ mCursorAnchorInfoBuilder.setInsertionMarkerLocation(
+ mInsertionMarkerHorizontal,
+ mInsertionMarkerTop,
+ mInsertionMarkerBottom,
+ mInsertionMarkerBottom,
+ mIsInsertionMarkerVisible ? CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION :
+ CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION);
+ }
+ mLastCursorAnchorInfo = mCursorAnchorInfoBuilder.build();
+ }
+
+ if (mInputMethodManagerWrapper != null) {
+ mInputMethodManagerWrapper.updateCursorAnchorInfo(view, mLastCursorAnchorInfo);
+ }
+ mHasPendingImmediateRequest = false;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698