From c2ddd6023688db5ecf6c586e05f55e262b4a802e Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Tue, 6 May 2014 21:22:49 +0900 Subject: [PATCH] Introduce new API for floating window support This CL introduces a new API IMM#updateCursorAnchorInfo for floating window support. BUG: 14579622 Change-Id: I61dec2f8fa671ba891da1d4af08975750e3acb04 --- api/current.txt | 32 ++ .../IInputMethodSessionWrapper.java | 12 + .../InputMethodService.java | 23 + .../view/inputmethod/CorrectionInfo.java | 19 +- .../view/inputmethod/CursorAnchorInfo.aidl | 19 + .../view/inputmethod/CursorAnchorInfo.java | 449 ++++++++++++++++++ .../view/inputmethod/InputMethodManager.java | 29 +- .../view/inputmethod/InputMethodSession.java | 12 +- .../view/inputmethod/SparseRectFArray.java | 265 +++++++++++ .../internal/view/IInputMethodSession.aidl | 3 + .../run_core_inputmethod_test.sh | 2 +- .../src/android/os/CursorAnchorInfoTest.java | 164 +++++++ .../src/android/os/SparseRectFArrayTest.java | 233 +++++++++ 13 files changed, 1249 insertions(+), 13 deletions(-) create mode 100644 core/java/android/view/inputmethod/CursorAnchorInfo.aidl create mode 100644 core/java/android/view/inputmethod/CursorAnchorInfo.java create mode 100644 core/java/android/view/inputmethod/SparseRectFArray.java create mode 100644 core/tests/inputmethodtests/src/android/os/CursorAnchorInfoTest.java create mode 100644 core/tests/inputmethodtests/src/android/os/SparseRectFArrayTest.java diff --git a/api/current.txt b/api/current.txt index edc8700e63caf..278db11444971 100644 --- a/api/current.txt +++ b/api/current.txt @@ -12867,6 +12867,7 @@ package android.inputmethodservice { method public void onStartInputView(android.view.inputmethod.EditorInfo, boolean); method public void onUnbindInput(); method public void onUpdateCursor(android.graphics.Rect); + method public void onUpdateCursorAnchorInfo(android.view.inputmethod.CursorAnchorInfo); method public void onUpdateExtractedText(int, android.view.inputmethod.ExtractedText); method public void onUpdateExtractingViews(android.view.inputmethod.EditorInfo); method public void onUpdateExtractingVisibility(android.view.inputmethod.EditorInfo); @@ -12916,6 +12917,7 @@ package android.inputmethodservice { method public void finishInput(); method public void toggleSoftInput(int, int); method public void updateCursor(android.graphics.Rect); + method public void updateCursorAnchorInfo(android.view.inputmethod.CursorAnchorInfo); method public void updateExtractedText(int, android.view.inputmethod.ExtractedText); method public void updateSelection(int, int, int, int, int, int); method public void viewClicked(boolean); @@ -32548,6 +32550,34 @@ package android.view.inputmethod { field public static final android.os.Parcelable.Creator CREATOR; } + public final class CursorAnchorInfo implements android.os.Parcelable { + ctor public CursorAnchorInfo(android.os.Parcel); + method public int describeContents(); + method public int getCandidatesEnd(); + method public int getCandidatesStart(); + method public android.graphics.RectF getCharacterRect(int); + method public float getInsertionMarkerBaseline(); + method public float getInsertionMarkerBottom(); + method public float getInsertionMarkerHorizontal(); + method public float getInsertionMarkerTop(); + method public android.graphics.Matrix getMatrix(); + method public int getSelectionEnd(); + method public int getSelectionStart(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class CursorAnchorInfo.CursorAnchorInfoBuilder { + ctor public CursorAnchorInfo.CursorAnchorInfoBuilder(); + method public android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder addCharacterRect(int, float, float, float, float); + method public android.view.inputmethod.CursorAnchorInfo build(); + method public void reset(); + method public android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder setCandidateRange(int, int); + method public android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder setInsertionMarkerLocation(float, float, float, float); + method public android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder setMatrix(android.graphics.Matrix); + method public android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder setSelectionRange(int, int); + } + public class EditorInfo implements android.text.InputType android.os.Parcelable { ctor public EditorInfo(); method public int describeContents(); @@ -32756,6 +32786,7 @@ package android.view.inputmethod { method public void toggleSoftInput(int, int); method public void toggleSoftInputFromWindow(android.os.IBinder, int, int); method public void updateCursor(android.view.View, int, int, int, int); + method public void updateCursorAnchorInfo(android.view.View, android.view.inputmethod.CursorAnchorInfo); method public void updateExtractedText(android.view.View, int, android.view.inputmethod.ExtractedText); method public void updateSelection(android.view.View, int, int, int, int); method public void viewClicked(android.view.View); @@ -32778,6 +32809,7 @@ package android.view.inputmethod { method public abstract void finishInput(); method public abstract void toggleSoftInput(int, int); method public abstract void updateCursor(android.graphics.Rect); + method public abstract void updateCursorAnchorInfo(android.view.inputmethod.CursorAnchorInfo); method public abstract void updateExtractedText(int, android.view.inputmethod.ExtractedText); method public abstract void updateSelection(int, int, int, int, int, int); method public abstract void viewClicked(boolean); diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index 8437228257a09..ed223d1fd503d 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -36,6 +36,7 @@ import android.view.MotionEvent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.InputMethodSession; +import android.view.inputmethod.CursorAnchorInfo; class IInputMethodSessionWrapper extends IInputMethodSession.Stub implements HandlerCaller.Callback { @@ -46,6 +47,7 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub private static final int DO_UPDATE_EXTRACTED_TEXT = 67; private static final int DO_UPDATE_SELECTION = 90; private static final int DO_UPDATE_CURSOR = 95; + private static final int DO_UPDATE_CURSOR_ANCHOR_INFO = 99; private static final int DO_APP_PRIVATE_COMMAND = 100; private static final int DO_TOGGLE_SOFT_INPUT = 105; private static final int DO_FINISH_SESSION = 110; @@ -108,6 +110,10 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mInputMethodSession.updateCursor((Rect)msg.obj); return; } + case DO_UPDATE_CURSOR_ANCHOR_INFO: { + mInputMethodSession.updateCursorAnchorInfo((CursorAnchorInfo)msg.obj); + return; + } case DO_APP_PRIVATE_COMMAND: { SomeArgs args = (SomeArgs)msg.obj; mInputMethodSession.appPrivateCommand((String)args.arg1, @@ -180,6 +186,12 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub mCaller.obtainMessageO(DO_UPDATE_CURSOR, newCursor)); } + @Override + public void updateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_UPDATE_CURSOR_ANCHOR_INFO, cursorAnchorInfo)); + } + @Override public void appPrivateCommand(String action, Bundle data) { mCaller.executeOrSendMessage( diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index f6438b444cb5a..4bccaf1d6f523 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -51,6 +51,7 @@ import android.view.WindowManager; import android.view.WindowManager.BadTokenException; import android.view.animation.AnimationUtils; import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -545,6 +546,17 @@ public class InputMethodService extends AbstractInputMethodService { public void toggleSoftInput(int showFlags, int hideFlags) { InputMethodService.this.onToggleSoftInput(showFlags, hideFlags); } + + /** + * Call {@link InputMethodService#onUpdateCursorAnchorInfo + * InputMethodService.onUpdateCursorAnchorInfo()}. + */ + public void updateCursorAnchorInfo(CursorAnchorInfo info) { + if (!isEnabled()) { + return; + } + InputMethodService.this.onUpdateCursorAnchorInfo(info); + } } /** @@ -1716,6 +1728,17 @@ public class InputMethodService extends AbstractInputMethodService { // Intentionally empty } + /** + * Called when the application has reported a new location of its text insertion point and + * characters in the composition string. This is only called if explicitly requested by the + * input method. The default implementation does nothing. + * @param cursorAnchorInfo The positional information of the text insertion point and the + * composition string. + */ + public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) { + // Intentionally empty + } + /** * Update the cursor/anthor monitor mode. */ diff --git a/core/java/android/view/inputmethod/CorrectionInfo.java b/core/java/android/view/inputmethod/CorrectionInfo.java index 1b04e4980588a..a43dfe89cee14 100644 --- a/core/java/android/view/inputmethod/CorrectionInfo.java +++ b/core/java/android/view/inputmethod/CorrectionInfo.java @@ -88,16 +88,15 @@ public final class CorrectionInfo implements Parcelable { /** * Used to make this class parcelable. */ - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - public CorrectionInfo createFromParcel(Parcel source) { - return new CorrectionInfo(source); - } - - public CorrectionInfo[] newArray(int size) { - return new CorrectionInfo[size]; - } - }; + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public CorrectionInfo createFromParcel(Parcel source) { + return new CorrectionInfo(source); + } + public CorrectionInfo[] newArray(int size) { + return new CorrectionInfo[size]; + } + }; public int describeContents() { return 0; diff --git a/core/java/android/view/inputmethod/CursorAnchorInfo.aidl b/core/java/android/view/inputmethod/CursorAnchorInfo.aidl new file mode 100644 index 0000000000000..2ee9edbd1b31d --- /dev/null +++ b/core/java/android/view/inputmethod/CursorAnchorInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +parcelable CursorAnchorInfo; diff --git a/core/java/android/view/inputmethod/CursorAnchorInfo.java b/core/java/android/view/inputmethod/CursorAnchorInfo.java new file mode 100644 index 0000000000000..92455df900b17 --- /dev/null +++ b/core/java/android/view/inputmethod/CursorAnchorInfo.java @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.Layout; +import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder; + +import java.util.Objects; + +/** + * Positional information about the text insertion point and characters in the composition string. + * + *

