diff --git a/core/java/android/text/SpannableString.java b/core/java/android/text/SpannableString.java index 56d0946babf23..afb5df809bc03 100644 --- a/core/java/android/text/SpannableString.java +++ b/core/java/android/text/SpannableString.java @@ -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) { diff --git a/core/java/android/text/SpannableStringInternal.java b/core/java/android/text/SpannableStringInternal.java index 366ec145ffc44..5dd1a52b4a7a0 100644 --- a/core/java/android/text/SpannableStringInternal.java +++ b/core/java/android/text/SpannableStringInternal.java @@ -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()) { + private void copySpans(SpannableStringInternal src, int start, int end, + boolean ignoreNoCopySpan) { + int count = 0; + 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; + + 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 { - int count = 0; - int[] srcData = src.mSpanData; - int limit = src.mSpanCount; - 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; - count++; - } - - if (count == 0) return; - - Object[] srcSpans = src.mSpans; 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; diff --git a/core/java/android/text/SpannedString.java b/core/java/android/text/SpannedString.java index afed221f4152f..acee3c5f1a41a 100644 --- a/core/java/android/text/SpannedString.java +++ b/core/java/android/text/SpannedString.java @@ -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) { diff --git a/core/tests/coretests/src/android/text/SpannableStringNoCopyTest.java b/core/tests/coretests/src/android/text/SpannableStringNoCopyTest.java new file mode 100644 index 0000000000000..c205f9695bb84 --- /dev/null +++ b/core/tests/coretests/src/android/text/SpannableStringNoCopyTest.java @@ -0,0 +1,163 @@ +/* + * 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[] getSpans(int start, int end, Class 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(); + } + }; +} diff --git a/core/tests/coretests/src/android/text/SpannedStringNoCopyTest.java b/core/tests/coretests/src/android/text/SpannedStringNoCopyTest.java new file mode 100644 index 0000000000000..068092484b1b8 --- /dev/null +++ b/core/tests/coretests/src/android/text/SpannedStringNoCopyTest.java @@ -0,0 +1,153 @@ +/* + * 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[] getSpans(int start, int end, Class 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(); + } + }; +}