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

Commit 710fef67 authored by Haoyu Zhang's avatar Haoyu Zhang Committed by Automerger Merge Worker
Browse files

Merge "Fix: insert mode crash when EmojiCompat is used" into udc-dev am: 74ad406b am: ddd6d9a2

parents 43d826bf ddd6d9a2
Loading
Loading
Loading
Loading
+24 −3
Original line number Diff line number Diff line
@@ -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) {
                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);
        }
+124 −0
Original line number Diff line number Diff line
@@ -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.
     */