Fix: EditText crash when insert emoji in insert mode

InsertModeTransformationMethod.TransformedText#getSpan returns wrong
spans when the given range is collapsed. This caused the EditText to
crash when insert mode and EmojiCompat are used together. This CL fixed
the above issue and also updated the tests.

Bug: 278153638
Test: atest InsertModeTransformationMethodTest
Change-Id: Ia036be0efc1cb358857ff827f0fba7d60fb58912
This commit is contained in:
Haoyu Zhang
2023-04-20 10:46:48 -07:00
parent 54c2554def
commit 8603da6323
2 changed files with 194 additions and 11 deletions

View File

@@ -37,6 +37,8 @@ import android.view.View;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import java.lang.reflect.Array;
/**
* The transformation method used by handwriting insert mode.
* This transformation will insert a placeholder string to the original text at the given
@@ -309,26 +311,51 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex
return ArrayUtils.emptyArray(type);
}
final T[] spansOriginal;
T[] spansOriginal = null;
if (mSpannedOriginal != null) {
final int originalStart =
transformedToOriginal(start, OffsetMapping.MAP_STRATEGY_CURSOR);
final int originalEnd =
transformedToOriginal(end, OffsetMapping.MAP_STRATEGY_CURSOR);
// We can't simply call SpannedString.getSpans(originalStart, originalEnd) here.
// When start == end SpannedString.getSpans returns spans whose spanEnd == start.
// For example,
// text: abcd span: [1, 3)
// getSpan(3, 3) will return the span [1, 3) but getSpan(3, 4) returns no span.
//
// This creates some special cases when originalStart == originalEnd.
// For example:
// original text: abcd span1: [1, 3) span2: [3, 4) span3: [3, 3)
// transformed text: abc\n\nd span1: [1, 3) span2: [5, 6) span3: [3, 3)
// Case 1:
// When start = 3 and end = 4, transformedText#getSpan(3, 4) should return span3.
// However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3)
// returns span1, span2 and span3.
//
// Case 2:
// When start == end == 4, transformedText#getSpan(4, 4) should return nothing.
// However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3)
// return span1, span2 and span3.
//
// Case 3:
// When start == end == 5, transformedText#getSpan(5, 5) should return span2.
// However, because originalStart == originalEnd == 3, originalText#getSpan(3, 3)
// return span1, span2 and span3.
//
// To handle the issue, we need to filter out the invalid spans.
spansOriginal = mSpannedOriginal.getSpans(originalStart, originalEnd, type);
} else {
spansOriginal = null;
spansOriginal = ArrayUtils.filter(spansOriginal,
size -> (T[]) Array.newInstance(type, size),
span -> intersect(getSpanStart(span), getSpanEnd(span), start, end));
}
final T[] spansPlaceholder;
T[] spansPlaceholder = null;
if (mSpannedPlaceholder != null
&& intersect(start, end, mEnd, mEnd + mPlaceholder.length())) {
final int placeholderStart = Math.max(start - mEnd, 0);
final int placeholderEnd = Math.min(end - mEnd, mPlaceholder.length());
int placeholderStart = Math.max(start - mEnd, 0);
int placeholderEnd = Math.min(end - mEnd, mPlaceholder.length());
spansPlaceholder =
mSpannedPlaceholder.getSpans(placeholderStart, placeholderEnd, type);
} else {
spansPlaceholder = null;
}
// TODO: sort the spans based on their priority.
@@ -340,7 +367,10 @@ public class InsertModeTransformationMethod implements TransformationMethod, Tex
if (mSpannedOriginal != null) {
final int index = mSpannedOriginal.getSpanStart(tag);
if (index >= 0) {
if (index < mEnd) {
// When originalSpanStart == originalSpanEnd == mEnd, the span should be
// considered "before" the placeholder text. So we return the originalSpanStart.
if (index < mEnd
|| (index == mEnd && mSpannedOriginal.getSpanEnd(tag) == index)) {
return index;
}
return index + mPlaceholder.length();

View File

@@ -224,6 +224,12 @@ public class InsertModeTransformationMethodTest {
assertThat(spans0to2.length).isEqualTo(1);
assertThat(spans0to2[0]).isEqualTo(span1);
// only span2 is in the range of [3, 4).
// note: span1 [0, 3) is not in the range because [3, 4) is not collapsed.
final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class);
assertThat(spans3to4.length).isEqualTo(1);
assertThat(spans3to4[0]).isEqualTo(span2);
// span1 and span2 are in the range of [1, 6).
final TestSpan[] spans1to6 = transformedText.getSpans(1, 6, TestSpan.class);
assertThat(spans1to6.length).isEqualTo(2);
@@ -262,7 +268,7 @@ public class InsertModeTransformationMethodTest {
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// In the transformedText, the new ranges of the spans are:
// In the transformedText "abc\uFFFD def", the new ranges of the spans are:
// span1: [0, 3)
// span2: [2, 5)
// span3: [5, 6)
@@ -277,6 +283,12 @@ public class InsertModeTransformationMethodTest {
assertThat(spans0to2.length).isEqualTo(1);
assertThat(spans0to2[0]).isEqualTo(span1);
// only span2 is in the range of [3, 4).
// note: span1 [0, 3) is not in the range because [3, 4) is not collapsed.
final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class);
assertThat(spans3to4.length).isEqualTo(1);
assertThat(spans3to4[0]).isEqualTo(span2);
// span1 and span2 are in the range of [1, 5).
final TestSpan[] spans1to4 = transformedText.getSpans(1, 4, TestSpan.class);
assertThat(spans1to4.length).isEqualTo(2);
@@ -318,20 +330,143 @@ public class InsertModeTransformationMethodTest {
}
@Test
public void transformedText_getSpanStartAndEnd() {
public void transformedText_getSpans_collapsedRange() {
final SpannableString text = new SpannableString(TEXT);
final TestSpan span1 = new TestSpan();
final TestSpan span2 = new TestSpan();
final TestSpan span3 = new TestSpan();
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span2, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span3, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// In the transformedText "abc\n\n def", the new ranges of the spans are:
// span1: [0, 3)
// span2: [3, 3)
// span3: [5, 6)
final InsertModeTransformationMethod transformationMethod =
new InsertModeTransformationMethod(3, false, null);
final Spanned transformedText =
(Spanned) transformationMethod.getTransformation(text, sView);
// only span1 is in the range of [0, 0).
final TestSpan[] spans0to0 = transformedText.getSpans(0, 0, TestSpan.class);
assertThat(spans0to0.length).isEqualTo(1);
assertThat(spans0to0[0]).isEqualTo(span1);
// span1 and span 2 are in the range of [3, 3).
final TestSpan[] spans3to3 = transformedText.getSpans(3, 3, TestSpan.class);
assertThat(spans3to3.length).isEqualTo(2);
assertThat(spans3to3[0]).isEqualTo(span1);
assertThat(spans3to3[1]).isEqualTo(span2);
// only the span2 with collapsed range is in the range of [3, 4).
final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class);
assertThat(spans3to4.length).isEqualTo(1);
assertThat(spans3to4[0]).isEqualTo(span2);
// no span is in the range of [4, 5). (span2 is not mistakenly included.)
final TestSpan[] spans4to5 = transformedText.getSpans(4, 5, TestSpan.class);
assertThat(spans4to5).isEmpty();
// only span3 is in the range of [4, 6). (span2 is not mistakenly included.)
final TestSpan[] spans4to6 = transformedText.getSpans(4, 6, TestSpan.class);
assertThat(spans4to6.length).isEqualTo(1);
assertThat(spans4to6[0]).isEqualTo(span3);
// no span is in the range of [4, 4).
final TestSpan[] spans4to4 = transformedText.getSpans(4, 4, TestSpan.class);
assertThat(spans4to4.length).isEqualTo(0);
// span3 is in the range of [5, 5).
final TestSpan[] spans5to5 = transformedText.getSpans(5, 5, TestSpan.class);
assertThat(spans5to5.length).isEqualTo(1);
assertThat(spans5to5[0]).isEqualTo(span3);
// span3 is in the range of [6, 6).
final TestSpan[] spans6to6 = transformedText.getSpans(6, 6, TestSpan.class);
assertThat(spans6to6.length).isEqualTo(1);
assertThat(spans6to6[0]).isEqualTo(span3);
}
@Test
public void transformedText_getSpans_collapsedRange_singleLine() {
final SpannableString text = new SpannableString(TEXT);
final TestSpan span1 = new TestSpan();
final TestSpan span2 = new TestSpan();
final TestSpan span3 = new TestSpan();
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span2, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span3, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// In the transformedText "abc\uFFFD def", the new ranges of the spans are:
// span1: [0, 3)
// span2: [3, 3)
// span3: [4, 5)
final InsertModeTransformationMethod transformationMethod =
new InsertModeTransformationMethod(3, true, null);
final Spanned transformedText =
(Spanned) transformationMethod.getTransformation(text, sView);
// only span1 is in the range of [0, 0).
final TestSpan[] spans0to0 = transformedText.getSpans(0, 0, TestSpan.class);
assertThat(spans0to0.length).isEqualTo(1);
assertThat(spans0to0[0]).isEqualTo(span1);
// span1 and span2 are in the range of [3, 3).
final TestSpan[] spans3to3 = transformedText.getSpans(3, 3, TestSpan.class);
assertThat(spans3to3.length).isEqualTo(2);
assertThat(spans3to3[0]).isEqualTo(span1);
assertThat(spans3to3[1]).isEqualTo(span2);
// only the span2 with collapsed range is in the range of [3, 4).
final TestSpan[] spans3to4 = transformedText.getSpans(3, 4, TestSpan.class);
assertThat(spans3to4.length).isEqualTo(1);
assertThat(spans3to4[0]).isEqualTo(span2);
// span3 is in the range of [4, 5). (span2 is not mistakenly included.)
final TestSpan[] spans4to5 = transformedText.getSpans(4, 5, TestSpan.class);
assertThat(spans4to5.length).isEqualTo(1);
assertThat(spans4to5[0]).isEqualTo(span3);
// only span3 is in the range of [4, 6). (span2 is not mistakenly included.)
final TestSpan[] spans4to6 = transformedText.getSpans(4, 6, TestSpan.class);
assertThat(spans4to6.length).isEqualTo(1);
assertThat(spans4to6[0]).isEqualTo(span3);
// span3 is in the range of [4, 4).
final TestSpan[] spans4to4 = transformedText.getSpans(4, 4, TestSpan.class);
assertThat(spans4to4.length).isEqualTo(1);
assertThat(spans4to4[0]).isEqualTo(span3);
// span3 is in the range of [5, 5).
final TestSpan[] spans5to5 = transformedText.getSpans(5, 5, TestSpan.class);
assertThat(spans5to5.length).isEqualTo(1);
assertThat(spans5to5[0]).isEqualTo(span3);
}
@Test
public void transformedText_getSpanStartAndEnd() {
final SpannableString text = new SpannableString(TEXT);
final TestSpan span1 = new TestSpan();
final TestSpan span2 = new TestSpan();
final TestSpan span3 = new TestSpan();
final TestSpan span4 = new TestSpan();
final TestSpan span5 = new TestSpan();
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span4, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span5, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// In the transformedText, the new ranges of the spans are:
// span1: [0, 3)
// span2: [2, 6)
// span3: [6, 7)
// span4: [3, 3)
// span5: [5, 6)
final InsertModeTransformationMethod transformationMethod =
new InsertModeTransformationMethod(3, false, null);
final Spanned transformedText =
@@ -345,6 +480,12 @@ public class InsertModeTransformationMethodTest {
assertThat(transformedText.getSpanStart(span3)).isEqualTo(6);
assertThat(transformedText.getSpanEnd(span3)).isEqualTo(7);
assertThat(transformedText.getSpanStart(span4)).isEqualTo(3);
assertThat(transformedText.getSpanEnd(span4)).isEqualTo(3);
assertThat(transformedText.getSpanStart(span5)).isEqualTo(5);
assertThat(transformedText.getSpanEnd(span5)).isEqualTo(6);
}
@Test
@@ -353,15 +494,21 @@ public class InsertModeTransformationMethodTest {
final TestSpan span1 = new TestSpan();
final TestSpan span2 = new TestSpan();
final TestSpan span3 = new TestSpan();
final TestSpan span4 = new TestSpan();
final TestSpan span5 = new TestSpan();
text.setSpan(span1, 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span2, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span3, 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span4, 3, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(span5, 3, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// In the transformedText, the new ranges of the spans are:
// span1: [0, 3)
// span2: [2, 5)
// span3: [5, 6)
// span4: [3. 3)
// span5: [4, 5)
final InsertModeTransformationMethod transformationMethod =
new InsertModeTransformationMethod(3, true, null);
final Spanned transformedText =
@@ -376,6 +523,12 @@ public class InsertModeTransformationMethodTest {
assertThat(transformedText.getSpanStart(span3)).isEqualTo(5);
assertThat(transformedText.getSpanEnd(span3)).isEqualTo(6);
assertThat(transformedText.getSpanStart(span4)).isEqualTo(3);
assertThat(transformedText.getSpanEnd(span4)).isEqualTo(3);
assertThat(transformedText.getSpanStart(span5)).isEqualTo(4);
assertThat(transformedText.getSpanEnd(span5)).isEqualTo(5);
final ReplacementSpan[] replacementSpans =
transformedText.getSpans(0, 8, ReplacementSpan.class);
assertThat(transformedText.getSpanStart(replacementSpans[0])).isEqualTo(3);