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

Commit 4de5306e authored by Chris.CC Lee's avatar Chris.CC Lee
Browse files

Refine surrounding text logic and test case readability.

1. Follow up refinement for CL[1].
2. Test case to verify the InitialSurroundingText parcel
wrapping/unwrapping logic.

[1] c486acc4, Ie04f2349b1157408aa8ed9044aea12ce99132cb4

Bug: 148035211
Test: atest FrameworksCoreTests:EditorInfoTest
Change-Id: I9609b8a7dabb6285ed673cb0890bb3019e3b7caa
parent aa6753b9
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));