This class encapsulates locations of the text insertion point and the composition string in + * the screen coordinates so that IMEs can render their UI components near where the text is + * actually inserted.

+ */ +public final class CursorAnchorInfo implements Parcelable { + private final int mSelectionStart; + private final int mSelectionEnd; + private final int mCandidatesStart; + private final int mCandidatesEnd; + + /** + * Horizontal position of the insertion marker, in the local coordinates that will be + * transformed with the transformation matrix when rendered on the screen. This should be + * calculated or compatible with {@link Layout#getPrimaryHorizontal(int)}. This can be + * {@code java.lang.Float.NaN} when no value is specified. + */ + private final float mInsertionMarkerHorizontal; + /** + * Vertical position of the insertion marker, in the local coordinates that will be + * transformed with the transformation matrix when rendered on the screen. This should be + * calculated or compatible with {@link Layout#getLineTop(int)}. This can be + * {@code java.lang.Float.NaN} when no value is specified. + */ + private final float mInsertionMarkerTop; + /** + * Vertical position of the insertion marker, in the local coordinates that will be + * transformed with the transformation matrix when rendered on the screen. This should be + * calculated or compatible with {@link Layout#getLineBaseline(int)}. This can be + * {@code java.lang.Float.NaN} when no value is specified. + */ + private final float mInsertionMarkerBaseline; + /** + * Vertical position of the insertion marker, in the local coordinates that will be + * transformed with the transformation matrix when rendered on the screen. This should be + * calculated or compatible with {@link Layout#getLineBottom(int)}. This can be + * {@code java.lang.Float.NaN} when no value is specified. + */ + private final float mInsertionMarkerBottom; + + /** + * Container of rectangular position of characters, keyed with character index in a unit of + * Java chars, in the local coordinates that will be transformed with the transformation matrix + * when rendered on the screen. + */ + private final SparseRectFArray mCharacterRects; + + /** + * Transformation matrix that is applied to any positional information of this class to + * transform local coordinates into screen coordinates. + */ + private final Matrix mMatrix; + + public CursorAnchorInfo(final Parcel source) { + mSelectionStart = source.readInt(); + mSelectionEnd = source.readInt(); + mCandidatesStart = source.readInt(); + mCandidatesEnd = source.readInt(); + mInsertionMarkerHorizontal = source.readFloat(); + mInsertionMarkerTop = source.readFloat(); + mInsertionMarkerBaseline = source.readFloat(); + mInsertionMarkerBottom = source.readFloat(); + mCharacterRects = source.readParcelable(SparseRectFArray.class.getClassLoader()); + mMatrix = new Matrix(); + mMatrix.setValues(source.createFloatArray()); + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mSelectionStart); + dest.writeInt(mSelectionEnd); + dest.writeInt(mCandidatesStart); + dest.writeInt(mCandidatesEnd); + dest.writeFloat(mInsertionMarkerHorizontal); + dest.writeFloat(mInsertionMarkerTop); + dest.writeFloat(mInsertionMarkerBaseline); + dest.writeFloat(mInsertionMarkerBottom); + dest.writeParcelable(mCharacterRects, flags); + final float[] matrixArray = new float[9]; + mMatrix.getValues(matrixArray); + dest.writeFloatArray(matrixArray); + } + + @Override + public int hashCode(){ + // TODO: Improve the hash function. + final float floatHash = mSelectionStart + mSelectionEnd + mCandidatesStart + mCandidatesEnd + + mInsertionMarkerHorizontal + mInsertionMarkerTop + mInsertionMarkerBaseline + + mInsertionMarkerBottom; + int hash = floatHash > 0 ? (int) floatHash : (int)(-floatHash); + if (mCharacterRects != null) { + hash += mCharacterRects.hashCode(); + } + hash += mMatrix.hashCode(); + return hash; + } + + @Override + public boolean equals(Object obj){ + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof CursorAnchorInfo)) { + return false; + } + final CursorAnchorInfo that = (CursorAnchorInfo) obj; + if (hashCode() != that.hashCode()) { + return false; + } + if (mSelectionStart != that.mSelectionStart + || mSelectionEnd != that.mSelectionEnd + || mCandidatesStart != that.mCandidatesStart + || mCandidatesEnd != that.mCandidatesEnd) { + return false; + } + if (!Objects.equals(mCharacterRects, that.mCharacterRects)) { + return false; + } + if (!Objects.equals(mMatrix, that.mMatrix)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "SelectionInfo{mSelection=" + mSelectionStart + "," + mSelectionEnd + + " mCandiadtes=" + mCandidatesStart + "," + mCandidatesEnd + + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal + + " mInsertionMarkerTop=" + mInsertionMarkerTop + + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline + + " mInsertionMarkerBottom=" + mInsertionMarkerBottom + + " mCharacterRects=" + (mCharacterRects != null ? mCharacterRects : "null") + + " mMatrix=" + mMatrix + + "}"; + } + + /** + * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe. + */ + public static final class CursorAnchorInfoBuilder { + /** + * Sets the text range of the selection. Calling this can be skipped if there is no + * selection. + */ + public CursorAnchorInfoBuilder setSelectionRange(final int newStart, final int newEnd) { + mSelectionStart = newStart; + mSelectionEnd = newEnd; + return this; + } + private int mSelectionStart = -1; + private int mSelectionEnd = -1; + + /** + * Sets the text range of the composition string. Calling this can be skipped if there is + * no composition. + */ + public CursorAnchorInfoBuilder setCandidateRange(final int start, final int end) { + mCandidateStart = start; + mCandidateEnd = end; + return this; + } + private int mCandidateStart = -1; + private int mCandidateEnd = -1; + + /** + * Sets the location of the text insertion point (zero width cursor) as a rectangle in + * local coordinates. Calling this can be skipped when there is no text insertion point; + * however if there is an insertion point, editors must call this method. + * @param horizontalPosition horizontal position of the insertion marker, in the local + * coordinates that will be transformed with the transformation matrix when rendered on the + * screen. This should be calculated or compatible with + * {@link Layout#getPrimaryHorizontal(int)}. + * @param lineTop vertical position of the insertion marker, in the local coordinates that + * will be transformed with the transformation matrix when rendered on the screen. This + * should be calculated or compatible with {@link Layout#getLineTop(int)}. + * @param lineBaseline vertical position of the insertion marker, in the local coordinates + * that will be transformed with the transformation matrix when rendered on the screen. This + * should be calculated or compatible with {@link Layout#getLineBaseline(int)}. + * @param lineBottom vertical position of the insertion marker, in the local coordinates + * that will be transformed with the transformation matrix when rendered on the screen. This + * should be calculated or compatible with {@link Layout#getLineBottom(int)}. + */ + public CursorAnchorInfoBuilder setInsertionMarkerLocation( + final float horizontalPosition, final float lineTop, final float lineBaseline, + final float lineBottom){ + mInsertionMarkerHorizontal = horizontalPosition; + mInsertionMarkerTop = lineTop; + mInsertionMarkerBaseline = lineBaseline; + mInsertionMarkerBottom = lineBottom; + return this; + } + private float mInsertionMarkerHorizontal = Float.NaN; + private float mInsertionMarkerTop = Float.NaN; + private float mInsertionMarkerBaseline = Float.NaN; + private float mInsertionMarkerBottom = Float.NaN; + + /** + * Adds the bounding box of the character specified with the index. + *

+ * Editor authors should not call this method for characters that are invisible. + *

+ * + * @param index index of the character in Java chars units. Must be specified in + * ascending order across successive calls. + * @param leadingEdgeX x coordinate of the leading edge of the character in local + * coordinates, that is, left edge for LTR text and right edge for RTL text. + * @param leadingEdgeY y coordinate of the leading edge of the character in local + * coordinates. + * @param trailingEdgeX x coordinate of the trailing edge of the character in local + * coordinates, that is, right edge for LTR text and left edge for RTL text. + * @param trailingEdgeY y coordinate of the trailing edge of the character in local + * coordinates. + * @throws IllegalArgumentException If the index is a negative value, or not greater than + * all of the previously called indices. + */ + public CursorAnchorInfoBuilder addCharacterRect(final int index, + final float leadingEdgeX, final float leadingEdgeY, final float trailingEdgeX, + final float trailingEdgeY) { + if (index < 0) { + throw new IllegalArgumentException("index must not be a negative integer."); + } + if (mCharacterRectBuilder == null) { + mCharacterRectBuilder = new SparseRectFArrayBuilder(); + } + mCharacterRectBuilder.append(index, leadingEdgeX, leadingEdgeY, trailingEdgeX, + trailingEdgeY); + return this; + } + private SparseRectFArrayBuilder mCharacterRectBuilder = null; + + /** + * Sets the matrix that transforms local coordinates into screen coordinates. + * @param matrix transformation matrix from local coordinates into screen coordinates. null + * is interpreted as an identity matrix. + */ + public CursorAnchorInfoBuilder setMatrix(final Matrix matrix) { + if (matrix != null) { + mMatrix = matrix; + } else { + mMatrix = Matrix.IDENTITY_MATRIX; + } + return this; + } + private Matrix mMatrix = Matrix.IDENTITY_MATRIX; + + /** + * @return {@link CursorAnchorInfo} using parameters in this + * {@link CursorAnchorInfoBuilder}. + */ + public CursorAnchorInfo build() { + return new CursorAnchorInfo(this); + } + + /** + * Resets the internal state so that this instance can be reused to build another + * instance of {@link CursorAnchorInfo}. + */ + public void reset() { + mSelectionStart = -1; + mSelectionEnd = -1; + mCandidateStart = -1; + mCandidateEnd = -1; + mInsertionMarkerHorizontal = Float.NaN; + mInsertionMarkerTop = Float.NaN; + mInsertionMarkerBaseline = Float.NaN; + mInsertionMarkerBottom = Float.NaN; + mMatrix = Matrix.IDENTITY_MATRIX; + if (mCharacterRectBuilder != null) { + mCharacterRectBuilder.reset(); + } + } + } + + private CursorAnchorInfo(final CursorAnchorInfoBuilder builder) { + mSelectionStart = builder.mSelectionStart; + mSelectionEnd = builder.mSelectionEnd; + mCandidatesStart = builder.mCandidateStart; + mCandidatesEnd = builder.mCandidateEnd; + mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal; + mInsertionMarkerTop = builder.mInsertionMarkerTop; + mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline; + mInsertionMarkerBottom = builder.mInsertionMarkerBottom; + mCharacterRects = builder.mCharacterRectBuilder != null ? + builder.mCharacterRectBuilder.build() : null; + mMatrix = builder.mMatrix; + } + + /** + * Returns the index where the selection starts. + * @return -1 if there is no selection. + */ + public int getSelectionStart() { + return mSelectionStart; + } + + /** + * Returns the index where the selection ends. + * @return -1 if there is no selection. + */ + public int getSelectionEnd() { + return mSelectionEnd; + } + + /** + * Returns the index where the composition starts. + * @return -1 if there is no composition. + */ + public int getCandidatesStart() { + return mCandidatesStart; + } + + /** + * Returns the index where the composition ends. + * @return -1 if there is no composition. + */ + public int getCandidatesEnd() { + return mCandidatesEnd; + } + + /** + * Returns the horizontal start of the insertion marker, in the local coordinates that will + * be transformed with {@link #getMatrix()} when rendered on the screen. + * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}. + * Pay special care to RTL/LTR handling. + * {@code java.lang.Float.NaN} if not specified. + * @see Layout#getPrimaryHorizontal(int) + */ + public float getInsertionMarkerHorizontal() { + return mInsertionMarkerHorizontal; + } + /** + * Returns the vertical top position of the insertion marker, in the local coordinates that + * will be transformed with {@link #getMatrix()} when rendered on the screen. + * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}. + * {@code java.lang.Float.NaN} if not specified. + */ + public float getInsertionMarkerTop() { + return mInsertionMarkerTop; + } + /** + * Returns the vertical baseline position of the insertion marker, in the local coordinates + * that will be transformed with {@link #getMatrix()} when rendered on the screen. + * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}. + * {@code java.lang.Float.NaN} if not specified. + */ + public float getInsertionMarkerBaseline() { + return mInsertionMarkerBaseline; + } + /** + * Returns the vertical bottom position of the insertion marker, in the local coordinates + * that will be transformed with {@link #getMatrix()} when rendered on the screen. + * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}. + * {@code java.lang.Float.NaN} if not specified. + */ + public float getInsertionMarkerBottom() { + return mInsertionMarkerBottom; + } + + /** + * Returns a new instance of {@link RectF} that indicates the location of the character + * specified with the index. + *

+ * Note that coordinates are not necessarily contiguous or even monotonous, especially when + * RTL text and LTR text are mixed. + *

+ * @param index index of the character in a Java chars. + * @return a new instance of {@link RectF} that represents the location of the character in + * local coordinates. null if the character is invisible or the application did not provide + * the location. Note that the {@code left} field can be greater than the {@code right} field + * if the character is in RTL text. + */ + // TODO: Prepare a document about the expected behavior for surrogate pairs, combining + // characters, and non-graphical chars. + public RectF getCharacterRect(final int index) { + if (mCharacterRects == null) { + return null; + } + return mCharacterRects.get(index); + } + + /** + * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation + * matrix that is to be applied other positional data in this class. + * @return a new instance (copy) of the transformation matrix. + */ + public Matrix getMatrix() { + return new Matrix(mMatrix); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public CursorAnchorInfo createFromParcel(Parcel source) { + return new CursorAnchorInfo(source); + } + + @Override + public CursorAnchorInfo[] newArray(int size) { + return new CursorAnchorInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 02278733d270b..e1c6f522f101e 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -49,6 +49,7 @@ import android.view.InputEventSender; import android.view.KeyEvent; import android.view.View; import android.view.ViewRootImpl; +import android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -321,6 +322,7 @@ public final class InputMethodManager { * The buffer to retrieve the view location in screen coordinates in {@link #updateCursor}. */ private final int[] mViewTopLeft = new int[2]; + private final CursorAnchorInfoBuilder mCursorAnchorInfoBuilder = new CursorAnchorInfoBuilder(); // ----------------------------------------------------------- @@ -1435,7 +1437,7 @@ public final class InputMethodManager { || mCurrentTextBoxAttribute == null || mCurMethod == null) { return; } - + if (mCursorSelStart != selStart || mCursorSelEnd != selEnd || mCursorCandStart != candidatesStart || mCursorCandEnd != candidatesEnd) { @@ -1555,6 +1557,31 @@ public final class InputMethodManager { } } + /** + * Report positional change of the text insertion point and/or characters in the composition + * string. + */ + public void updateCursorAnchorInfo(View view, final CursorAnchorInfo cursorAnchorInfo) { + if (view == null || cursorAnchorInfo == null) { + return; + } + checkFocus(); + synchronized (mH) { + if ((mServedView != view && + (mServedView == null || !mServedView.checkInputConnectionProxy(view))) + || mCurrentTextBoxAttribute == null || mCurMethod == null) { + return; + } + if (DEBUG) Log.d(TAG, "updateCursorAnchorInfo"); + + try { + mCurMethod.updateCursorAnchorInfo(cursorAnchorInfo); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + } + } + } + /** * Call {@link InputMethodSession#appPrivateCommand(String, Bundle) * InputMethodSession.appPrivateCommand()} on the current Input Method. diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java index 63862999519d9..74fbbc7e22adb 100644 --- a/core/java/android/view/inputmethod/InputMethodSession.java +++ b/core/java/android/view/inputmethod/InputMethodSession.java @@ -165,7 +165,7 @@ public interface InputMethodSession { public void appPrivateCommand(String action, Bundle data); /** - * Toggle the soft input window. + * Toggle the soft input window. * Applications can toggle the state of the soft input window. * @param showFlags Provides additional operating flags. May be * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT}, @@ -175,4 +175,14 @@ public interface InputMethodSession { * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set. */ public void toggleSoftInput(int showFlags, int hideFlags); + + /** + * This method is called when the cursor and/or the character position relevant to text input + * is changed on the screen. This is not called by default. It will only be reported if + * requested by the input method. + * + * @param cursorAnchorInfo Positional information relevant to text input, such as text + * insertion point and composition string. + */ + public void updateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo); } diff --git a/core/java/android/view/inputmethod/SparseRectFArray.java b/core/java/android/view/inputmethod/SparseRectFArray.java new file mode 100644 index 0000000000000..40cade74f864c --- /dev/null +++ b/core/java/android/view/inputmethod/SparseRectFArray.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package android.view.inputmethod; + +import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Arrays; + +/** + * An implementation of SparseArray specialized for {@link android.graphics.RectF}. + *

