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

Commit 3483bc7d authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Introduce new constructor for not copying NoCopySpan

To hold the original text in PrecomputedText, need to create
SpannableString, but copying NoCopySpan causes some side effect.
This CL introduces a way of copying SpannableString/SpannedString
with all spans other than NoCopySpan.

Bug: 72998298
Bug: 35638900
Test: atest CtsWidgetTestCases:EditTextTest
    CtsWidgetTestCases:TextViewFadingEdgeTest
    FrameworksCoreTests:TextViewFallbackLineSpacingTest
    FrameworksCoreTests:TextViewTest FrameworksCoreTests:TypefaceTest
    CtsGraphicsTestCases:TypefaceTest CtsWidgetTestCases:TextViewTest
    CtsTextTestCases FrameworksCoreTests:android.text
    CtsWidgetTestCases:TextViewPrecomputedTextTest

Change-Id: I20dea2114ccaa54b16ff679c97682a5003f9a4c1
parent 1817c132
Loading
Loading
Loading
Loading
+17 −3
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package android.text;


/**
 * This is the class for text whose content is immutable but to which
 * markup objects can be attached and detached.
@@ -26,12 +25,27 @@ public class SpannableString
extends SpannableStringInternal
implements CharSequence, GetChars, Spannable
{
    /**
     * @param source source object to copy from
     * @param ignoreNoCopySpan whether to copy NoCopySpans in the {@code source}
     * @hide
     */
    public SpannableString(CharSequence source, boolean ignoreNoCopySpan) {
        super(source, 0, source.length(), ignoreNoCopySpan);
    }

    /**
     * For the backward compatibility reasons, this constructor copies all spans including {@link
     * android.text.NoCopySpan}.
     * @param source source text
     */
    public SpannableString(CharSequence source) {
        super(source, 0, source.length());
        this(source, false /* ignoreNoCopySpan */);  // preserve existing NoCopySpan behavior
    }

    private SpannableString(CharSequence source, int start, int end) {
        super(source, start, end);
        // preserve existing NoCopySpan behavior
        super(source, start, end, false /* ignoreNoCopySpan */);
    }

    public static SpannableString valueOf(CharSequence source) {
+61 −20
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@ import java.lang.reflect.Array;
/* package */ abstract class SpannableStringInternal
{
    /* package */ SpannableStringInternal(CharSequence source,
                                          int start, int end) {
                                          int start, int end, boolean ignoreNoCopySpan) {
        if (start == 0 && end == source.length())
            mText = source.toString();
        else
@@ -38,24 +38,37 @@ import java.lang.reflect.Array;

        if (source instanceof Spanned) {
            if (source instanceof SpannableStringInternal) {
                copySpans((SpannableStringInternal) source, start, end);
                copySpans((SpannableStringInternal) source, start, end, ignoreNoCopySpan);
            } else {
                copySpans((Spanned) source, start, end);
                copySpans((Spanned) source, start, end, ignoreNoCopySpan);
            }
        }
    }

    /**
     * This unused method is left since this is listed in hidden api list.
     *
     * Due to backward compatibility reasons, we copy even NoCopySpan by default
     */
    /* package */ SpannableStringInternal(CharSequence source, int start, int end) {
        this(source, start, end, false /* ignoreNoCopySpan */);
    }

    /**
     * Copies another {@link Spanned} object's spans between [start, end] into this object.
     *
     * @param src Source object to copy from.
     * @param start Start index in the source object.
     * @param end End index in the source object.
     * @param ignoreNoCopySpan whether to copy NoCopySpans in the {@code source}
     */
    private final void copySpans(Spanned src, int start, int end) {
    private void copySpans(Spanned src, int start, int end, boolean ignoreNoCopySpan) {
        Object[] spans = src.getSpans(start, end, Object.class);

        for (int i = 0; i < spans.length; i++) {
            if (ignoreNoCopySpan && spans[i] instanceof NoCopySpan) {
                continue;
            }
            int st = src.getSpanStart(spans[i]);
            int en = src.getSpanEnd(spans[i]);
            int fl = src.getSpanFlags(spans[i]);
@@ -76,35 +89,48 @@ import java.lang.reflect.Array;
     * @param src Source object to copy from.
     * @param start Start index in the source object.
     * @param end End index in the source object.
     * @param ignoreNoCopySpan copy NoCopySpan for backward compatible reasons.
     */
    private final void copySpans(SpannableStringInternal src, int start, int end) {
        if (start == 0 && end == src.length()) {
            mSpans = ArrayUtils.newUnpaddedObjectArray(src.mSpans.length);
            mSpanData = new int[src.mSpanData.length];
            mSpanCount = src.mSpanCount;
            System.arraycopy(src.mSpans, 0, mSpans, 0, src.mSpans.length);
            System.arraycopy(src.mSpanData, 0, mSpanData, 0, mSpanData.length);
        } else {
    private void copySpans(SpannableStringInternal src, int start, int end,
            boolean ignoreNoCopySpan) {
        int count = 0;
            int[] srcData = src.mSpanData;
            int limit = src.mSpanCount;
        final int[] srcData = src.mSpanData;
        final Object[] srcSpans = src.mSpans;
        final int limit = src.mSpanCount;
        boolean hasNoCopySpan = false;

        for (int i = 0; i < limit; i++) {
            int spanStart = srcData[i * COLUMNS + START];
            int spanEnd = srcData[i * COLUMNS + END];
            if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue;
            if (srcSpans[i] instanceof NoCopySpan) {
                hasNoCopySpan = true;
                if (ignoreNoCopySpan) {
                    continue;
                }
            }
            count++;
        }

        if (count == 0) return;

            Object[] srcSpans = src.mSpans;
        if (!hasNoCopySpan && start == 0 && end == src.length()) {
            mSpans = ArrayUtils.newUnpaddedObjectArray(src.mSpans.length);
            mSpanData = new int[src.mSpanData.length];
            mSpanCount = src.mSpanCount;
            System.arraycopy(src.mSpans, 0, mSpans, 0, src.mSpans.length);
            System.arraycopy(src.mSpanData, 0, mSpanData, 0, mSpanData.length);
        } else {
            mSpanCount = count;
            mSpans = ArrayUtils.newUnpaddedObjectArray(mSpanCount);
            mSpanData = new int[mSpans.length * COLUMNS];
            for (int i = 0, j = 0; i < limit; i++) {
                int spanStart = srcData[i * COLUMNS + START];
                int spanEnd = srcData[i * COLUMNS + END];
                if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue;
                if (isOutOfCopyRange(start, end, spanStart, spanEnd)
                        || (ignoreNoCopySpan && srcSpans[i] instanceof NoCopySpan)) {
                    continue;
                }
                if (spanStart < start) spanStart = start;
                if (spanEnd > end) spanEnd = end;

@@ -494,6 +520,21 @@ import java.lang.reflect.Array;
        return hash;
    }

    /**
     * Following two unused methods are left since these are listed in hidden api list.
     *
     * Due to backward compatibility reasons, we copy even NoCopySpan by default
     */
    private void copySpans(Spanned src, int start, int end) {
        copySpans(src, start, end, false);
    }

    private void copySpans(SpannableStringInternal src, int start, int end) {
        copySpans(src, start, end, false);
    }



    private String mText;
    private Object[] mSpans;
    private int[] mSpanData;
+17 −2
Original line number Diff line number Diff line
@@ -26,12 +26,27 @@ public final class SpannedString
extends SpannableStringInternal
implements CharSequence, GetChars, Spanned
{
    /**
     * @param source source object to copy from
     * @param ignoreNoCopySpan whether to copy NoCopySpans in the {@code source}
     * @hide
     */
    public SpannedString(CharSequence source, boolean ignoreNoCopySpan) {
        super(source, 0, source.length(), ignoreNoCopySpan);
    }

    /**
     * For the backward compatibility reasons, this constructor copies all spans including {@link
     * android.text.NoCopySpan}.
     * @param source source text
     */
    public SpannedString(CharSequence source) {
        super(source, 0, source.length());
        this(source, false /* ignoreNoCopySpan */);  // preserve existing NoCopySpan behavior
    }

    private SpannedString(CharSequence source, int start, int end) {
        super(source, start, end);
        // preserve existing NoCopySpan behavior
        super(source, start, end, false /* ignoreNoCopySpan */);
    }

    public CharSequence subSequence(int start, int end) {
+163 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;

import android.annotation.NonNull;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.text.style.QuoteSpan;
import android.text.style.UnderlineSpan;

import org.junit.Test;
import org.junit.runner.RunWith;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class SpannableStringNoCopyTest {
    @Test
    public void testCopyConstructor_copyNoCopySpans_SpannableStringInternalImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // By default, copy NoCopySpans
        final SpannedString copied = new SpannedString(first);
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(3, spans.length);
    }

    @Test
    public void testCopyConstructor_doesNotCopyNoCopySpans_SpannableStringInternalImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // Do not copy NoCopySpan if specified so.
        final SpannedString copied = new SpannedString(first, false /* copyNoCopySpan */);
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(2, spans.length);

        for (int i = 0; i < spans.length; i++) {
            assertFalse(spans[i] instanceof NoCopySpan);
        }
    }

    @Test
    public void testCopyConstructor_copyNoCopySpans_OtherSpannableImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // By default, copy NoCopySpans
        final SpannedString copied = new SpannedString(new CustomSpannable(first));
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(3, spans.length);
    }

    @Test
    public void testCopyConstructor_doesNotCopyNoCopySpans_OtherSpannableImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // Do not copy NoCopySpan if specified so.
        final SpannedString copied = new SpannedString(
                new CustomSpannable(first), false /* copyNoCopySpan */);
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(2, spans.length);

        for (int i = 0; i < spans.length; i++) {
            assertFalse(spans[i] instanceof NoCopySpan);
        }
    }

    // A custom implementation of Spannable.
    private static class CustomSpannable implements Spannable {
        private final @NonNull Spannable mText;

        CustomSpannable(@NonNull Spannable text) {
            mText = text;
        }

        @Override
        public void setSpan(Object what, int start, int end, int flags) {
            mText.setSpan(what, start, end, flags);
        }

        @Override
        public void removeSpan(Object what) {
            mText.removeSpan(what);
        }

        @Override
        public <T> T[] getSpans(int start, int end, Class<T> type) {
            return mText.getSpans(start, end, type);
        }

        @Override
        public int getSpanStart(Object tag) {
            return mText.getSpanStart(tag);
        }

        @Override
        public int getSpanEnd(Object tag) {
            return mText.getSpanEnd(tag);
        }

        @Override
        public int getSpanFlags(Object tag) {
            return mText.getSpanFlags(tag);
        }

        @Override
        public int nextSpanTransition(int start, int limit, Class type) {
            return mText.nextSpanTransition(start, limit, type);
        }

        @Override
        public int length() {
            return mText.length();
        }

        @Override
        public char charAt(int index) {
            return mText.charAt(index);
        }

        @Override
        public CharSequence subSequence(int start, int end) {
            return mText.subSequence(start, end);
        }

        @Override
        public String toString() {
            return mText.toString();
        }
    };
}
+153 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;

import android.annotation.NonNull;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.text.style.QuoteSpan;
import android.text.style.UnderlineSpan;

import org.junit.Test;
import org.junit.runner.RunWith;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class SpannedStringNoCopyTest {
    @Test
    public void testCopyConstructor_copyNoCopySpans_SpannableStringInternalImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // By default, copy NoCopySpans
        final SpannedString copied = new SpannedString(first);
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(3, spans.length);
    }

    @Test
    public void testCopyConstructor_doesNotCopyNoCopySpans_SpannableStringInternalImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // Do not copy NoCopySpan if specified so.
        final SpannedString copied = new SpannedString(first, false /* copyNoCopySpan */);
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(2, spans.length);

        for (int i = 0; i < spans.length; i++) {
            assertFalse(spans[i] instanceof NoCopySpan);
        }
    }

    @Test
    public void testCopyConstructor_copyNoCopySpans_OtherSpannedImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // By default, copy NoCopySpans
        final SpannedString copied = new SpannedString(new CustomSpanned(first));
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(3, spans.length);
    }

    @Test
    public void testCopyConstructor_doesNotCopyNoCopySpans_OtherSpannedImpl() {
        final SpannableString first = new SpannableString("t\nest data");
        first.setSpan(new QuoteSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
        first.setSpan(new NoCopySpan.Concrete(), 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        first.setSpan(new UnderlineSpan(), 0, first.length(), Spanned.SPAN_PRIORITY);

        // Do not copy NoCopySpan if specified so.
        final SpannedString copied = new SpannedString(
                new CustomSpanned(first), false /* copyNoCopySpan */);
        final Object[] spans = copied.getSpans(0, copied.length(), Object.class);
        assertNotNull(spans);
        assertEquals(2, spans.length);

        for (int i = 0; i < spans.length; i++) {
            assertFalse(spans[i] instanceof NoCopySpan);
        }
    }

    // A custom implementation of Spanned
    private static class CustomSpanned implements Spanned {
        private final @NonNull Spanned mText;

        CustomSpanned(@NonNull Spannable text) {
            mText = text;
        }

        @Override
        public <T> T[] getSpans(int start, int end, Class<T> type) {
            return mText.getSpans(start, end, type);
        }

        @Override
        public int getSpanStart(Object tag) {
            return mText.getSpanStart(tag);
        }

        @Override
        public int getSpanEnd(Object tag) {
            return mText.getSpanEnd(tag);
        }

        @Override
        public int getSpanFlags(Object tag) {
            return mText.getSpanFlags(tag);
        }

        @Override
        public int nextSpanTransition(int start, int limit, Class type) {
            return mText.nextSpanTransition(start, limit, type);
        }

        @Override
        public int length() {
            return mText.length();
        }

        @Override
        public char charAt(int index) {
            return mText.charAt(index);
        }

        @Override
        public CharSequence subSequence(int start, int end) {
            return mText.subSequence(start, end);
        }

        @Override
        public String toString() {
            return mText.toString();
        }
    };
}