From 02f167c35ad81fce82c0ebc14a15892fb6981866 Mon Sep 17 00:00:00 2001 From: Roozbeh Pournader Date: Tue, 6 Jun 2017 11:31:33 -0700 Subject: [PATCH] Move tests with private APIs to coretests Tests have been moved from CTS tests, with some small modifications. Test: adb shell am instrument -w -e package android.text com.android.frameworks.coretests/android.support.test.runner.AndroidJUnitRunner Bug: 37780152 Change-Id: I87ce084276db0f1157260bbeb0c9009c5227bfcf --- .../src/android/text/BidiFormatterTest.java | 84 ++++ .../src/android/text/LayoutTest.java | 469 ++++++++++++++++++ .../text/SpannableStringBuilderTest.java | 37 ++ .../text/StaticLayoutLineBreakingTest.java | 461 +++++++++++++++++ .../src/android/text/StaticLayoutTest.java | 261 ++++++++++ .../android/text/format/DateUtilsTest.java | 42 +- .../android/text/method/WordIteratorTest.java | 118 ++++- .../android/text/style/UnderlineSpanTest.java | 65 +++ .../src/android/text/util/LinkifyTest.java | 53 +- 9 files changed, 1585 insertions(+), 5 deletions(-) create mode 100644 core/tests/coretests/src/android/text/BidiFormatterTest.java create mode 100644 core/tests/coretests/src/android/text/LayoutTest.java create mode 100644 core/tests/coretests/src/android/text/StaticLayoutLineBreakingTest.java create mode 100644 core/tests/coretests/src/android/text/style/UnderlineSpanTest.java diff --git a/core/tests/coretests/src/android/text/BidiFormatterTest.java b/core/tests/coretests/src/android/text/BidiFormatterTest.java new file mode 100644 index 0000000000000..14705add258fa --- /dev/null +++ b/core/tests/coretests/src/android/text/BidiFormatterTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2013 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 android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BidiFormatterTest { + private static final BidiFormatter LTR_FMT = BidiFormatter.getInstance(false /* LTR context */); + private static final BidiFormatter RTL_FMT = BidiFormatter.getInstance(true /* RTL context */); + + private static final String EN = "abba"; + private static final String HE = "\u05E0\u05E1"; + + private static final String LRM = "\u200E"; + private static final String RLM = "\u200F"; + + @Test + public void testMarkAfter() { + assertEquals("uniform dir matches LTR context", + "", LTR_FMT.markAfter(EN, TextDirectionHeuristics.LTR)); + assertEquals("uniform dir matches RTL context", + "", RTL_FMT.markAfter(HE, TextDirectionHeuristics.RTL)); + + assertEquals("exit dir opposite to LTR context", + LRM, LTR_FMT.markAfter(EN + HE, TextDirectionHeuristics.LTR)); + assertEquals("exit dir opposite to RTL context", + RLM, RTL_FMT.markAfter(HE + EN, TextDirectionHeuristics.RTL)); + + assertEquals("overall dir (but not exit dir) opposite to LTR context", + LRM, LTR_FMT.markAfter(HE + EN, TextDirectionHeuristics.RTL)); + assertEquals("overall dir (but not exit dir) opposite to RTL context", + RLM, RTL_FMT.markAfter(EN + HE, TextDirectionHeuristics.LTR)); + + assertEquals("exit dir neutral, overall dir matches LTR context", + "", LTR_FMT.markAfter(".", TextDirectionHeuristics.LTR)); + assertEquals("exit dir neutral, overall dir matches RTL context", + "", RTL_FMT.markAfter(".", TextDirectionHeuristics.RTL)); + } + + @Test + public void testMarkBefore() { + assertEquals("uniform dir matches LTR context", + "", LTR_FMT.markBefore(EN, TextDirectionHeuristics.LTR)); + assertEquals("uniform dir matches RTL context", + "", RTL_FMT.markBefore(HE, TextDirectionHeuristics.RTL)); + + assertEquals("entry dir opposite to LTR context", + LRM, LTR_FMT.markBefore(HE + EN, TextDirectionHeuristics.LTR)); + assertEquals("entry dir opposite to RTL context", + RLM, RTL_FMT.markBefore(EN + HE, TextDirectionHeuristics.RTL)); + + assertEquals("overall dir (but not entry dir) opposite to LTR context", + LRM, LTR_FMT.markBefore(EN + HE, TextDirectionHeuristics.RTL)); + assertEquals("overall dir (but not entry dir) opposite to RTL context", + RLM, RTL_FMT.markBefore(HE + EN, TextDirectionHeuristics.LTR)); + + assertEquals("exit dir neutral, overall dir matches LTR context", + "", LTR_FMT.markBefore(".", TextDirectionHeuristics.LTR)); + assertEquals("exit dir neutral, overall dir matches RTL context", + "", RTL_FMT.markBefore(".", TextDirectionHeuristics.RTL)); + } +} diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java new file mode 100644 index 0000000000000..6d610bbbf5e9c --- /dev/null +++ b/core/tests/coretests/src/android/text/LayoutTest.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2008 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.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.text.Layout.Alignment; +import android.text.style.StrikethroughSpan; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LayoutTest { + private static final int LINE_COUNT = 5; + private static final int LINE_HEIGHT = 12; + private static final int LINE_DESCENT = 4; + private static final CharSequence LAYOUT_TEXT = "alwei\t;sdfs\ndf @"; + + private SpannableString mSpannedText; + + private int mWidth; + private Layout.Alignment mAlign; + private float mSpacingMult; + private float mSpacingAdd; + private TextPaint mTextPaint; + + @Before + public void setup() { + mTextPaint = new TextPaint(); + mSpannedText = new SpannableString(LAYOUT_TEXT); + mSpannedText.setSpan(new StrikethroughSpan(), 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + mWidth = 11; + mAlign = Alignment.ALIGN_CENTER; + mSpacingMult = 1; + mSpacingAdd = 2; + } + + @Test + public void testConstructor() { + new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, mSpacingMult, mSpacingAdd); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorNull() { + new MockLayout(null, null, -1, null, 0, 0); + } + + @Test + public void testGetText() { + CharSequence text = "test case 1"; + Layout layout = new MockLayout(text, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(text, layout.getText()); + + layout = new MockLayout(null, mTextPaint, mWidth, mAlign, mSpacingMult, mSpacingAdd); + assertNull(layout.getText()); + } + + @Test + public void testGetPaint() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + + assertSame(mTextPaint, layout.getPaint()); + + layout = new MockLayout(LAYOUT_TEXT, null, mWidth, mAlign, mSpacingMult, mSpacingAdd); + assertNull(layout.getPaint()); + } + + @Test + public void testGetWidth() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 10, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(10, layout.getWidth()); + + layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 0, mAlign, mSpacingMult, mSpacingAdd); + assertEquals(0, layout.getWidth()); + } + + @Test + public void testGetEllipsizedWidth() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 15, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(15, layout.getEllipsizedWidth()); + + layout = new MockLayout(LAYOUT_TEXT, mTextPaint, 0, mAlign, mSpacingMult, mSpacingAdd); + assertEquals(0, layout.getEllipsizedWidth()); + } + + @Test + public void testIncreaseWidthTo() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + int oldWidth = layout.getWidth(); + + layout.increaseWidthTo(oldWidth); + assertEquals(oldWidth, layout.getWidth()); + + try { + layout.increaseWidthTo(oldWidth - 1); + fail("should throw runtime exception attempted to reduce Layout width"); + } catch (RuntimeException e) { + } + + layout.increaseWidthTo(oldWidth + 1); + assertEquals(oldWidth + 1, layout.getWidth()); + } + + @Test + public void testGetHeight() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(60, layout.getHeight()); + } + + @Test + public void testGetAlignment() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertSame(mAlign, layout.getAlignment()); + + layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, null, mSpacingMult, mSpacingAdd); + assertNull(layout.getAlignment()); + } + + @Test + public void testGetSpacingMultiplier() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, -1, mSpacingAdd); + assertEquals(-1.0f, layout.getSpacingMultiplier(), 0.0f); + + layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, 5, mSpacingAdd); + assertEquals(5.0f, layout.getSpacingMultiplier(), 0.0f); + } + + @Test + public void testGetSpacingAdd() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, mSpacingMult, -1); + assertEquals(-1.0f, layout.getSpacingAdd(), 0.0f); + + layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, mAlign, mSpacingMult, 20); + assertEquals(20.0f, layout.getSpacingAdd(), 0.0f); + } + + @Test + public void testGetLineBounds() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + Rect bounds = new Rect(); + + assertEquals(32, layout.getLineBounds(2, bounds)); + assertEquals(0, bounds.left); + assertEquals(mWidth, bounds.right); + assertEquals(24, bounds.top); + assertEquals(36, bounds.bottom); + } + + @Test + public void testGetLineForVertical() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(0, layout.getLineForVertical(-1)); + assertEquals(0, layout.getLineForVertical(0)); + assertEquals(0, layout.getLineForVertical(LINE_COUNT)); + assertEquals(LINE_COUNT - 1, layout.getLineForVertical(1000)); + } + + @Test + public void testGetLineForOffset() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(0, layout.getLineForOffset(-1)); + assertEquals(1, layout.getLineForOffset(1)); + assertEquals(LINE_COUNT - 1, layout.getLineForOffset(LINE_COUNT - 1)); + assertEquals(LINE_COUNT - 1, layout.getLineForOffset(1000)); + } + + @Test + public void testGetLineEnd() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(2, layout.getLineEnd(1)); + } + + @Test + public void testGetLineVisibleEnd() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + + assertEquals(2, layout.getLineVisibleEnd(1)); + assertEquals(LINE_COUNT, layout.getLineVisibleEnd(LINE_COUNT - 1)); + assertEquals(LAYOUT_TEXT.length(), layout.getLineVisibleEnd(LAYOUT_TEXT.length() - 1)); + try { + layout.getLineVisibleEnd(LAYOUT_TEXT.length()); + fail("should throw .StringIndexOutOfBoundsException here"); + } catch (StringIndexOutOfBoundsException e) { + } + } + + @Test + public void testGetLineBottom() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(LINE_HEIGHT, layout.getLineBottom(0)); + } + + @Test + public void testGetLineBaseline() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(8, layout.getLineBaseline(0)); + } + + @Test + public void testGetLineAscent() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(-8, layout.getLineAscent(0)); + } + + @Test + public void testGetParagraphAlignment() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertSame(mAlign, layout.getParagraphAlignment(0)); + + layout = new MockLayout(mSpannedText, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertSame(mAlign, layout.getParagraphAlignment(0)); + assertSame(mAlign, layout.getParagraphAlignment(1)); + } + + @Test + public void testGetParagraphLeft() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(0, layout.getParagraphLeft(0)); + } + + @Test + public void testGetParagraphRight() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertEquals(mWidth, layout.getParagraphRight(0)); + } + + @Test + public void testIsSpanned() { + MockLayout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + // default is not spanned text + assertFalse(layout.mockIsSpanned()); + + // try to create a spanned text + layout = new MockLayout(mSpannedText, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + assertTrue(layout.mockIsSpanned()); + } + + private static final class MockLayout extends Layout { + MockLayout(CharSequence text, TextPaint paint, int width, + Alignment align, float spacingmult, float spacingadd) { + super(text, paint, width, align, spacingmult, spacingadd); + } + + protected boolean mockIsSpanned() { + return super.isSpanned(); + } + + @Override + public int getBottomPadding() { + return 0; + } + + @Override + public int getEllipsisCount(int line) { + return 0; + } + + @Override + public int getEllipsisStart(int line) { + return 0; + } + + @Override + public boolean getLineContainsTab(int line) { + return false; + } + + @Override + public int getLineCount() { + return LINE_COUNT; + } + + @Override + public int getLineDescent(int line) { + return LINE_DESCENT; + } + + @Override + public Directions getLineDirections(int line) { + return Layout.DIRS_ALL_LEFT_TO_RIGHT; + } + + @Override + public int getLineStart(int line) { + if (line < 0) { + return 0; + } + return line; + } + + @Override + public int getLineTop(int line) { + if (line < 0) { + return 0; + } + return LINE_HEIGHT * (line); + } + + @Override + public int getParagraphDirection(int line) { + return 0; + } + + @Override + public int getTopPadding() { + return 0; + } + } + + @Test + public void testGetLineWidth() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + for (int i = 0; i < LINE_COUNT; i++) { + int start = layout.getLineStart(i); + int end = layout.getLineEnd(i); + String text = LAYOUT_TEXT.toString().substring(start, end); + assertEquals(mTextPaint.measureText(text), layout.getLineWidth(i), 1.0f); + } + } + + @Test + public void testGetCursorPath() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + Path path = new Path(); + final float epsilon = 1.0f; + for (int i = 0; i < LINE_COUNT; i++) { + layout.getCursorPath(i, path, LAYOUT_TEXT); + RectF bounds = new RectF(); + path.computeBounds(bounds, false); + assertTrue(bounds.top >= layout.getLineTop(i) - epsilon); + assertTrue(bounds.bottom <= layout.getLineBottom(i) + epsilon); + } + } + + @Test + public void testDraw() { + Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth, + mAlign, mSpacingMult, mSpacingAdd); + final int width = 256; + final int height = 256; + MockCanvas c = new MockCanvas(width, height); + layout.draw(c); + List drawCommands = c.getDrawCommands(); + assertEquals(LINE_COUNT, drawCommands.size()); + for (int i = 0; i < LINE_COUNT; i++) { + MockCanvas.DrawCommand drawCommand = drawCommands.get(i); + int start = layout.getLineStart(i); + int end = layout.getLineEnd(i); + assertEquals(LAYOUT_TEXT.toString().substring(start, end), drawCommand.text); + float expected_y = (i + 1) * LINE_HEIGHT - LINE_DESCENT; + assertEquals(expected_y, drawCommand.y, 0.0f); + } + } + + private final class MockCanvas extends Canvas { + + class DrawCommand { + public final String text; + public final float x; + public final float y; + + DrawCommand(String text, float x, float y) { + this.text = text; + this.x = x; + this.y = y; + } + } + + List mDrawCommands; + + MockCanvas(int width, int height) { + super(); + mDrawCommands = new ArrayList<>(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + setBitmap(bitmap); + } + + // Drawing text with either drawText or drawTextRun is valid; we don't care which. + // We also don't care which of the string representations is used. + + @Override + public void drawText(String text, int start, int end, float x, float y, Paint p) { + mDrawCommands.add(new DrawCommand(text.substring(start, end), x, y)); + } + + @Override + public void drawText(CharSequence text, int start, int end, float x, float y, Paint p) { + drawText(text.toString(), start, end, x, y, p); + } + + @Override + public void drawText(char[] text, int index, int count, float x, float y, Paint p) { + mDrawCommands.add(new DrawCommand(new String(text, index, count), x, y)); + } + + @Override + public void drawTextRun(CharSequence text, int start, int end, int contextStart, + int contextEnd, float x, float y, boolean isRtl, Paint paint) { + drawText(text, start, end, x, y, paint); + } + + @Override + public void drawTextRun(char[] text, int index, int count, int contextIndex, + int contextCount, float x, float y, boolean isRtl, Paint paint) { + drawText(text, index, count, x, y, paint); + } + + List getDrawCommands() { + return mDrawCommands; + } + } +} + diff --git a/core/tests/coretests/src/android/text/SpannableStringBuilderTest.java b/core/tests/coretests/src/android/text/SpannableStringBuilderTest.java index 1f1a689758319..04a486eb7cda3 100644 --- a/core/tests/coretests/src/android/text/SpannableStringBuilderTest.java +++ b/core/tests/coretests/src/android/text/SpannableStringBuilderTest.java @@ -16,9 +16,46 @@ package android.text; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.text.style.BulletSpan; +import android.text.style.QuoteSpan; +import android.text.style.SubscriptSpan; +import android.text.style.UnderlineSpan; + +import org.junit.Test; + public class SpannableStringBuilderTest extends SpannableTest { protected Spannable newSpannableWithText(String text) { return new SpannableStringBuilder(text); } + + @Test + public void testGetSpans_sortsByPriorityEvenWhenSortParamIsFalse() { + String text = "p_in_s"; + SpannableStringBuilder builder = new SpannableStringBuilder(text); + Object first = new SubscriptSpan(); + Object second = new UnderlineSpan(); + Object third = new BulletSpan(); + Object fourth = new QuoteSpan(); + + builder.setSpan(first, 2, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(second, 1, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(third, 2, text.length(), 1 << Spanned.SPAN_PRIORITY_SHIFT); + builder.setSpan(fourth, 0, text.length(), 2 << Spanned.SPAN_PRIORITY_SHIFT); + + Object[] spans = builder.getSpans(0, text.length(), Object.class, false); + + assertNotNull(spans); + assertEquals(4, spans.length); + // priority spans are first + assertEquals(fourth, spans[0]); + assertEquals(third, spans[1]); + // other spans should be there + assertEquals(second, spans[2]); + assertEquals(first, spans[3]); + } } diff --git a/core/tests/coretests/src/android/text/StaticLayoutLineBreakingTest.java b/core/tests/coretests/src/android/text/StaticLayoutLineBreakingTest.java new file mode 100644 index 0000000000000..2ee855ef957d5 --- /dev/null +++ b/core/tests/coretests/src/android/text/StaticLayoutLineBreakingTest.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2012 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 android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.text.Layout.Alignment; +import android.text.style.MetricAffectingSpan; +import android.util.Log; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StaticLayoutLineBreakingTest { + // Span test are currently not supported because text measurement uses the MeasuredText + // internal mWorkPaint instead of the provided MockTestPaint. + private static final boolean SPAN_TESTS_SUPPORTED = false; + private static final boolean DEBUG = false; + + private static final float SPACE_MULTI = 1.0f; + private static final float SPACE_ADD = 0.0f; + private static final int WIDTH = 100; + private static final Alignment ALIGN = Alignment.ALIGN_LEFT; + + private static final char SURR_FIRST = '\uD800'; + private static final char SURR_SECOND = '\uDF31'; + + private static final int[] NO_BREAK = new int[] {}; + + private static final TextPaint sTextPaint = new MockTextPaint(); + + private static class MockTextPaint extends TextPaint { + + @Override + public float getTextRunAdvances(char[] chars, int index, int count, + int contextIndex, int contextCount, boolean isRtl, float[] advances, + int advancesIndex) { + + // Conditions copy pasted from Paint + if (chars == null) { + throw new IllegalArgumentException("text cannot be null"); + } + + if ((index | count | contextIndex | contextCount | advancesIndex + | (index - contextIndex) | (contextCount - count) + | ((contextIndex + contextCount) - (index + count)) + | (chars.length - (contextIndex + contextCount)) + | (advances == null ? 0 : + (advances.length - (advancesIndex + count)))) < 0) { + throw new IndexOutOfBoundsException(); + } + + float res = 0.0f; + + if (advances != null) { + for (int i = 0; i < count; i++) { + float width = getCharWidth(chars[index + i]); + advances[advancesIndex + i] = width; + res += width; + } + } + + return res; + } + } + + private static float getCharWidth(char c) { + switch (Character.toUpperCase(c)) { + // Roman figures + case 'I': return 1.0f; + case 'V': return 5.0f; + case 'X': return 10.0f; + case 'L': return 50.0f; + case 'C': return 100.0f; // equals to WIDTH + case ' ': return 10.0f; + case '_': return 0.0f; // 0-width character + case SURR_FIRST: return 7.0f; + case SURR_SECOND: return 3.0f; // Sum of SURR_FIRST-SURR_SECOND is 10 + default: return 10.0f; + } + } + + private static StaticLayout getStaticLayout(CharSequence source, int width) { + return new StaticLayout(source, sTextPaint, width, ALIGN, SPACE_MULTI, SPACE_ADD, false); + } + + private static int[] getBreaks(CharSequence source) { + return getBreaks(source, WIDTH); + } + + private static int[] getBreaks(CharSequence source, int width) { + StaticLayout staticLayout = getStaticLayout(source, width); + + int[] breaks = new int[staticLayout.getLineCount() - 1]; + for (int line = 0; line < breaks.length; line++) { + breaks[line] = staticLayout.getLineEnd(line); + } + return breaks; + } + + private static void debugLayout(CharSequence source, StaticLayout staticLayout) { + if (DEBUG) { + int count = staticLayout.getLineCount(); + Log.i("SLLBTest", "\"" + source.toString() + "\": " + + count + " lines"); + for (int line = 0; line < count; line++) { + int lineStart = staticLayout.getLineStart(line); + int lineEnd = staticLayout.getLineEnd(line); + Log.i("SLLBTest", "Line " + line + " [" + lineStart + ".." + + lineEnd + "]\t" + source.subSequence(lineStart, lineEnd)); + } + } + } + + private static void layout(CharSequence source, int[] breaks) { + layout(source, breaks, WIDTH); + } + + private static void layout(CharSequence source, int[] breaks, int width) { + StaticLayout staticLayout = getStaticLayout(source, width); + + debugLayout(source, staticLayout); + + int lineCount = breaks.length + 1; + assertEquals("Number of lines", lineCount, staticLayout.getLineCount()); + + for (int line = 0; line < lineCount; line++) { + int lineStart = staticLayout.getLineStart(line); + int lineEnd = staticLayout.getLineEnd(line); + + if (line == 0) { + assertEquals("Line start for first line", 0, lineStart); + } else { + assertEquals("Line start for line " + line, breaks[line - 1], lineStart); + } + + if (line == lineCount - 1) { + assertEquals("Line end for last line", source.length(), lineEnd); + } else { + assertEquals("Line end for line " + line, breaks[line], lineEnd); + } + } + } + + private static void layoutMaxLines(CharSequence source, int[] breaks, int maxLines) { + StaticLayout staticLayout = new StaticLayout(source, 0, source.length(), sTextPaint, WIDTH, + ALIGN, TextDirectionHeuristics.LTR, SPACE_MULTI, SPACE_ADD, false /* includePad */, + null, WIDTH, maxLines); + + debugLayout(source, staticLayout); + + final int lineCount = staticLayout.getLineCount(); + + for (int line = 0; line < lineCount; line++) { + int lineStart = staticLayout.getLineStart(line); + int lineEnd = staticLayout.getLineEnd(line); + + if (line == 0) { + assertEquals("Line start for first line", 0, lineStart); + } else { + assertEquals("Line start for line " + line, breaks[line - 1], lineStart); + } + + if (line == lineCount - 1 && line != breaks.length - 1) { + assertEquals("Line end for last line", source.length(), lineEnd); + } else { + assertEquals("Line end for line " + line, breaks[line], lineEnd); + } + } + } + + private static final int MAX_SPAN_COUNT = 10; + private static final int[] sSpanStarts = new int[MAX_SPAN_COUNT]; + private static final int[] sSpanEnds = new int[MAX_SPAN_COUNT]; + + private static MetricAffectingSpan getMetricAffectingSpan() { + return new MetricAffectingSpan() { + @Override + public void updateDrawState(TextPaint tp) { /* empty */ } + + @Override + public void updateMeasureState(TextPaint p) { /* empty */ } + }; + } + + /** + * Replaces the "<...>" blocks by spans, assuming non overlapping, correctly defined spans + * @param text + * @return A CharSequence with '<' '>' replaced by MetricAffectingSpan + */ + private static CharSequence spanify(String text) { + int startIndex = text.indexOf('<'); + if (startIndex < 0) return text; + + int spanCount = 0; + do { + int endIndex = text.indexOf('>'); + if (endIndex < 0) throw new IllegalArgumentException("Unbalanced span markers"); + + text = text.substring(0, startIndex) + text.substring(startIndex + 1, endIndex) + + text.substring(endIndex + 1); + + sSpanStarts[spanCount] = startIndex; + sSpanEnds[spanCount] = endIndex - 2; + spanCount++; + + startIndex = text.indexOf('<'); + } while (startIndex >= 0); + + SpannableStringBuilder result = new SpannableStringBuilder(text); + for (int i = 0; i < spanCount; i++) { + result.setSpan(getMetricAffectingSpan(), sSpanStarts[i], sSpanEnds[i], + Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + return result; + } + + @Test + public void testNoLineBreak() { + // Width lower than WIDTH + layout("", NO_BREAK); + layout("I", NO_BREAK); + layout("V", NO_BREAK); + layout("X", NO_BREAK); + layout("L", NO_BREAK); + layout("I VILI", NO_BREAK); + layout("XXXX", NO_BREAK); + layout("LXXXX", NO_BREAK); + + // Width equal to WIDTH + layout("C", NO_BREAK); + layout("LL", NO_BREAK); + layout("L XXXX", NO_BREAK); + layout("XXXXXXXXXX", NO_BREAK); + layout("XXX XXXXXX", NO_BREAK); + layout("XXX XXXX X", NO_BREAK); + layout("XXX XXXXX ", NO_BREAK); + layout(" XXXXXXXX ", NO_BREAK); + layout(" XX XXX ", NO_BREAK); + // 0123456789 + + // Width greater than WIDTH, but no break + layout(" XX XXX ", NO_BREAK); + layout("XX XXX XXX ", NO_BREAK); + layout("XX XXX XXX ", NO_BREAK); + layout("XXXXXXXXXX ", NO_BREAK); + // 01234567890 + } + + @Test + public void testOneLineBreak() { + // 01234567890 + layout("XX XXX XXXX", new int[] {7}); + layout("XX XXXX XXX", new int[] {8}); + layout("XX XXXXX XX", new int[] {9}); + layout("XX XXXXXX X", new int[] {10}); + // 01234567890 + layout("XXXXXXXXXXX", new int[] {10}); + layout("XXXXXXXXX X", new int[] {10}); + layout("XXXXXXXX XX", new int[] {9}); + layout("XXXXXXX XXX", new int[] {8}); + layout("XXXXXX XXXX", new int[] {7}); + // 01234567890 + layout("LL LL", new int[] {3}); + layout("LLLL", new int[] {2}); + layout("C C", new int[] {2}); + layout("CC", new int[] {1}); + } + + @Test + public void testSpaceAtBreak() { + // 0123456789012 + layout("XXXX XXXXX X", new int[] {11}); + layout("XXXXXXXXXX X", new int[] {11}); + layout("XXXXXXXXXV X", new int[] {11}); + layout("C X", new int[] {2}); + } + + @Test + public void testMultipleSpacesAtBreak() { + // 0123456789012 + layout("LXX XXXX", new int[] {4}); + layout("LXX XXXX", new int[] {5}); + layout("LXX XXXX", new int[] {6}); + layout("LXX XXXX", new int[] {7}); + layout("LXX XXXX", new int[] {8}); + } + + @Test + public void testZeroWidthCharacters() { + // 0123456789012345678901234 + layout("X_X_X_X_X_X_X_X_X_X", NO_BREAK); + layout("___X_X_X_X_X_X_X_X_X_X___", NO_BREAK); + layout("C_X", new int[] {2}); + layout("C__X", new int[] {3}); + } + + /** + * Note that when the text has spans, StaticLayout does not use the provided TextPaint to + * measure text runs anymore. This is probably a bug. + * To be able to use the fake sTextPaint and make this test pass, use mPaint instead of + * mWorkPaint in MeasuredText#addStyleRun + */ + @Test + public void testWithSpans() { + if (!SPAN_TESTS_SUPPORTED) return; + + layout(spanify("<012 456 89>"), NO_BREAK); + layout(spanify("012 <456> 89"), NO_BREAK); + layout(spanify("<012> <456>< 89>"), NO_BREAK); + layout(spanify("<012> <456> <89>"), NO_BREAK); + + layout(spanify("<012> <456> <89>012"), new int[] {8}); + layout(spanify("<012> <456> 89<012>"), new int[] {8}); + layout(spanify("<012> <456> <89><012>"), new int[] {8}); + layout(spanify("<012> <456> 89 <123>"), new int[] {11}); + layout(spanify("<012> <456> 89< 123>"), new int[] {11}); + layout(spanify("<012> <456> <89> <123>"), new int[] {11}); + layout(spanify("012 456 89 XX XX"), new int[] {11, 18}); + } + + /* + * Adding a span to the string should not change the layout, since the metrics are unchanged. + */ + @Test + public void testWithOneSpan() { + if (!SPAN_TESTS_SUPPORTED) return; + + String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012", + "012 456 89012 456 89012", "0123456789012" }; + + MetricAffectingSpan metricAffectingSpan = getMetricAffectingSpan(); + + for (String text : texts) { + // Get the line breaks without any span + int[] breaks = getBreaks(text); + + // Add spans on all possible offsets + for (int spanStart = 0; spanStart < text.length(); spanStart++) { + for (int spanEnd = spanStart; spanEnd < text.length(); spanEnd++) { + SpannableStringBuilder ssb = new SpannableStringBuilder(text); + ssb.setSpan(metricAffectingSpan, spanStart, spanEnd, + Spanned.SPAN_INCLUSIVE_INCLUSIVE); + layout(ssb, breaks); + } + } + } + } + + @Test + public void testWithTwoSpans() { + if (!SPAN_TESTS_SUPPORTED) return; + + String[] texts = new String[] { "0123", "012 456", "012 456 89 123", "012 45678 012", + "012 456 89012 456 89012", "0123456789012" }; + + MetricAffectingSpan metricAffectingSpan1 = getMetricAffectingSpan(); + MetricAffectingSpan metricAffectingSpan2 = getMetricAffectingSpan(); + + for (String text : texts) { + // Get the line breaks without any span + int[] breaks = getBreaks(text); + + // Add spans on all possible offsets + for (int spanStart1 = 0; spanStart1 < text.length(); spanStart1++) { + for (int spanEnd1 = spanStart1; spanEnd1 < text.length(); spanEnd1++) { + SpannableStringBuilder ssb = new SpannableStringBuilder(text); + ssb.setSpan(metricAffectingSpan1, spanStart1, spanEnd1, + Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + for (int spanStart2 = 0; spanStart2 < text.length(); spanStart2++) { + for (int spanEnd2 = spanStart2; spanEnd2 < text.length(); spanEnd2++) { + ssb.setSpan(metricAffectingSpan2, spanStart2, spanEnd2, + Spanned.SPAN_INCLUSIVE_INCLUSIVE); + layout(ssb, breaks); + } + } + } + } + } + } + + public static String replace(String string, char c, char r) { + return string.replaceAll(String.valueOf(c), String.valueOf(r)); + } + + @Test + public void testWithSurrogate() { + layout("LX" + SURR_FIRST + SURR_SECOND, NO_BREAK); + layout("LXXXX" + SURR_FIRST + SURR_SECOND, NO_BREAK); + // LXXXXI (91) + SURR_FIRST (7) fits. But we should not break the surrogate pair + // Bug: surrogate pair is broken, should be 6 (breaking after the 'V') + // Maybe not: may be ok if the second character of a pair always has a 0-width + layout("LXXXXI" + SURR_FIRST + SURR_SECOND, new int[] {7}); + + // LXXXXI (95) + SURR_SECOND (3) fits, but this is not a valid surrogate pair, breaking it + layout("LXXXXV" + SURR_SECOND + SURR_FIRST, new int[] {7}); + + layout("C" + SURR_FIRST + SURR_SECOND, new int[] {1}); + } + + @Test + public void testNarrowWidth() { + int[] widths = new int[] { 0, 4, 10 }; + String[] texts = new String[] { "", "X", " ", "XX", " X", "XXX" }; + + for (String text: texts) { + // 15 is such that only one character will fit + int[] breaks = getBreaks(text, 15); + + // Width under 15 should all lead to the same line break + for (int width: widths) { + layout(text, breaks, width); + } + } + } + + @Test + public void testNarrowWidthZeroWidth() { + int[] widths = new int[] { 1, 4 }; + for (int width: widths) { + layout("X.", new int[] {1}, width); + layout("X__", NO_BREAK, width); + layout("X__X", new int[] {3}, width); + layout("X__X_", new int[] {3}, width); + + layout("_", NO_BREAK, width); + layout("__", NO_BREAK, width); + layout("_X", new int[] {1}, width); + layout("_X_", new int[] {1}, width); + layout("__X__", new int[] {2}, width); + } + } + + @Test + public void testMaxLines() { + layoutMaxLines("C", NO_BREAK, 1); + layoutMaxLines("C C", new int[] {2}, 1); + layoutMaxLines("C C", new int[] {2}, 2); + layoutMaxLines("CC", new int[] {1}, 1); + layoutMaxLines("CC", new int[] {1}, 2); + } +} diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java index b7ca21963f099..74a6cc6299373 100644 --- a/core/tests/coretests/src/android/text/StaticLayoutTest.java +++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java @@ -17,7 +17,9 @@ package android.text; import static android.text.Layout.Alignment.ALIGN_NORMAL; + import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import android.graphics.Paint.FontMetricsInt; import android.support.test.filters.SmallTest; @@ -26,15 +28,68 @@ import android.text.Layout.Alignment; import android.text.method.EditorState; import android.util.Log; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.List; + /** * Tests StaticLayout vertical metrics behavior. */ @SmallTest @RunWith(AndroidJUnit4.class) public class StaticLayoutTest { + private static final float SPACE_MULTI = 1.0f; + private static final float SPACE_ADD = 0.0f; + private static final int DEFAULT_OUTER_WIDTH = 150; + + private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar" + + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong"; + private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence"; + + private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER; + private static final int ELLIPSIZE_WIDTH = 8; + + private StaticLayout mDefaultLayout; + private TextPaint mDefaultPaint; + + @Before + public void setup() { + mDefaultPaint = new TextPaint(); + mDefaultLayout = createDefaultStaticLayout(); + } + + private StaticLayout createDefaultStaticLayout() { + return new StaticLayout(LAYOUT_TEXT, mDefaultPaint, + DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true); + } + + @Test + public void testBuilder() { + { + // Obtain. + final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, + LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); + final StaticLayout layout = builder.build(); + // Check default value. + assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR, + layout.getTextDirectionHeuristic()); + } + { + // setTextDirection. + final StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0, + LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH); + builder.setTextDirection(TextDirectionHeuristics.RTL); + final StaticLayout layout = builder.build(); + // Always returns TextDirectionHeuristics.FIRSTSTRONG_LTR. + assertEquals(TextDirectionHeuristics.FIRSTSTRONG_LTR, + layout.getTextDirectionHeuristic()); + } + } + /** * Basic test showing expected behavior and relationship between font * metrics and line metrics. @@ -410,4 +465,210 @@ public class StaticLayoutTest { state.setByString("| U+261D U+1F3FB U+261D U+1F3FB U+261D U+1F3FB"); moveCursorToLeftCursorableOffset(state, paint); } + + private StaticLayout createEllipsizeStaticLayout(CharSequence text, + TextUtils.TruncateAt ellipsize, int maxLines) { + return new StaticLayout(text, 0, text.length(), + mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, + TextDirectionHeuristics.FIRSTSTRONG_LTR, + SPACE_MULTI, SPACE_ADD, true /* include pad */, + ellipsize, + ELLIPSIZE_WIDTH, + maxLines); + } + + @Test + public void testEllipsis_singleLine() { + { + // Single line case and TruncateAt.END so that we have some ellipsis + StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, + TextUtils.TruncateAt.END, 1); + assertTrue(layout.getEllipsisCount(0) > 0); + } + { + // Single line case and TruncateAt.MIDDLE so that we have some ellipsis + StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, + TextUtils.TruncateAt.MIDDLE, 1); + assertTrue(layout.getEllipsisCount(0) > 0); + } + { + // Single line case and TruncateAt.END so that we have some ellipsis + StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, + TextUtils.TruncateAt.END, 1); + assertTrue(layout.getEllipsisCount(0) > 0); + } + { + // Single line case and TruncateAt.MARQUEE so that we have NO ellipsis + StaticLayout layout = createEllipsizeStaticLayout(LAYOUT_TEXT_SINGLE_LINE, + TextUtils.TruncateAt.MARQUEE, 1); + assertTrue(layout.getEllipsisCount(0) == 0); + } + { + final String text = "\u3042" // HIRAGANA LETTER A + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; + final float textWidth = mDefaultPaint.measureText(text); + final int halfWidth = (int) (textWidth / 2.0f); + { + StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, + halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, + SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1); + assertTrue(layout.getEllipsisCount(0) > 0); + assertTrue(layout.getEllipsisStart(0) > 0); + } + { + StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, + halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, + SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.START, halfWidth, 1); + assertTrue(layout.getEllipsisCount(0) > 0); + assertEquals(0, mDefaultLayout.getEllipsisStart(0)); + } + { + StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, + halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, + SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MIDDLE, halfWidth, 1); + assertTrue(layout.getEllipsisCount(0) > 0); + assertTrue(layout.getEllipsisStart(0) > 0); + } + { + StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, + halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, + SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.MARQUEE, halfWidth, 1); + assertEquals(0, layout.getEllipsisCount(0)); + } + } + + { + // The white spaces in this text will be trailing if maxLines is larger than 1, but + // width of the trailing white spaces must not be ignored if ellipsis is applied. + final String text = "abc def"; + final float textWidth = mDefaultPaint.measureText(text); + final int halfWidth = (int) (textWidth / 2.0f); + { + StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, + halfWidth, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, + SPACE_MULTI, SPACE_ADD, false, TextUtils.TruncateAt.END, halfWidth, 1); + assertTrue(layout.getEllipsisCount(0) > 0); + assertTrue(layout.getEllipsisStart(0) > 0); + } + } + + { + // 2 family emojis (11 code units + 11 code units). + final String text = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66" + + "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; + final float textWidth = mDefaultPaint.measureText(text); + + final TextUtils.TruncateAt[] kinds = {TextUtils.TruncateAt.START, + TextUtils.TruncateAt.MIDDLE, TextUtils.TruncateAt.END}; + for (final TextUtils.TruncateAt kind : kinds) { + for (int i = 0; i <= 8; i++) { + int avail = (int) (textWidth * i / 7.0f); + StaticLayout layout = new StaticLayout(text, 0, text.length(), mDefaultPaint, + avail, DEFAULT_ALIGN, TextDirectionHeuristics.FIRSTSTRONG_LTR, + SPACE_MULTI, SPACE_ADD, false, kind, avail, 1); + + assertTrue(layout.getEllipsisCount(0) == text.length() + || layout.getEllipsisCount(0) == text.length() / 2 + || layout.getEllipsisCount(0) == 0); + } + } + } + } + + // String wrapper for testing not well known implementation of CharSequence. + private class FakeCharSequence implements CharSequence { + private String mStr; + + FakeCharSequence(String str) { + mStr = str; + } + + @Override + public char charAt(int index) { + return mStr.charAt(index); + } + + @Override + public int length() { + return mStr.length(); + } + + @Override + public CharSequence subSequence(int start, int end) { + return mStr.subSequence(start, end); + } + + @Override + public String toString() { + return mStr; + } + }; + + private List buildTestCharSequences(String testString, Normalizer.Form[] forms) { + List result = new ArrayList<>(); + + List normalizedStrings = new ArrayList<>(); + for (Normalizer.Form form: forms) { + normalizedStrings.add(Normalizer.normalize(testString, form)); + } + + for (String str: normalizedStrings) { + result.add(str); + result.add(new SpannedString(str)); + result.add(new SpannableString(str)); + result.add(new SpannableStringBuilder(str)); // as a GraphicsOperations implementation. + result.add(new FakeCharSequence(str)); // as a not well known implementation. + } + return result; + } + + private String buildTestMessage(CharSequence seq) { + String normalized; + if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) { + normalized = "NFC"; + } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) { + normalized = "NFD"; + } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) { + normalized = "NFKC"; + } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) { + normalized = "NFKD"; + } else { + throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD"); + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < seq.length(); ++i) { + builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i)))); + } + + return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]" + + ", class: " + seq.getClass().getName() + + ", Normalization: " + normalized; + } + + @Test + public void testGetOffset_UNICODE_Hebrew() { + String testString = "\u05DE\u05E1\u05E2\u05D3\u05D4"; // Hebrew Characters + for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { + StaticLayout layout = new StaticLayout(seq, mDefaultPaint, + DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, + TextDirectionHeuristics.RTL, SPACE_MULTI, SPACE_ADD, true); + + String testLabel = buildTestMessage(seq); + + assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0)); + assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1)); + assertEquals(testLabel, 3, layout.getOffsetToLeftOf(2)); + assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3)); + assertEquals(testLabel, 5, layout.getOffsetToLeftOf(4)); + assertEquals(testLabel, 5, layout.getOffsetToLeftOf(5)); + + assertEquals(testLabel, 0, layout.getOffsetToRightOf(0)); + assertEquals(testLabel, 0, layout.getOffsetToRightOf(1)); + assertEquals(testLabel, 1, layout.getOffsetToRightOf(2)); + assertEquals(testLabel, 2, layout.getOffsetToRightOf(3)); + assertEquals(testLabel, 3, layout.getOffsetToRightOf(4)); + assertEquals(testLabel, 4, layout.getOffsetToRightOf(5)); + } + } } diff --git a/core/tests/coretests/src/android/text/format/DateUtilsTest.java b/core/tests/coretests/src/android/text/format/DateUtilsTest.java index 9271cb44e903f..6063e1a6921d7 100644 --- a/core/tests/coretests/src/android/text/format/DateUtilsTest.java +++ b/core/tests/coretests/src/android/text/format/DateUtilsTest.java @@ -24,12 +24,16 @@ import android.os.LocaleList; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; -import java.util.Locale; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + @SmallTest @RunWith(AndroidJUnit4.class) public class DateUtilsTest { @@ -103,6 +107,42 @@ public class DateUtilsTest { assertEquals("48h", DateUtils.formatDuration(172800000, DateUtils.LENGTH_SHORTEST)); } + @Test + public void testFormatSameDayTime() { + // This test assumes a default DateFormat.is24Hour setting. + DateFormat.is24Hour = null; + Date date = new Date(109, 0, 19, 3, 30, 15); + long fixedTime = date.getTime(); + + int currentYear = Calendar.getInstance().get(Calendar.YEAR); + Date dateWithCurrentYear = new Date(currentYear - 1900, 0, 19, 3, 30, 15); + + final long dayDuration = 5 * 24 * 60 * 60 * 1000; + assertEquals("Saturday, January 24, 2009", DateUtils.formatSameDayTime( + fixedTime + dayDuration, fixedTime, java.text.DateFormat.FULL, + java.text.DateFormat.FULL)); + assertEquals("Jan 24, 2009", DateUtils.formatSameDayTime(fixedTime + dayDuration, + fixedTime, java.text.DateFormat.DEFAULT, java.text.DateFormat.FULL)); + assertEquals("January 24, 2009", DateUtils.formatSameDayTime(fixedTime + dayDuration, + fixedTime, java.text.DateFormat.LONG, java.text.DateFormat.FULL)); + assertEquals("Jan 24, 2009", DateUtils.formatSameDayTime(fixedTime + dayDuration, + fixedTime, java.text.DateFormat.MEDIUM, java.text.DateFormat.FULL)); + assertEquals("1/24/09", DateUtils.formatSameDayTime(fixedTime + dayDuration, + fixedTime, java.text.DateFormat.SHORT, java.text.DateFormat.FULL)); + + final long hourDuration = 2 * 60 * 60 * 1000; + assertEquals("5:30:15 AM GMT+00:00", DateUtils.formatSameDayTime(fixedTime + hourDuration, + fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.FULL)); + assertEquals("5:30:15 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration, + fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.DEFAULT)); + assertEquals("5:30:15 AM GMT+00:00", DateUtils.formatSameDayTime(fixedTime + hourDuration, + fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.LONG)); + assertEquals("5:30:15 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration, + fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.MEDIUM)); + assertEquals("5:30 AM", DateUtils.formatSameDayTime(fixedTime + hourDuration, + fixedTime, java.text.DateFormat.FULL, java.text.DateFormat.SHORT)); + } + private void setLocales(LocaleList locales) { final Resources systemResources = Resources.getSystem(); final Configuration config = new Configuration(systemResources.getConfiguration()); diff --git a/core/tests/coretests/src/android/text/method/WordIteratorTest.java b/core/tests/coretests/src/android/text/method/WordIteratorTest.java index 3499a7483d844..d1f4f7e84c255 100644 --- a/core/tests/coretests/src/android/text/method/WordIteratorTest.java +++ b/core/tests/coretests/src/android/text/method/WordIteratorTest.java @@ -21,19 +21,131 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.support.test.runner.AndroidJUnit4; import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; import java.text.BreakIterator; import java.util.Locale; -import org.junit.Test; -import org.junit.runner.RunWith; // TODO(Bug: 24062099): Add more tests for non-ascii text. @SmallTest @RunWith(AndroidJUnit4.class) public class WordIteratorTest { + private WordIterator mWordIterator = new WordIterator(); + + private void verifyIsWordWithSurrogate(int beginning, int end, int surrogateIndex) { + for (int i = beginning; i <= end; i++) { + if (i == surrogateIndex) continue; + assertEquals(beginning, mWordIterator.getBeginning(i)); + assertEquals(end, mWordIterator.getEnd(i)); + } + } + + private void setCharSequence(String string) { + mWordIterator.setCharSequence(string, 0, string.length()); + } + + private void verifyIsWord(int beginning, int end) { + verifyIsWordWithSurrogate(beginning, end, -1); + } + + private void verifyIsNotWord(int beginning, int end) { + for (int i = beginning; i <= end; i++) { + assertEquals(BreakIterator.DONE, mWordIterator.getBeginning(i)); + assertEquals(BreakIterator.DONE, mWordIterator.getEnd(i)); + } + } + + @Test + public void testEmptyString() { + setCharSequence(""); + assertEquals(BreakIterator.DONE, mWordIterator.following(0)); + assertEquals(BreakIterator.DONE, mWordIterator.preceding(0)); + + assertEquals(BreakIterator.DONE, mWordIterator.getBeginning(0)); + assertEquals(BreakIterator.DONE, mWordIterator.getEnd(0)); + } + + @Test + public void testOneWord() { + setCharSequence("I"); + verifyIsWord(0, 1); + + setCharSequence("am"); + verifyIsWord(0, 2); + + setCharSequence("zen"); + verifyIsWord(0, 3); + } + + @Test + public void testSpacesOnly() { + setCharSequence(" "); + verifyIsNotWord(0, 1); + + setCharSequence(", "); + verifyIsNotWord(0, 2); + + setCharSequence(":-)"); + verifyIsNotWord(0, 3); + } + + @Test + public void testBeginningEnd() { + setCharSequence("Well hello, there! "); + // 0123456789012345678901 + verifyIsWord(0, 4); + verifyIsWord(5, 10); + verifyIsNotWord(11, 13); + verifyIsWord(14, 19); + verifyIsNotWord(20, 21); + + setCharSequence(" Another - sentence"); + // 012345678901234567890 + verifyIsNotWord(0, 1); + verifyIsWord(2, 9); + verifyIsNotWord(10, 11); + verifyIsWord(12, 20); + + setCharSequence("This is \u0644\u0627 tested"); // Lama-aleph + // 012345678 9 01234567 + verifyIsWord(0, 4); + verifyIsWord(5, 7); + verifyIsWord(8, 10); + verifyIsWord(11, 17); + } + + @Test + public void testSurrogate() { + final String gothicBairkan = "\uD800\uDF31"; + + setCharSequence("one we" + gothicBairkan + "ird word"); + // 012345 67 890123456 + + verifyIsWord(0, 3); + // Skip index 7 (there is no point in starting between the two surrogate characters) + verifyIsWordWithSurrogate(4, 11, 7); + verifyIsWord(12, 16); + + setCharSequence("one " + gothicBairkan + "xxx word"); + // 0123 45 678901234 + + verifyIsWord(0, 3); + verifyIsWordWithSurrogate(4, 9, 5); + verifyIsWord(10, 14); + + setCharSequence("one xxx" + gothicBairkan + " word"); + // 0123456 78 901234 + + verifyIsWord(0, 3); + verifyIsWordWithSurrogate(4, 9, 8); + verifyIsWord(10, 14); + } + @Test public void testSetCharSequence() { final String text = "text"; diff --git a/core/tests/coretests/src/android/text/style/UnderlineSpanTest.java b/core/tests/coretests/src/android/text/style/UnderlineSpanTest.java new file mode 100644 index 0000000000000..e5c4b821c1cf0 --- /dev/null +++ b/core/tests/coretests/src/android/text/style/UnderlineSpanTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2017 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.style; + + +import static org.junit.Assert.assertEquals; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.StaticLayout; +import android.text.TextPaint; + +import org.junit.Test; +import org.junit.runner.RunWith; + + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class UnderlineSpanTest { + private class RedUnderlineSpan extends UnderlineSpan { + @Override + public void updateDrawState(TextPaint ds) { + ds.setUnderlineText(android.graphics.Color.RED, 1.0f); + } + } + + // Identical to the normal UnderlineSpan test, except that a subclass of UnderlineSpan is used + // that draws a red underline. This shouldn't affect width either. + @Test + public void testDoesntAffectWidth_colorUnderlineSubclass() { + // Roboto kerns between "P" and "." + final SpannableString text = new SpannableString("P."); + final float origLineWidth = textWidth(text); + // Underline just the "P". + text.setSpan(new RedUnderlineSpan(), 0, 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + final float underlinedLineWidth = textWidth(text); + assertEquals(origLineWidth, underlinedLineWidth, 0.0f); + } + + // Measures the width of some potentially-spanned text, assuming it's not too wide. + private float textWidth(CharSequence text) { + final TextPaint tp = new TextPaint(); + tp.setTextSize(100.0f); // Large enough so that the difference in kerning is visible. + final int largeWidth = 10000; // Enough width so the whole text fits in one line. + final StaticLayout layout = StaticLayout.Builder.obtain( + text, 0, text.length(), tp, largeWidth).build(); + return layout.getLineWidth(0); + } +} diff --git a/core/tests/coretests/src/android/text/util/LinkifyTest.java b/core/tests/coretests/src/android/text/util/LinkifyTest.java index 23c34085c34e2..73ff046f0f835 100644 --- a/core/tests/coretests/src/android/text/util/LinkifyTest.java +++ b/core/tests/coretests/src/android/text/util/LinkifyTest.java @@ -16,6 +16,7 @@ package android.text.util; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -25,15 +26,20 @@ import android.os.LocaleList; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import android.text.Spannable; +import android.text.SpannableString; import android.text.method.LinkMovementMethod; +import android.text.style.URLSpan; +import android.util.Patterns; import android.widget.TextView; -import java.util.Locale; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Locale; + /** * LinkifyTest tests {@link Linkify}. */ @@ -98,4 +104,49 @@ public class LinkifyTest { overrideConfig.setLocales(LOCALE_LIST_US); return mContext.createConfigurationContext(overrideConfig); } + + @Test + public void testAddLinks_addsLinksWhenDefaultSchemeIsNull() { + Spannable spannable = new SpannableString("any https://android.com any android.com any"); + Linkify.addLinks(spannable, Patterns.AUTOLINK_WEB_URL, null, null, null); + + URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); + assertEquals("android.com and https://android.com should be linkified", 2, spans.length); + assertEquals("https://android.com", spans[0].getURL()); + assertEquals("android.com", spans[1].getURL()); + } + + @Test + public void testAddLinks_addsLinksWhenSchemesArrayIsNull() { + Spannable spannable = new SpannableString("any https://android.com any android.com any"); + Linkify.addLinks(spannable, Patterns.AUTOLINK_WEB_URL, "http://", null, null); + + URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); + assertEquals("android.com and https://android.com should be linkified", 2, spans.length); + // expected behavior, passing null schemes array means: prepend defaultScheme to all links. + assertEquals("http://https://android.com", spans[0].getURL()); + assertEquals("http://android.com", spans[1].getURL()); + } + + @Test + public void testAddLinks_prependsDefaultSchemeToBeginingOfLink() { + Spannable spannable = new SpannableString("any android.com any"); + Linkify.addLinks(spannable, Patterns.AUTOLINK_WEB_URL, "http://", + new String[] { "http://", "https://"}, null, null); + + URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); + assertEquals("android.com should be linkified", 1, spans.length); + assertEquals("http://android.com", spans[0].getURL()); + } + + @Test + public void testAddLinks_doesNotPrependSchemeIfSchemeExists() { + Spannable spannable = new SpannableString("any https://android.com any"); + Linkify.addLinks(spannable, Patterns.AUTOLINK_WEB_URL, "http://", + new String[] { "http://", "https://"}, null, null); + + URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); + assertEquals("android.com should be linkified", 1, spans.length); + assertEquals("https://android.com", spans[0].getURL()); + } }