Fix: insert mode crash when EmojiCompat is used

Bug: 279057703
Test: atest DynamicLayoutOffsetMappingTest
Change-Id: I2469cdb0c4cba5ef91799b79fad3155e65ff24bb
This commit is contained in:
Haoyu Zhang
2023-04-20 12:12:18 -07:00
parent 497d09ac7e
commit 47ef59ecf0
2 changed files with 148 additions and 3 deletions

View File

@@ -1106,6 +1106,16 @@ public class DynamicLayout extends Layout {
mTransformedTextUpdate.before = before;
mTransformedTextUpdate.after = after;
}
// When there is a transformed text, we have to reflow the DynamicLayout based on
// the transformed indices instead of the range in base text.
// For example,
// base text: abcd > abce
// updated range: where = 3, before = 1, after = 1
// transformed text: abxxcd > abxxce
// updated range: where = 5, before = 1, after = 1
//
// Because the transformedText is udapted simultaneously with the base text,
// the range must be transformed before the base text changes.
transformedText.originalToTransformed(mTransformedTextUpdate);
}
}
@@ -1113,9 +1123,20 @@ public class DynamicLayout extends Layout {
public void onTextChanged(CharSequence s, int where, int before, int after) {
final DynamicLayout dynamicLayout = mLayout.get();
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
where = mTransformedTextUpdate.where;
before = mTransformedTextUpdate.before;
after = mTransformedTextUpdate.after;
if (mTransformedTextUpdate != null && mTransformedTextUpdate.where >= 0) {
where = mTransformedTextUpdate.where;
before = mTransformedTextUpdate.before;
after = mTransformedTextUpdate.after;
// Set where to -1 so that we know if beforeTextChanged is called.
mTransformedTextUpdate.where = -1;
} else {
// onTextChanged is called without beforeTextChanged. Reflow the entire text.
where = 0;
// We can't get the before length from the text, use the line end of the
// last line instead.
before = dynamicLayout.getLineEnd(dynamicLayout.getLineCount() - 1);
after = dynamicLayout.mDisplay.length();
}
}
reflow(s, where, before, after);
}

View File

@@ -119,6 +119,86 @@ public class DynamicLayoutOffsetMappingTest {
assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8);
}
@Test
public void textWithOffsetMapping_blockBeforeTextChanged_deletion() {
final String text = "abcdef";
final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text);
final CharSequence transformedText =
new TestOffsetMapping(spannable, 5, "\n\n");
final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
.setAlignment(ALIGN_NORMAL)
.setIncludePad(false)
.setDisplayText(transformedText)
.build();
// delete "cd", original text becomes "abef"
spannable.delete(2, 4);
assertThat(transformedText.toString()).isEqualTo("abe\n\nf");
assertLineRange(layout, /* lineBreaks */ 0, 4, 5, 6);
// delete "abe", original text becomes "f"
spannable.delete(0, 3);
assertThat(transformedText.toString()).isEqualTo("\n\nf");
assertLineRange(layout, /* lineBreaks */ 0, 1, 2, 3);
}
@Test
public void textWithOffsetMapping_blockBeforeTextChanged_insertion() {
final String text = "abcdef";
final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text);
final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
.setAlignment(ALIGN_NORMAL)
.setIncludePad(false)
.setDisplayText(transformedText)
.build();
spannable.insert(3, "x");
assertThat(transformedText.toString()).isEqualTo("abcx\n\ndef");
assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 9);
spannable.insert(5, "x");
assertThat(transformedText.toString()).isEqualTo("abcx\n\ndxef");
assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 10);
}
@Test
public void textWithOffsetMapping_blockBeforeTextChanged_replace() {
final String text = "abcdef";
final SpannableStringBuilder spannable = new TestNoBeforeTextChangeSpannableString(text);
final CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
final DynamicLayout layout = DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
.setAlignment(ALIGN_NORMAL)
.setIncludePad(false)
.setDisplayText(transformedText)
.build();
spannable.replace(2, 4, "xx");
assertThat(transformedText.toString()).isEqualTo("abxx\n\nef");
assertLineRange(layout, /* lineBreaks */ 0, 5, 6, 8);
}
@Test
public void textWithOffsetMapping_onlyCallOnTextChanged_notCrash() {
String text = "abcdef";
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
CharSequence transformedText = new TestOffsetMapping(spannable, 3, "\n\n");
DynamicLayout.Builder.obtain(spannable, sTextPaint, WIDTH)
.setAlignment(ALIGN_NORMAL)
.setIncludePad(false)
.setDisplayText(transformedText)
.build();
TextWatcher[] textWatcher = spannable.getSpans(0, spannable.length(), TextWatcher.class);
assertThat(textWatcher.length).isEqualTo(1);
textWatcher[0].onTextChanged(spannable, 0, 2, 2);
}
private void assertLineRange(Layout layout, int... lineBreaks) {
final int lineCount = lineBreaks.length - 1;
assertThat(layout.getLineCount()).isEqualTo(lineCount);
@@ -128,6 +208,50 @@ public class DynamicLayoutOffsetMappingTest {
assertThat(layout.getLineEnd(lineCount - 1)).isEqualTo(lineBreaks[lineCount]);
}
/**
* A test SpannableStringBuilder that doesn't call beforeTextChanged. It's used to test
* DynamicLayout against some special cases where beforeTextChanged callback is not properly
* called.
*/
private static class TestNoBeforeTextChangeSpannableString extends SpannableStringBuilder {
TestNoBeforeTextChangeSpannableString(CharSequence text) {
super(text);
}
@Override
public void setSpan(Object what, int start, int end, int flags) {
if (what instanceof TextWatcher) {
super.setSpan(new TestNoBeforeTextChangeWatcherWrapper((TextWatcher) what), start,
end, flags);
} else {
super.setSpan(what, start, end, flags);
}
}
}
/** A TextWatcherWrapper that blocks beforeTextChanged callback. */
private static class TestNoBeforeTextChangeWatcherWrapper implements TextWatcher {
private final TextWatcher mTextWatcher;
TestNoBeforeTextChangeWatcherWrapper(TextWatcher textWatcher) {
mTextWatcher = textWatcher;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mTextWatcher.onTextChanged(s, start, before, count);
}
@Override
public void afterTextChanged(Editable s) {
mTextWatcher.afterTextChanged(s);
}
}
/**
* A test TransformedText that inserts some text at the given offset.
*/