+ * As this is a sparse array, it represents an array of {@link RectF} most of which are null. This + * class could be in some other packages like android.graphics or android.util but currently + * belong to android.view.inputmethod because this class is hidden and used only in input method + * framework. + *

+ * @hide + */ +public final class SparseRectFArray implements Parcelable { + /** + * The keys, in ascending order, of those {@link RectF} that are not null. For example, + * {@code [null, null, null, Rect1, null, Rect2]} would be represented by {@code [3,5]}. + * @see #mCoordinates + */ + private final int[] mKeys; + + /** + * Stores coordinates of the rectangles, in the order of + * {@code rects[mKeys[0]].left}, {@code rects[mKeys[0]].top}, + * {@code rects[mKeys[0]].right}, {@code rects[mKeys[0]].bottom}, + * {@code rects[mKeys[1]].left}, {@code rects[mKeys[1]].top}, + * {@code rects[mKeys[1]].right}, {@code rects[mKeys[1]].bottom}, + * {@code rects[mKeys[2]].left}, {@code rects[mKeys[2]].top}, .... + */ + private final float[] mCoordinates; + + public SparseRectFArray(final Parcel source) { + mKeys = source.createIntArray(); + mCoordinates = source.createFloatArray(); + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeIntArray(mKeys); + dest.writeFloatArray(mCoordinates); + } + + @Override + public int hashCode() { + // TODO: Improve the hash function. + if (mKeys == null || mKeys.length == 0) { + return 0; + } + int hash = mKeys.length; + // For performance reasons, only the first rectangle is used for the hash code now. + for (int i = 0; i < 4; i++) { + hash *= 31; + hash += mCoordinates[i]; + } + return hash; + } + + @Override + public boolean equals(Object obj){ + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof SparseRectFArray)) { + return false; + } + final SparseRectFArray that = (SparseRectFArray) obj; + + return Arrays.equals(mKeys, that.mKeys) && Arrays.equals(mCoordinates, that.mCoordinates); + } + + @Override + public String toString() { + if (mKeys == null || mCoordinates == null) { + return "SparseRectFArray{}"; + } + final StringBuilder sb = new StringBuilder(); + sb.append("SparseRectFArray{"); + for (int i = 0; i < mKeys.length; i++) { + if (i != 0) { + sb.append(", "); + } + final int baseIndex = i * 4; + sb.append(mKeys[i]); + sb.append(":["); + sb.append(mCoordinates[baseIndex + 0]); + sb.append(","); + sb.append(mCoordinates[baseIndex + 1]); + sb.append("],["); + sb.append(mCoordinates[baseIndex + 2]); + sb.append(","); + sb.append(mCoordinates[baseIndex + 3]); + sb.append("]"); + } + sb.append("}"); + return sb.toString(); + } + + /** + * Builder for {@link SparseRectFArray}. This class is not designed to be thread-safe. + * @hide + */ + public static final class SparseRectFArrayBuilder { + /** + * Throws {@link IllegalArgumentException} to make sure that this class is correctly used. + * @param key key to be checked. + */ + private void checkIndex(final int key) { + if (mCount == 0) { + return; + } + if (mKeys[mCount - 1] >= key) { + throw new IllegalArgumentException("key must be greater than all existing keys."); + } + } + + /** + * Extends the internal array if necessary. + */ + private void ensureBufferSize() { + if (mKeys == null) { + mKeys = new int[INITIAL_SIZE]; + } + if (mCoordinates == null) { + mCoordinates = new float[INITIAL_SIZE * 4]; + } + final int requiredIndexArraySize = mCount + 1; + if (mKeys.length <= requiredIndexArraySize) { + final int[] newArray = new int[requiredIndexArraySize * 2]; + System.arraycopy(mKeys, 0, newArray, 0, mCount); + mKeys = newArray; + } + final int requiredCoordinatesArraySize = (mCount + 1) * 4; + if (mCoordinates.length <= requiredCoordinatesArraySize) { + final float[] newArray = new float[requiredCoordinatesArraySize * 2]; + System.arraycopy(mCoordinates, 0, newArray, 0, mCount * 4); + mCoordinates = newArray; + } + } + + /** + * Puts the rectangle with an integer key. + * @param key the key to be associated with the rectangle. It must be greater than all + * existing keys that have been previously specified. + * @param left left of the rectangle. + * @param top top of the rectangle. + * @param right right of the rectangle. + * @param bottom bottom of the rectangle. + * @return the receiver object itself for chaining method calls. + * @throws IllegalArgumentException If the index is not greater than all of existing keys. + */ + public SparseRectFArrayBuilder append(final int key, + final float left, final float top, final float right, final float bottom) { + checkIndex(key); + ensureBufferSize(); + final int baseCoordinatesIndex = mCount * 4; + mCoordinates[baseCoordinatesIndex + 0] = left; + mCoordinates[baseCoordinatesIndex + 1] = top; + mCoordinates[baseCoordinatesIndex + 2] = right; + mCoordinates[baseCoordinatesIndex + 3] = bottom; + mKeys[mCount] = key; + ++mCount; + return this; + } + private int mCount = 0; + private int[] mKeys = null; + private float[] mCoordinates = null; + private static int INITIAL_SIZE = 16; + + /** + * @return {@link SparseRectFArray} using parameters in this {@link SparseRectFArray}. + */ + public SparseRectFArray build() { + return new SparseRectFArray(this); + } + + public void reset() { + if (mCount == 0) { + mKeys = null; + mCoordinates = null; + } + mCount = 0; + } + } + + private SparseRectFArray(final SparseRectFArrayBuilder builder) { + if (builder.mCount == 0) { + mKeys = null; + mCoordinates = null; + } else { + mKeys = new int[builder.mCount]; + mCoordinates = new float[builder.mCount * 4]; + System.arraycopy(builder.mKeys, 0, mKeys, 0, builder.mCount); + System.arraycopy(builder.mCoordinates, 0, mCoordinates, 0, builder.mCount * 4); + } + } + + public RectF get(final int index) { + if (mKeys == null) { + return null; + } + if (index < 0) { + return null; + } + final int arrayIndex = Arrays.binarySearch(mKeys, index); + if (arrayIndex < 0) { + return null; + } + final int baseCoordIndex = arrayIndex * 4; + return new RectF(mCoordinates[baseCoordIndex], + mCoordinates[baseCoordIndex + 1], + mCoordinates[baseCoordIndex + 2], + mCoordinates[baseCoordIndex + 3]); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SparseRectFArray createFromParcel(Parcel source) { + return new SparseRectFArray(source); + } + @Override + public SparseRectFArray[] newArray(int size) { + return new SparseRectFArray[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } +} + diff --git a/core/java/com/android/internal/view/IInputMethodSession.aidl b/core/java/com/android/internal/view/IInputMethodSession.aidl index 90210ce9866c7..367b713ed78f5 100644 --- a/core/java/com/android/internal/view/IInputMethodSession.aidl +++ b/core/java/com/android/internal/view/IInputMethodSession.aidl @@ -21,6 +21,7 @@ import android.os.Bundle; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.ExtractedText; /** @@ -47,4 +48,6 @@ oneway interface IInputMethodSession { void toggleSoftInput(int showFlags, int hideFlags); void finishSession(); + + void updateCursorAnchorInfo(in CursorAnchorInfo cursorAnchorInfo); } diff --git a/core/tests/inputmethodtests/run_core_inputmethod_test.sh b/core/tests/inputmethodtests/run_core_inputmethod_test.sh index 9029ba5c3f396..e0f4f6d4303ac 100755 --- a/core/tests/inputmethodtests/run_core_inputmethod_test.sh +++ b/core/tests/inputmethodtests/run_core_inputmethod_test.sh @@ -21,4 +21,4 @@ if [[ $rebuild == true ]]; then $COMMAND fi -adb shell am instrument -w -e class android.os.InputMethodTest,android.os.InputMethodSubtypeArrayTest,android.os.InputMethodSubtypeSwitchingControllerTest com.android.frameworks.coretests.inputmethod/android.test.InstrumentationTestRunner +adb shell am instrument -w -e class android.os.InputMethodTest,android.os.InputMethodSubtypeArrayTest,android.os.InputMethodSubtypeSwitchingControllerTest,android.os.CursorAnchorInfoTest,android.os.SparseRectFArrayTest com.android.frameworks.coretests.inputmethod/android.test.InstrumentationTestRunner diff --git a/core/tests/inputmethodtests/src/android/os/CursorAnchorInfoTest.java b/core/tests/inputmethodtests/src/android/os/CursorAnchorInfoTest.java new file mode 100644 index 0000000000000..59a63144c954d --- /dev/null +++ b/core/tests/inputmethodtests/src/android/os/CursorAnchorInfoTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.graphics.Matrix; +import android.graphics.RectF; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder; + +public class CursorAnchorInfoTest extends InstrumentationTestCase { + // null represents a character that is invisible, for example because it's overlapped by some + // other UI elements. + private static final RectF[] MANY_RECTS = new RectF[] { + null, + new RectF(102.0f, 202.0f, 302.0f, 402.0f), + new RectF(103.0f, 203.0f, 303.0f, 403.0f), + new RectF(104.0f, 204.0f, 304.0f, 404.0f), + new RectF(105.0f, 205.0f, 305.0f, 405.0f), + new RectF(106.0f, 206.0f, 306.0f, 406.0f), + null, + new RectF(108.0f, 208.0f, 308.0f, 408.0f), + new RectF(109.0f, 209.0f, 309.0f, 409.0f), + new RectF(110.0f, 210.0f, 310.0f, 410.0f), + new RectF(111.0f, 211.0f, 311.0f, 411.0f), + new RectF(112.0f, 212.0f, 312.0f, 412.0f), + new RectF(113.0f, 213.0f, 313.0f, 413.0f), + new RectF(114.0f, 214.0f, 314.0f, 414.0f), + new RectF(115.0f, 215.0f, 315.0f, 415.0f), + new RectF(116.0f, 216.0f, 316.0f, 416.0f), + new RectF(117.0f, 217.0f, 317.0f, 417.0f), + null, + null, + }; + + @SmallTest + public void testBuilder() throws Exception { + final int SELECTION_START = 30; + final int SELECTION_END = 40; + final int CANDIDATES_START = 32; + final int CANDIDATES_END = 33; + final float INSERTION_MARKER_HORIZONTAL = 10.5f; + final float INSERTION_MARKER_TOP = 100.1f; + final float INSERTION_MARKER_BASELINE = 110.4f; + final float INSERTION_MARKER_BOTOM = 111.0f; + Matrix TRANSFORM_MATRIX = new Matrix(Matrix.IDENTITY_MATRIX); + TRANSFORM_MATRIX.setScale(10.0f, 20.0f); + + final CursorAnchorInfoBuilder builder = new CursorAnchorInfoBuilder(); + builder.setSelectionRange(SELECTION_START, SELECTION_END) + .setCandidateRange(CANDIDATES_START, CANDIDATES_END) + .setInsertionMarkerLocation(INSERTION_MARKER_HORIZONTAL, INSERTION_MARKER_TOP, + INSERTION_MARKER_BASELINE, INSERTION_MARKER_BOTOM) + .setMatrix(TRANSFORM_MATRIX); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + if (rect != null) { + builder.addCharacterRect(i, rect.left, rect.top, rect.right, rect.bottom); + } + } + + final CursorAnchorInfo info = builder.build(); + assertEquals(SELECTION_START, info.getSelectionStart()); + assertEquals(SELECTION_END, info.getSelectionEnd()); + assertEquals(CANDIDATES_START, info.getCandidatesStart()); + assertEquals(CANDIDATES_END, info.getCandidatesEnd()); + assertEquals(INSERTION_MARKER_HORIZONTAL, info.getInsertionMarkerHorizontal()); + assertEquals(INSERTION_MARKER_TOP, info.getInsertionMarkerTop()); + assertEquals(INSERTION_MARKER_BASELINE, info.getInsertionMarkerBaseline()); + assertEquals(INSERTION_MARKER_BOTOM, info.getInsertionMarkerBottom()); + assertEquals(TRANSFORM_MATRIX, info.getMatrix()); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + assertEquals(rect, info.getCharacterRect(i)); + } + + // Make sure that the builder can reproduce the same object. + final CursorAnchorInfo info2 = builder.build(); + assertEquals(SELECTION_START, info2.getSelectionStart()); + assertEquals(SELECTION_END, info2.getSelectionEnd()); + assertEquals(CANDIDATES_START, info2.getCandidatesStart()); + assertEquals(CANDIDATES_END, info2.getCandidatesEnd()); + assertEquals(INSERTION_MARKER_HORIZONTAL, info2.getInsertionMarkerHorizontal()); + assertEquals(INSERTION_MARKER_TOP, info2.getInsertionMarkerTop()); + assertEquals(INSERTION_MARKER_BASELINE, info2.getInsertionMarkerBaseline()); + assertEquals(INSERTION_MARKER_BOTOM, info2.getInsertionMarkerBottom()); + assertEquals(TRANSFORM_MATRIX, info2.getMatrix()); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + assertEquals(rect, info2.getCharacterRect(i)); + } + assertEquals(info, info2); + assertEquals(info.hashCode(), info2.hashCode()); + + // Make sure that object can be marshalled via {@link Parsel}. + final CursorAnchorInfo info3 = cloneViaParcel(info2); + assertEquals(SELECTION_START, info3.getSelectionStart()); + assertEquals(SELECTION_END, info3.getSelectionEnd()); + assertEquals(CANDIDATES_START, info3.getCandidatesStart()); + assertEquals(CANDIDATES_END, info3.getCandidatesEnd()); + assertEquals(INSERTION_MARKER_HORIZONTAL, info3.getInsertionMarkerHorizontal()); + assertEquals(INSERTION_MARKER_TOP, info3.getInsertionMarkerTop()); + assertEquals(INSERTION_MARKER_BASELINE, info3.getInsertionMarkerBaseline()); + assertEquals(INSERTION_MARKER_BOTOM, info3.getInsertionMarkerBottom()); + assertEquals(TRANSFORM_MATRIX, info3.getMatrix()); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + assertEquals(rect, info3.getCharacterRect(i)); + } + assertEquals(info.hashCode(), info3.hashCode()); + + builder.reset(); + final CursorAnchorInfo uninitializedInfo = builder.build(); + assertEquals(-1, uninitializedInfo.getSelectionStart()); + assertEquals(-1, uninitializedInfo.getSelectionEnd()); + assertEquals(-1, uninitializedInfo.getCandidatesStart()); + assertEquals(-1, uninitializedInfo.getCandidatesEnd()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerHorizontal()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerTop()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerBaseline()); + assertEquals(Float.NaN, uninitializedInfo.getInsertionMarkerBottom()); + assertEquals(Matrix.IDENTITY_MATRIX, uninitializedInfo.getMatrix()); + } + + @SmallTest + public void testBuilderAdd() throws Exception { + // A negative index should be rejected. + try { + new CursorAnchorInfoBuilder().addCharacterRect(-1, 0.0f, 0.0f, 0.0f, 0.0f); + } catch (IllegalArgumentException ex) { + assertTrue(true); + } + } + + private static CursorAnchorInfo cloneViaParcel(final CursorAnchorInfo src) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + src.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return new CursorAnchorInfo(parcel); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } +} + diff --git a/core/tests/inputmethodtests/src/android/os/SparseRectFArrayTest.java b/core/tests/inputmethodtests/src/android/os/SparseRectFArrayTest.java new file mode 100644 index 0000000000000..fae7230ac6ca7 --- /dev/null +++ b/core/tests/inputmethodtests/src/android/os/SparseRectFArrayTest.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.graphics.RectF; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.inputmethod.SparseRectFArray; +import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder; + +import java.util.Objects; + +public class SparseRectFArrayTest extends InstrumentationTestCase { + // A test data for {@link SparseRectFArray}. null represents the gap of indices. + private static final RectF[] MANY_RECTS = new RectF[] { + null, + new RectF(102.0f, 202.0f, 302.0f, 402.0f), + new RectF(103.0f, 203.0f, 303.0f, 403.0f), + new RectF(104.0f, 204.0f, 304.0f, 404.0f), + new RectF(105.0f, 205.0f, 305.0f, 405.0f), + new RectF(106.0f, 206.0f, 306.0f, 406.0f), + null, + new RectF(108.0f, 208.0f, 308.0f, 408.0f), + new RectF(109.0f, 209.0f, 309.0f, 409.0f), + new RectF(110.0f, 210.0f, 310.0f, 410.0f), + new RectF(111.0f, 211.0f, 311.0f, 411.0f), + new RectF(112.0f, 212.0f, 312.0f, 412.0f), + new RectF(113.0f, 213.0f, 313.0f, 413.0f), + new RectF(114.0f, 214.0f, 314.0f, 414.0f), + new RectF(115.0f, 215.0f, 315.0f, 415.0f), + new RectF(116.0f, 216.0f, 316.0f, 416.0f), + new RectF(117.0f, 217.0f, 317.0f, 417.0f), + null, + null, + new RectF(118.0f, 218.0f, 318.0f, 418.0f), + }; + + @SmallTest + public void testBuilder() throws Exception { + final RectF TEMP_RECT = new RectF(10.0f, 20.0f, 30.0f, 40.0f); + + final SparseRectFArrayBuilder builder = new SparseRectFArrayBuilder(); + builder.append(100, TEMP_RECT.left, TEMP_RECT.top, TEMP_RECT.right, TEMP_RECT.bottom); + assertNull(builder.build().get(-1)); + assertNull(builder.build().get(0)); + assertNull(builder.build().get(99)); + assertEquals(TEMP_RECT, builder.build().get(100)); + assertNull(builder.build().get(101)); + + // Test if {@link SparseRectFArrayBuilder#reset} resets its internal state. + builder.reset(); + assertNull(builder.build().get(100)); + + builder.reset(); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + if (rect != null) { + builder.append(i, rect.left, rect.top, rect.right, rect.bottom); + } + } + final SparseRectFArray array = builder.build(); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + assertEquals(rect, array.get(i)); + } + + // Make sure the builder reproduces an equivalent object. + final SparseRectFArray array2 = builder.build(); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + assertEquals(rect, array2.get(i)); + } + assertEqualRects(array, array2); + + // Make sure the instance can be marshaled via {@link Parcel}. + final SparseRectFArray array3 = cloneViaParcel(array); + for (int i = 0; i < MANY_RECTS.length; i++) { + final RectF rect = MANY_RECTS[i]; + assertEquals(rect, array3.get(i)); + } + assertEqualRects(array, array3); + + // Make sure the builder can be reset. + builder.reset(); + assertNull(builder.build().get(0)); + } + + @SmallTest + public void testEquality() throws Exception { + // Empty array should be equal. + assertEqualRects(new SparseRectFArrayBuilder().build(), + new SparseRectFArrayBuilder().build()); + + assertEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f).build(), + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f).build(), + new SparseRectFArrayBuilder().append(100, 2.0f, 2.0f, 3.0f, 4.0f).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder().append(100, 1.0f, 2.0f, 3.0f, 4.0f).build(), + new SparseRectFArrayBuilder().append(101, 1.0f, 2.0f, 3.0f, 4.0f).build()); + + assertEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 1.0f, 0.0f, 0.0f, 0.0f).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 1.0f, 0.0f, 0.0f, 0.0f).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f).build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(101, 0.0f, 0.0f, 0.0f, 0.0f).build(), + new SparseRectFArrayBuilder() + .append(100, 1.0f, 2.0f, 3.0f, 4.0f) + .append(102, 0.0f, 0.0f, 0.0f, 0.0f).build()); + + assertEqualRects( + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f) + .build(), + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f) + .build()); + + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f) + .build(), + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f) + .build()); + assertNotEqualRects( + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f) + .append(1000, 0.0f, 0.0f, 0.0f, 0.0f) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f) + .build(), + new SparseRectFArrayBuilder() + .append(1, 1.0f, 2.0f, 3.0f, 4.0f) + .append(1000, 1.0f, 0.0f, 0.0f, 0.0f) + .append(100000000, 0.0f, 0.0f, 0.0f, 0.0f) + .build()); + } + + @SmallTest + public void testBuilderAppend() throws Exception { + // Key should be appended in ascending order. + try { + new SparseRectFArrayBuilder().append(10, 0, 0, 0, 0).append(0, 1, 2, 3, 4); + } catch (IllegalArgumentException ex) { + assertTrue(true); + } + + try { + new SparseRectFArrayBuilder().append(10, 0, 0, 0, 0).append(10, 1, 2, 3, 4); + } catch (IllegalArgumentException ex) { + assertTrue(true); + } + } + + private static void assertEqualRects(SparseRectFArray a, SparseRectFArray b) { + assertEquals(a, b); + if (a != null && b != null) { + assertEquals(a.hashCode(), b.hashCode()); + } + } + + private static void assertNotEqualRects(SparseRectFArray a, SparseRectFArray b) { + assertFalse(Objects.equals(a, b)); + } + + private static SparseRectFArray cloneViaParcel(final SparseRectFArray src) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + src.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return new SparseRectFArray(parcel); + } finally { + if (parcel != null) { + parcel.recycle(); + } + } + } +}