Loading core/java/android/view/inputmethod/EditorInfo.java +43 −33 Original line number Diff line number Diff line Loading @@ -33,11 +33,11 @@ import android.util.Printer; import android.view.View; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Objects; /** * An EditorInfo describes several attributes of a text editing object Loading Loading @@ -558,7 +558,7 @@ public class EditorInfo implements InputType, Parcelable { * editor wants to trim out the first 10 chars, subTextStart should be 10. */ public void setInitialSurroundingSubText(@NonNull CharSequence subText, int subTextStart) { Preconditions.checkNotNull(subText); Objects.requireNonNull(subText); // Swap selection start and end if necessary. final int subTextSelStart = initialSelStart > initialSelEnd Loading @@ -585,25 +585,35 @@ public class EditorInfo implements InputType, Parcelable { return; } // The input text is too long. Let's try to trim it reasonably. Fundamental rules are: // 1. Text before the cursor is the most important information to IMEs. // 2. Text after the cursor is the second important information to IMEs. // 3. Selected text is the least important information but it shall NEVER be truncated. // When it is too long, just drop it. // // Source: <TextBeforeCursor><Selection><TextAfterCursor> // Possible results: // 1. <(maybeTrimmedAtHead)TextBeforeCursor><Selection><TextAfterCursor(maybeTrimmedAtTail)> // 2. <(maybeTrimmedAtHead)TextBeforeCursor><TextAfterCursor(maybeTrimmedAtTail)> // final int sourceSelLength = subTextSelEnd - subTextSelStart; trimLongSurroundingText(subText, subTextSelStart, subTextSelEnd); } /** * Trims the initial surrounding text when it is over sized. Fundamental trimming rules are: * - The text before the cursor is the most important information to IMEs. * - The text after the cursor is the second important information to IMEs. * - The selected text is the least important information but it shall NEVER be truncated. When * it is too long, just drop it. *<p><pre> * For example, the subText can be viewed as * TextBeforeCursor + Selection + TextAfterCursor * The result could be * 1. (maybeTrimmedAtHead)TextBeforeCursor + Selection + TextAfterCursor(maybeTrimmedAtTail) * 2. (maybeTrimmedAtHead)TextBeforeCursor + TextAfterCursor(maybeTrimmedAtTail)</pre> * * @param subText The long text that needs to be trimmed. * @param selStart The text offset of the start of the selection. * @param selEnd The text offset of the end of the selection */ private void trimLongSurroundingText(CharSequence subText, int selStart, int selEnd) { final int sourceSelLength = selEnd - selStart; // When the selected text is too long, drop it. final int newSelLength = (sourceSelLength > MAX_INITIAL_SELECTION_LENGTH) ? 0 : sourceSelLength; // Distribute rest of length quota to TextBeforeCursor and TextAfterCursor in 4:1 ratio. final int subTextBeforeCursorLength = subTextSelStart; final int subTextAfterCursorLength = subTextLength - subTextSelEnd; final int subTextBeforeCursorLength = selStart; final int subTextAfterCursorLength = subText.length() - selEnd; final int maxLengthMinusSelection = MEMORY_EFFICIENT_TEXT_LENGTH - newSelLength; final int possibleMaxBeforeCursorLength = Math.min(subTextBeforeCursorLength, (int) (0.8 * maxLengthMinusSelection)); Loading @@ -617,24 +627,23 @@ public class EditorInfo implements InputType, Parcelable { // We don't want to cut surrogate pairs in the middle. Exam that at the new head and tail. if (isCutOnSurrogate(subText, subTextSelStart - newBeforeCursorLength, TrimPolicy.HEAD)) { selStart - newBeforeCursorLength, TrimPolicy.HEAD)) { newBeforeCursorHead = newBeforeCursorHead + 1; newBeforeCursorLength = newBeforeCursorLength - 1; } if (isCutOnSurrogate(subText, subTextSelEnd + newAfterCursorLength - 1, TrimPolicy.TAIL)) { selEnd + newAfterCursorLength - 1, TrimPolicy.TAIL)) { newAfterCursorLength = newAfterCursorLength - 1; } // Now we know where to trim, compose the initialSurroundingText. final int newTextLength = newBeforeCursorLength + newSelLength + newAfterCursorLength; CharSequence newInitialSurroundingText; final CharSequence newInitialSurroundingText; if (newSelLength != sourceSelLength) { final CharSequence beforeCursor = subText.subSequence(newBeforeCursorHead, newBeforeCursorHead + newBeforeCursorLength); final CharSequence afterCursor = subText.subSequence(subTextSelEnd, subTextSelEnd + newAfterCursorLength); final CharSequence afterCursor = subText.subSequence(selEnd, selEnd + newAfterCursorLength); newInitialSurroundingText = TextUtils.concat(beforeCursor, afterCursor); } else { Loading @@ -651,15 +660,16 @@ public class EditorInfo implements InputType, Parcelable { } /** * Get <var>n</var> characters of text before the current cursor position. May be {@code null} * when the protocol is not supported. * Get <var>length</var> characters of text before the current cursor position. May be * {@code null} when the protocol is not supported. * * @param length The expected length of the text. * @param flags Supplies additional options controlling how the text is returned. May be * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}. * @return the text before the cursor position; the length of the returned text might be less * than <var>n</var>. When there is no text before the cursor, an empty string will be returned. * It could also be {@code null} when the editor or system could not support this protocol. * than <var>length</var>. When there is no text before the cursor, an empty string will be * returned. It could also be {@code null} when the editor or system could not support this * protocol. */ @Nullable public CharSequence getInitialTextBeforeCursor(int length, int flags) { Loading @@ -667,8 +677,8 @@ public class EditorInfo implements InputType, Parcelable { } /** * Gets the selected text, if any. May be {@code null} when no text is selected or the selected * text is way too long. * Gets the selected text, if any. May be {@code null} when the protocol is not supported or the * selected text is way too long. * * @param flags Supplies additional options controlling how the text is returned. May be * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}. Loading @@ -693,15 +703,16 @@ public class EditorInfo implements InputType, Parcelable { } /** * Get <var>n</var> characters of text after the current cursor position. May be {@code null} * when the protocol is not supported. * Get <var>length</var> characters of text after the current cursor position. May be * {@code null} when the protocol is not supported. * * @param length The expected length of the text. * @param flags Supplies additional options controlling how the text is returned. May be * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}. * @return the text after the cursor position; the length of the returned text might be less * than <var>n</var>. When there is no text after the cursor, an empty string will be returned. * It could also be {@code null} when the editor or system could not support this protocol. * than <var>length</var>. When there is no text after the cursor, an empty string will be * returned. It could also be {@code null} when the editor or system could not support this * protocol. */ @Nullable public CharSequence getInitialTextAfterCursor(int length, int flags) { Loading Loading @@ -863,7 +874,6 @@ public class EditorInfo implements InputType, Parcelable { return 0; } // TODO(b/148035211): Unit tests for this class static final class InitialSurroundingText implements Parcelable { @Nullable final CharSequence mSurroundingText; final int mSelectionHead; Loading core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java +105 −95 Original line number Diff line number Diff line Loading @@ -20,11 +20,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; import android.annotation.Nullable; import android.os.Parcel; import android.os.UserHandle; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; Loading @@ -41,6 +41,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class EditorInfoTest { private static final int TEST_USER_ID = 42; private static final int LONG_EXP_TEXT_LENGTH = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH * 2; /** * Makes sure that {@code null} {@link EditorInfo#targetInputMethodUser} can be copied via Loading Loading @@ -79,8 +80,8 @@ public class EditorInfoTest { } @Test public void testNullTextInputComposeInitialSurroundingText() { final Spannable testText = null; public void setInitialText_nullInputText_throwsException() { final CharSequence testText = null; final EditorInfo editorInfo = new EditorInfo(); try { Loading @@ -92,56 +93,75 @@ public class EditorInfoTest { } @Test public void testNonNullTextInputComposeInitialSurroundingText() { final Spannable testText = createTestText(/* prependLength= */ 0, EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); public void setInitialText_cursorAtHead_dividesByCursorPosition() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); // Cursor at position 0. int selectionLength = 0; final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 0; editorInfo.initialSelStart = 0; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; int expectedTextBeforeCursorLength = 0; int expectedTextAfterCursorLength = testText.length(); final int expectedTextBeforeCursorLength = 0; final int expectedTextAfterCursorLength = testText.length(); editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Cursor at the end. @Test public void setInitialText_cursorAtTail_dividesByCursorPosition() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 0; editorInfo.initialSelStart = testText.length() - selectionLength; editorInfo.initialSelEnd = testText.length(); expectedTextBeforeCursorLength = testText.length(); expectedTextAfterCursorLength = 0; final int expectedTextBeforeCursorLength = testText.length(); final int expectedTextAfterCursorLength = 0; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Cursor at the middle. selectionLength = 2; @Test public void setInitialText_cursorAtMiddle_dividesByCursorPosition() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 2; editorInfo.initialSelStart = testText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; expectedTextBeforeCursorLength = editorInfo.initialSelStart; expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; final int expectedTextBeforeCursorLength = editorInfo.initialSelStart; final int expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Accidentally swap selection start and end. @Test public void setInitialText_incorrectCursorOrder_correctsThenDivide() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 2; editorInfo.initialSelEnd = testText.length() / 2; editorInfo.initialSelStart = editorInfo.initialSelEnd + selectionLength; final int expectedTextBeforeCursorLength = testText.length() / 2; final int expectedTextAfterCursorLength = testText.length() - testText.length() / 2 - selectionLength; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Invalid cursor position. @Test public void setInitialText_invalidCursorPosition_returnsNull() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); editorInfo.initialSelStart = -1; editorInfo.setInitialSurroundingText(testText); Loading @@ -153,64 +173,33 @@ public class EditorInfoTest { } @Test public void testTooLongTextInputComposeInitialSurroundingText() { final Spannable testText = createTestText(/* prependLength= */ 0, EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH + 2); public void setOverSizeInitialText_cursorAtMiddle_dividesProportionately() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH + 2); final EditorInfo editorInfo = new EditorInfo(); // Cursor at position 0. int selectionLength = 0; editorInfo.initialSelStart = 0; editorInfo.initialSelEnd = 0 + selectionLength; int expectedTextBeforeCursorLength = 0; int expectedTextAfterCursorLength = editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); // Cursor at the end. editorInfo.initialSelStart = testText.length() - selectionLength; editorInfo.initialSelEnd = testText.length(); expectedTextBeforeCursorLength = editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH; expectedTextAfterCursorLength = 0; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); // Cursor at the middle. selectionLength = 2; final int selectionLength = 2; editorInfo.initialSelStart = testText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, final int expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, (int) (0.8 * (EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH - selectionLength))); expectedTextAfterCursorLength = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH final int expectedTextAfterCursorLength = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH - expectedTextBeforeCursorLength - selectionLength; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Accidentally swap selection start and end. editorInfo.initialSelEnd = testText.length() / 2; editorInfo.initialSelStart = editorInfo.initialSelEnd + selectionLength; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); // Selection too long, selected text should be dropped. selectionLength = EditorInfo.MAX_INITIAL_SELECTION_LENGTH + 1; @Test public void setOverSizeInitialText_overSizeSelection_dropsSelection() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH + 2); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = EditorInfo.MAX_INITIAL_SELECTION_LENGTH + 1; editorInfo.initialSelStart = testText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, final int expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, (int) (0.8 * EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH)); expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; final int expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; editorInfo.setInitialSurroundingText(testText); Loading @@ -219,34 +208,59 @@ public class EditorInfoTest { } @Test public void testTooLongSubTextInputComposeInitialSurroundingText() { final int prependLength = 5; final int subTextLength = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH; final Spannable fullText = createTestText(prependLength, subTextLength); public void setInitialSubText_trimmedSubText_dividesByOriginalCursorPosition() { final String prefixString = "prefix"; final CharSequence subText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final CharSequence originalText = TextUtils.concat(prefixString, subText); final EditorInfo editorInfo = new EditorInfo(); // Cursor at the middle. final int selectionLength = 2; editorInfo.initialSelStart = fullText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; // #prependLength characters will be trimmed out. final Spannable expectedTextBeforeCursor = createExpectedText(/* startNumber= */0, editorInfo.initialSelStart - prependLength); final Spannable expectedSelectedText = createExpectedText( editorInfo.initialSelStart - prependLength, selectionLength); final Spannable expectedTextAfterCursor = createExpectedText( editorInfo.initialSelEnd - prependLength, fullText.length() - editorInfo.initialSelEnd); editorInfo.setInitialSurroundingSubText(fullText.subSequence(prependLength, fullText.length()), prependLength); final int selLength = 2; editorInfo.initialSelStart = originalText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selLength; final CharSequence expectedTextBeforeCursor = createExpectedText(/* startNumber= */0, editorInfo.initialSelStart - prefixString.length()); final CharSequence expectedSelectedText = createExpectedText( editorInfo.initialSelStart - prefixString.length(), selLength); final CharSequence expectedTextAfterCursor = createExpectedText( editorInfo.initialSelEnd - prefixString.length(), originalText.length() - editorInfo.initialSelEnd); editorInfo.setInitialSurroundingSubText(subText, prefixString.length()); assertTrue(TextUtils.equals(expectedTextBeforeCursor, editorInfo.getInitialTextBeforeCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES))); editorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, anyInt()))); assertTrue(TextUtils.equals(expectedSelectedText, editorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES))); editorInfo.getInitialSelectedText(anyInt()))); assertTrue(TextUtils.equals(expectedTextAfterCursor, editorInfo.getInitialTextAfterCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, editorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, anyInt()))); } @Test public void initialSurroundingText_wrapIntoParcel_staysIntact() { // EditorInfo.InitialSurroundingText is not visible to test class. But all its key elements // must stay intact for its getter methods to return correct value and it will be wrapped // into its outer class for parcel transfer, therefore we can verify its parcel // wrapping/unwrapping logic through its outer class. final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo sourceEditorInfo = new EditorInfo(); final int selectionLength = 2; sourceEditorInfo.initialSelStart = testText.length() / 2; sourceEditorInfo.initialSelEnd = sourceEditorInfo.initialSelStart + selectionLength; sourceEditorInfo.setInitialSurroundingText(testText); final EditorInfo targetEditorInfo = cloneViaParcel(sourceEditorInfo); assertTrue(TextUtils.equals( sourceEditorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES), targetEditorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES))); assertTrue(TextUtils.equals( sourceEditorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES), targetEditorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES))); assertTrue(TextUtils.equals( sourceEditorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES), targetEditorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES))); } Loading @@ -254,12 +268,12 @@ public class EditorInfoTest { @Nullable Integer expectBeforeCursorLength, @Nullable Integer expectSelectionLength, @Nullable Integer expectAfterCursorLength) { final CharSequence textBeforeCursor = editorInfo.getInitialTextBeforeCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, editorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES); final CharSequence selectedText = editorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES); final CharSequence textAfterCursor = editorInfo.getInitialTextAfterCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, editorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES); if (expectBeforeCursorLength == null) { Loading @@ -281,19 +295,15 @@ public class EditorInfoTest { } } private static Spannable createTestText(int prependLength, int surroundingLength) { private static CharSequence createTestText(int surroundingLength) { final SpannableStringBuilder builder = new SpannableStringBuilder(); for (int i = 0; i < prependLength; i++) { builder.append("a"); } for (int i = 0; i < surroundingLength; i++) { builder.append(Integer.toString(i % 10)); } return builder; } private static Spannable createExpectedText(int startNumber, int length) { private static CharSequence createExpectedText(int startNumber, int length) { final SpannableStringBuilder builder = new SpannableStringBuilder(); for (int i = startNumber; i < startNumber + length; i++) { builder.append(Integer.toString(i % 10)); Loading Loading
core/java/android/view/inputmethod/EditorInfo.java +43 −33 Original line number Diff line number Diff line Loading @@ -33,11 +33,11 @@ import android.util.Printer; import android.view.View; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Objects; /** * An EditorInfo describes several attributes of a text editing object Loading Loading @@ -558,7 +558,7 @@ public class EditorInfo implements InputType, Parcelable { * editor wants to trim out the first 10 chars, subTextStart should be 10. */ public void setInitialSurroundingSubText(@NonNull CharSequence subText, int subTextStart) { Preconditions.checkNotNull(subText); Objects.requireNonNull(subText); // Swap selection start and end if necessary. final int subTextSelStart = initialSelStart > initialSelEnd Loading @@ -585,25 +585,35 @@ public class EditorInfo implements InputType, Parcelable { return; } // The input text is too long. Let's try to trim it reasonably. Fundamental rules are: // 1. Text before the cursor is the most important information to IMEs. // 2. Text after the cursor is the second important information to IMEs. // 3. Selected text is the least important information but it shall NEVER be truncated. // When it is too long, just drop it. // // Source: <TextBeforeCursor><Selection><TextAfterCursor> // Possible results: // 1. <(maybeTrimmedAtHead)TextBeforeCursor><Selection><TextAfterCursor(maybeTrimmedAtTail)> // 2. <(maybeTrimmedAtHead)TextBeforeCursor><TextAfterCursor(maybeTrimmedAtTail)> // final int sourceSelLength = subTextSelEnd - subTextSelStart; trimLongSurroundingText(subText, subTextSelStart, subTextSelEnd); } /** * Trims the initial surrounding text when it is over sized. Fundamental trimming rules are: * - The text before the cursor is the most important information to IMEs. * - The text after the cursor is the second important information to IMEs. * - The selected text is the least important information but it shall NEVER be truncated. When * it is too long, just drop it. *<p><pre> * For example, the subText can be viewed as * TextBeforeCursor + Selection + TextAfterCursor * The result could be * 1. (maybeTrimmedAtHead)TextBeforeCursor + Selection + TextAfterCursor(maybeTrimmedAtTail) * 2. (maybeTrimmedAtHead)TextBeforeCursor + TextAfterCursor(maybeTrimmedAtTail)</pre> * * @param subText The long text that needs to be trimmed. * @param selStart The text offset of the start of the selection. * @param selEnd The text offset of the end of the selection */ private void trimLongSurroundingText(CharSequence subText, int selStart, int selEnd) { final int sourceSelLength = selEnd - selStart; // When the selected text is too long, drop it. final int newSelLength = (sourceSelLength > MAX_INITIAL_SELECTION_LENGTH) ? 0 : sourceSelLength; // Distribute rest of length quota to TextBeforeCursor and TextAfterCursor in 4:1 ratio. final int subTextBeforeCursorLength = subTextSelStart; final int subTextAfterCursorLength = subTextLength - subTextSelEnd; final int subTextBeforeCursorLength = selStart; final int subTextAfterCursorLength = subText.length() - selEnd; final int maxLengthMinusSelection = MEMORY_EFFICIENT_TEXT_LENGTH - newSelLength; final int possibleMaxBeforeCursorLength = Math.min(subTextBeforeCursorLength, (int) (0.8 * maxLengthMinusSelection)); Loading @@ -617,24 +627,23 @@ public class EditorInfo implements InputType, Parcelable { // We don't want to cut surrogate pairs in the middle. Exam that at the new head and tail. if (isCutOnSurrogate(subText, subTextSelStart - newBeforeCursorLength, TrimPolicy.HEAD)) { selStart - newBeforeCursorLength, TrimPolicy.HEAD)) { newBeforeCursorHead = newBeforeCursorHead + 1; newBeforeCursorLength = newBeforeCursorLength - 1; } if (isCutOnSurrogate(subText, subTextSelEnd + newAfterCursorLength - 1, TrimPolicy.TAIL)) { selEnd + newAfterCursorLength - 1, TrimPolicy.TAIL)) { newAfterCursorLength = newAfterCursorLength - 1; } // Now we know where to trim, compose the initialSurroundingText. final int newTextLength = newBeforeCursorLength + newSelLength + newAfterCursorLength; CharSequence newInitialSurroundingText; final CharSequence newInitialSurroundingText; if (newSelLength != sourceSelLength) { final CharSequence beforeCursor = subText.subSequence(newBeforeCursorHead, newBeforeCursorHead + newBeforeCursorLength); final CharSequence afterCursor = subText.subSequence(subTextSelEnd, subTextSelEnd + newAfterCursorLength); final CharSequence afterCursor = subText.subSequence(selEnd, selEnd + newAfterCursorLength); newInitialSurroundingText = TextUtils.concat(beforeCursor, afterCursor); } else { Loading @@ -651,15 +660,16 @@ public class EditorInfo implements InputType, Parcelable { } /** * Get <var>n</var> characters of text before the current cursor position. May be {@code null} * when the protocol is not supported. * Get <var>length</var> characters of text before the current cursor position. May be * {@code null} when the protocol is not supported. * * @param length The expected length of the text. * @param flags Supplies additional options controlling how the text is returned. May be * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}. * @return the text before the cursor position; the length of the returned text might be less * than <var>n</var>. When there is no text before the cursor, an empty string will be returned. * It could also be {@code null} when the editor or system could not support this protocol. * than <var>length</var>. When there is no text before the cursor, an empty string will be * returned. It could also be {@code null} when the editor or system could not support this * protocol. */ @Nullable public CharSequence getInitialTextBeforeCursor(int length, int flags) { Loading @@ -667,8 +677,8 @@ public class EditorInfo implements InputType, Parcelable { } /** * Gets the selected text, if any. May be {@code null} when no text is selected or the selected * text is way too long. * Gets the selected text, if any. May be {@code null} when the protocol is not supported or the * selected text is way too long. * * @param flags Supplies additional options controlling how the text is returned. May be * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}. Loading @@ -693,15 +703,16 @@ public class EditorInfo implements InputType, Parcelable { } /** * Get <var>n</var> characters of text after the current cursor position. May be {@code null} * when the protocol is not supported. * Get <var>length</var> characters of text after the current cursor position. May be * {@code null} when the protocol is not supported. * * @param length The expected length of the text. * @param flags Supplies additional options controlling how the text is returned. May be * either 0 or {@link InputConnection#GET_TEXT_WITH_STYLES}. * @return the text after the cursor position; the length of the returned text might be less * than <var>n</var>. When there is no text after the cursor, an empty string will be returned. * It could also be {@code null} when the editor or system could not support this protocol. * than <var>length</var>. When there is no text after the cursor, an empty string will be * returned. It could also be {@code null} when the editor or system could not support this * protocol. */ @Nullable public CharSequence getInitialTextAfterCursor(int length, int flags) { Loading Loading @@ -863,7 +874,6 @@ public class EditorInfo implements InputType, Parcelable { return 0; } // TODO(b/148035211): Unit tests for this class static final class InitialSurroundingText implements Parcelable { @Nullable final CharSequence mSurroundingText; final int mSelectionHead; Loading
core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java +105 −95 Original line number Diff line number Diff line Loading @@ -20,11 +20,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; import android.annotation.Nullable; import android.os.Parcel; import android.os.UserHandle; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; Loading @@ -41,6 +41,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class EditorInfoTest { private static final int TEST_USER_ID = 42; private static final int LONG_EXP_TEXT_LENGTH = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH * 2; /** * Makes sure that {@code null} {@link EditorInfo#targetInputMethodUser} can be copied via Loading Loading @@ -79,8 +80,8 @@ public class EditorInfoTest { } @Test public void testNullTextInputComposeInitialSurroundingText() { final Spannable testText = null; public void setInitialText_nullInputText_throwsException() { final CharSequence testText = null; final EditorInfo editorInfo = new EditorInfo(); try { Loading @@ -92,56 +93,75 @@ public class EditorInfoTest { } @Test public void testNonNullTextInputComposeInitialSurroundingText() { final Spannable testText = createTestText(/* prependLength= */ 0, EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); public void setInitialText_cursorAtHead_dividesByCursorPosition() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); // Cursor at position 0. int selectionLength = 0; final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 0; editorInfo.initialSelStart = 0; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; int expectedTextBeforeCursorLength = 0; int expectedTextAfterCursorLength = testText.length(); final int expectedTextBeforeCursorLength = 0; final int expectedTextAfterCursorLength = testText.length(); editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Cursor at the end. @Test public void setInitialText_cursorAtTail_dividesByCursorPosition() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 0; editorInfo.initialSelStart = testText.length() - selectionLength; editorInfo.initialSelEnd = testText.length(); expectedTextBeforeCursorLength = testText.length(); expectedTextAfterCursorLength = 0; final int expectedTextBeforeCursorLength = testText.length(); final int expectedTextAfterCursorLength = 0; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Cursor at the middle. selectionLength = 2; @Test public void setInitialText_cursorAtMiddle_dividesByCursorPosition() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 2; editorInfo.initialSelStart = testText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; expectedTextBeforeCursorLength = editorInfo.initialSelStart; expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; final int expectedTextBeforeCursorLength = editorInfo.initialSelStart; final int expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Accidentally swap selection start and end. @Test public void setInitialText_incorrectCursorOrder_correctsThenDivide() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = 2; editorInfo.initialSelEnd = testText.length() / 2; editorInfo.initialSelStart = editorInfo.initialSelEnd + selectionLength; final int expectedTextBeforeCursorLength = testText.length() / 2; final int expectedTextAfterCursorLength = testText.length() - testText.length() / 2 - selectionLength; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Invalid cursor position. @Test public void setInitialText_invalidCursorPosition_returnsNull() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo editorInfo = new EditorInfo(); editorInfo.initialSelStart = -1; editorInfo.setInitialSurroundingText(testText); Loading @@ -153,64 +173,33 @@ public class EditorInfoTest { } @Test public void testTooLongTextInputComposeInitialSurroundingText() { final Spannable testText = createTestText(/* prependLength= */ 0, EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH + 2); public void setOverSizeInitialText_cursorAtMiddle_dividesProportionately() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH + 2); final EditorInfo editorInfo = new EditorInfo(); // Cursor at position 0. int selectionLength = 0; editorInfo.initialSelStart = 0; editorInfo.initialSelEnd = 0 + selectionLength; int expectedTextBeforeCursorLength = 0; int expectedTextAfterCursorLength = editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); // Cursor at the end. editorInfo.initialSelStart = testText.length() - selectionLength; editorInfo.initialSelEnd = testText.length(); expectedTextBeforeCursorLength = editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH; expectedTextAfterCursorLength = 0; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); // Cursor at the middle. selectionLength = 2; final int selectionLength = 2; editorInfo.initialSelStart = testText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, final int expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, (int) (0.8 * (EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH - selectionLength))); expectedTextAfterCursorLength = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH final int expectedTextAfterCursorLength = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH - expectedTextBeforeCursorLength - selectionLength; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); } // Accidentally swap selection start and end. editorInfo.initialSelEnd = testText.length() / 2; editorInfo.initialSelStart = editorInfo.initialSelEnd + selectionLength; editorInfo.setInitialSurroundingText(testText); assertExpectedTextLength(editorInfo, expectedTextBeforeCursorLength, selectionLength, expectedTextAfterCursorLength); // Selection too long, selected text should be dropped. selectionLength = EditorInfo.MAX_INITIAL_SELECTION_LENGTH + 1; @Test public void setOverSizeInitialText_overSizeSelection_dropsSelection() { final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH + 2); final EditorInfo editorInfo = new EditorInfo(); final int selectionLength = EditorInfo.MAX_INITIAL_SELECTION_LENGTH + 1; editorInfo.initialSelStart = testText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, final int expectedTextBeforeCursorLength = Math.min(editorInfo.initialSelStart, (int) (0.8 * EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH)); expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; final int expectedTextAfterCursorLength = testText.length() - editorInfo.initialSelEnd; editorInfo.setInitialSurroundingText(testText); Loading @@ -219,34 +208,59 @@ public class EditorInfoTest { } @Test public void testTooLongSubTextInputComposeInitialSurroundingText() { final int prependLength = 5; final int subTextLength = EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH; final Spannable fullText = createTestText(prependLength, subTextLength); public void setInitialSubText_trimmedSubText_dividesByOriginalCursorPosition() { final String prefixString = "prefix"; final CharSequence subText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final CharSequence originalText = TextUtils.concat(prefixString, subText); final EditorInfo editorInfo = new EditorInfo(); // Cursor at the middle. final int selectionLength = 2; editorInfo.initialSelStart = fullText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selectionLength; // #prependLength characters will be trimmed out. final Spannable expectedTextBeforeCursor = createExpectedText(/* startNumber= */0, editorInfo.initialSelStart - prependLength); final Spannable expectedSelectedText = createExpectedText( editorInfo.initialSelStart - prependLength, selectionLength); final Spannable expectedTextAfterCursor = createExpectedText( editorInfo.initialSelEnd - prependLength, fullText.length() - editorInfo.initialSelEnd); editorInfo.setInitialSurroundingSubText(fullText.subSequence(prependLength, fullText.length()), prependLength); final int selLength = 2; editorInfo.initialSelStart = originalText.length() / 2; editorInfo.initialSelEnd = editorInfo.initialSelStart + selLength; final CharSequence expectedTextBeforeCursor = createExpectedText(/* startNumber= */0, editorInfo.initialSelStart - prefixString.length()); final CharSequence expectedSelectedText = createExpectedText( editorInfo.initialSelStart - prefixString.length(), selLength); final CharSequence expectedTextAfterCursor = createExpectedText( editorInfo.initialSelEnd - prefixString.length(), originalText.length() - editorInfo.initialSelEnd); editorInfo.setInitialSurroundingSubText(subText, prefixString.length()); assertTrue(TextUtils.equals(expectedTextBeforeCursor, editorInfo.getInitialTextBeforeCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES))); editorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, anyInt()))); assertTrue(TextUtils.equals(expectedSelectedText, editorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES))); editorInfo.getInitialSelectedText(anyInt()))); assertTrue(TextUtils.equals(expectedTextAfterCursor, editorInfo.getInitialTextAfterCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, editorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, anyInt()))); } @Test public void initialSurroundingText_wrapIntoParcel_staysIntact() { // EditorInfo.InitialSurroundingText is not visible to test class. But all its key elements // must stay intact for its getter methods to return correct value and it will be wrapped // into its outer class for parcel transfer, therefore we can verify its parcel // wrapping/unwrapping logic through its outer class. final CharSequence testText = createTestText(EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH); final EditorInfo sourceEditorInfo = new EditorInfo(); final int selectionLength = 2; sourceEditorInfo.initialSelStart = testText.length() / 2; sourceEditorInfo.initialSelEnd = sourceEditorInfo.initialSelStart + selectionLength; sourceEditorInfo.setInitialSurroundingText(testText); final EditorInfo targetEditorInfo = cloneViaParcel(sourceEditorInfo); assertTrue(TextUtils.equals( sourceEditorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES), targetEditorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES))); assertTrue(TextUtils.equals( sourceEditorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES), targetEditorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES))); assertTrue(TextUtils.equals( sourceEditorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES), targetEditorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES))); } Loading @@ -254,12 +268,12 @@ public class EditorInfoTest { @Nullable Integer expectBeforeCursorLength, @Nullable Integer expectSelectionLength, @Nullable Integer expectAfterCursorLength) { final CharSequence textBeforeCursor = editorInfo.getInitialTextBeforeCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, editorInfo.getInitialTextBeforeCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES); final CharSequence selectedText = editorInfo.getInitialSelectedText(InputConnection.GET_TEXT_WITH_STYLES); final CharSequence textAfterCursor = editorInfo.getInitialTextAfterCursor(editorInfo.MEMORY_EFFICIENT_TEXT_LENGTH, editorInfo.getInitialTextAfterCursor(LONG_EXP_TEXT_LENGTH, InputConnection.GET_TEXT_WITH_STYLES); if (expectBeforeCursorLength == null) { Loading @@ -281,19 +295,15 @@ public class EditorInfoTest { } } private static Spannable createTestText(int prependLength, int surroundingLength) { private static CharSequence createTestText(int surroundingLength) { final SpannableStringBuilder builder = new SpannableStringBuilder(); for (int i = 0; i < prependLength; i++) { builder.append("a"); } for (int i = 0; i < surroundingLength; i++) { builder.append(Integer.toString(i % 10)); } return builder; } private static Spannable createExpectedText(int startNumber, int length) { private static CharSequence createExpectedText(int startNumber, int length) { final SpannableStringBuilder builder = new SpannableStringBuilder(); for (int i = startNumber; i < startNumber + length; i++) { builder.append(Integer.toString(i % 10)); Loading