Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4b41675b authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Refine surrounding text logic and test case readability."

parents 45b0fc87 4de5306e
Loading
Loading
Loading
Loading
+43 −33
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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));
@@ -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 {
@@ -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) {
@@ -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}.
@@ -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) {
@@ -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;
+105 −95
Original line number Diff line number Diff line
@@ -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;

@@ -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
@@ -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 {
@@ -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);
@@ -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);

@@ -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)));
    }

@@ -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) {
@@ -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));