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

This commit is contained in:
TreeHugger Robot
2020-02-10 02:07:19 +00:00
committed by Android (Google) Code Review
2 changed files with 147 additions and 127 deletions

View File

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

View File

@@ -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);
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(fullText.subSequence(prependLength,
fullText.length()), prependLength);
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));