Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.text.cts;
     18 
     19 import static org.junit.Assert.assertEquals;
     20 import static org.junit.Assert.assertFalse;
     21 import static org.junit.Assert.assertNotNull;
     22 import static org.junit.Assert.assertTrue;
     23 import static org.junit.Assert.fail;
     24 import static org.mockito.Matchers.anyInt;
     25 import static org.mockito.Mockito.any;
     26 import static org.mockito.Mockito.mock;
     27 import static org.mockito.Mockito.when;
     28 
     29 import android.content.Context;
     30 import android.graphics.Bitmap;
     31 import android.graphics.Canvas;
     32 import android.graphics.Paint;
     33 import android.graphics.Paint.FontMetricsInt;
     34 import android.graphics.Typeface;
     35 import android.os.LocaleList;
     36 import android.text.Editable;
     37 import android.text.Layout;
     38 import android.text.Layout.Alignment;
     39 import android.text.PrecomputedText;
     40 import android.text.SpannableString;
     41 import android.text.SpannableStringBuilder;
     42 import android.text.Spanned;
     43 import android.text.SpannedString;
     44 import android.text.StaticLayout;
     45 import android.text.TextDirectionHeuristic;
     46 import android.text.TextDirectionHeuristics;
     47 import android.text.TextPaint;
     48 import android.text.TextUtils;
     49 import android.text.TextUtils.TruncateAt;
     50 import android.text.method.cts.EditorState;
     51 import android.text.style.LineBackgroundSpan;
     52 import android.text.style.LineHeightSpan;
     53 import android.text.style.ReplacementSpan;
     54 import android.text.style.StyleSpan;
     55 import android.text.style.TextAppearanceSpan;
     56 
     57 import androidx.test.InstrumentationRegistry;
     58 import androidx.test.filters.SmallTest;
     59 import androidx.test.runner.AndroidJUnit4;
     60 
     61 import org.junit.Before;
     62 import org.junit.Test;
     63 import org.junit.runner.RunWith;
     64 import org.mockito.ArgumentCaptor;
     65 
     66 import java.text.Normalizer;
     67 import java.util.ArrayList;
     68 import java.util.List;
     69 import java.util.Locale;
     70 
     71 @SmallTest
     72 @RunWith(AndroidJUnit4.class)
     73 public class StaticLayoutTest {
     74     private static final float SPACE_MULTI = 1.0f;
     75     private static final float SPACE_ADD = 0.0f;
     76     private static final int DEFAULT_OUTER_WIDTH = 150;
     77 
     78     private static final int LAST_LINE = 5;
     79     private static final int LINE_COUNT = 6;
     80     private static final int LARGER_THAN_LINE_COUNT  = 50;
     81 
     82     private static final String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing "
     83             + "elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad "
     84             + "minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
     85             + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse "
     86             + "cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non "
     87             + "proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
     88 
     89     /* the first line must have one tab. the others not. totally 6 lines
     90      */
     91     private static final CharSequence LAYOUT_TEXT = "CharSe\tq\nChar"
     92             + "Sequence\nCharSequence\nHelllo\n, world\nLongLongLong";
     93 
     94     private static final CharSequence LAYOUT_TEXT_SINGLE_LINE = "CharSequence";
     95 
     96     private static final int VERTICAL_BELOW_TEXT = 1000;
     97 
     98     private static final Alignment DEFAULT_ALIGN = Alignment.ALIGN_CENTER;
     99 
    100     private static final int ELLIPSIZE_WIDTH = 8;
    101 
    102     private StaticLayout mDefaultLayout;
    103     private TextPaint mDefaultPaint;
    104 
    105     private static class TestingTextPaint extends TextPaint {
    106         // need to have a subclass to ensure measurement happens in Java and not C++
    107     }
    108 
    109     @Before
    110     public void setup() {
    111         mDefaultPaint = new TextPaint();
    112         mDefaultLayout = createDefaultStaticLayout();
    113     }
    114 
    115     private StaticLayout createDefaultStaticLayout() {
    116         return new StaticLayout(LAYOUT_TEXT, mDefaultPaint,
    117                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    118     }
    119 
    120     private StaticLayout createEllipsizeStaticLayout() {
    121         return new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint,
    122                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true,
    123                 TextUtils.TruncateAt.MIDDLE, ELLIPSIZE_WIDTH);
    124     }
    125 
    126     private StaticLayout createEllipsizeStaticLayout(CharSequence text,
    127             TextUtils.TruncateAt ellipsize) {
    128         return new StaticLayout(text, 0, text.length(),
    129                 mDefaultPaint, DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN,
    130                 SPACE_MULTI, SPACE_ADD, true /* include pad */,
    131                 ellipsize,
    132                 ELLIPSIZE_WIDTH);
    133     }
    134 
    135     /**
    136      * Constructor test
    137      */
    138     @Test
    139     public void testConstructor() {
    140         new StaticLayout(LAYOUT_TEXT, mDefaultPaint, DEFAULT_OUTER_WIDTH,
    141                 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    142 
    143         new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint,
    144                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    145 
    146         new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(), mDefaultPaint,
    147                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, false, null, 0);
    148     }
    149 
    150     @Test(expected=NullPointerException.class)
    151     public void testConstructorNull() {
    152         new StaticLayout(null, null, -1, null, 0, 0, true);
    153     }
    154 
    155     @Test
    156     public void testBuilder() {
    157         {
    158             // Obtain.
    159             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    160                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    161             StaticLayout layout = builder.build();
    162             // Check values passed to obtain().
    163             assertEquals(LAYOUT_TEXT, layout.getText());
    164             assertEquals(mDefaultPaint, layout.getPaint());
    165             assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth());
    166             // Check default values.
    167             assertEquals(Alignment.ALIGN_NORMAL, layout.getAlignment());
    168             assertEquals(0.0f, layout.getSpacingAdd(), 0.0f);
    169             assertEquals(1.0f, layout.getSpacingMultiplier(), 0.0f);
    170             assertEquals(DEFAULT_OUTER_WIDTH, layout.getEllipsizedWidth());
    171         }
    172         {
    173             // Obtain with null objects.
    174             StaticLayout.Builder builder = StaticLayout.Builder.obtain(null, 0, 0, null, 0);
    175             try {
    176                 StaticLayout layout = builder.build();
    177                 fail("should throw NullPointerException here");
    178             } catch (NullPointerException e) {
    179             }
    180         }
    181         {
    182             // setText.
    183             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    184                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    185             builder.setText(LAYOUT_TEXT_SINGLE_LINE);
    186             StaticLayout layout = builder.build();
    187             assertEquals(LAYOUT_TEXT_SINGLE_LINE, layout.getText());
    188         }
    189         {
    190             // setAlignment.
    191             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    192                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    193             builder.setAlignment(DEFAULT_ALIGN);
    194             StaticLayout layout = builder.build();
    195             assertEquals(DEFAULT_ALIGN, layout.getAlignment());
    196         }
    197         {
    198             // setLineSpacing.
    199             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    200                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    201             builder.setLineSpacing(1.0f, 2.0f);
    202             StaticLayout layout = builder.build();
    203             assertEquals(1.0f, layout.getSpacingAdd(), 0.0f);
    204             assertEquals(2.0f, layout.getSpacingMultiplier(), 0.0f);
    205         }
    206         {
    207             // setEllipsizedWidth and setEllipsize.
    208             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    209                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    210             builder.setEllipsize(TruncateAt.END);
    211             builder.setEllipsizedWidth(ELLIPSIZE_WIDTH);
    212             StaticLayout layout = builder.build();
    213             assertEquals(ELLIPSIZE_WIDTH, layout.getEllipsizedWidth());
    214             assertEquals(DEFAULT_OUTER_WIDTH, layout.getWidth());
    215             assertTrue(layout.getEllipsisCount(0) == 0);
    216             assertTrue(layout.getEllipsisCount(5) > 0);
    217         }
    218         {
    219             // setMaxLines.
    220             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    221                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    222             builder.setMaxLines(1);
    223             builder.setEllipsize(TruncateAt.END);
    224             StaticLayout layout = builder.build();
    225             assertTrue(layout.getEllipsisCount(0) > 0);
    226             assertEquals(1, layout.getLineCount());
    227         }
    228         {
    229             // Setter methods that cannot be directly tested.
    230             // setBreakStrategy, setHyphenationFrequency, setIncludePad, and setIndents.
    231             StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    232                     LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    233             builder.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
    234             builder.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL);
    235             builder.setIncludePad(true);
    236             builder.setIndents(null, null);
    237             StaticLayout layout = builder.build();
    238             assertNotNull(layout);
    239         }
    240     }
    241 
    242     @Test
    243     public void testSetLineSpacing_whereLineEndsWithNextLine() {
    244         final float spacingAdd = 10f;
    245         final float spacingMult = 3f;
    246 
    247         // two lines of text, with line spacing, first line will have the spacing, but last line
    248         // wont have the spacing
    249         final String tmpText = "a\nb";
    250         StaticLayout.Builder builder = StaticLayout.Builder.obtain(tmpText, 0, tmpText.length(),
    251                 mDefaultPaint, DEFAULT_OUTER_WIDTH);
    252         builder.setLineSpacing(spacingAdd, spacingMult).setIncludePad(false);
    253         final StaticLayout comparisonLayout = builder.build();
    254 
    255         assertEquals(2, comparisonLayout.getLineCount());
    256         final int heightWithLineSpacing = comparisonLayout.getLineBottom(0)
    257                 - comparisonLayout.getLineTop(0);
    258         final int heightWithoutLineSpacing = comparisonLayout.getLineBottom(1)
    259                 - comparisonLayout.getLineTop(1);
    260         assertTrue(heightWithLineSpacing > heightWithoutLineSpacing);
    261 
    262         final String text = "a\n";
    263         // build the layout to be tested
    264         builder = StaticLayout.Builder.obtain("a\n", 0, text.length(), mDefaultPaint,
    265                 DEFAULT_OUTER_WIDTH);
    266         builder.setLineSpacing(spacingAdd, spacingMult).setIncludePad(false);
    267         final StaticLayout layout = builder.build();
    268 
    269         assertEquals(comparisonLayout.getLineCount(), layout.getLineCount());
    270         assertEquals(heightWithLineSpacing, layout.getLineBottom(0) - layout.getLineTop(0));
    271         assertEquals(heightWithoutLineSpacing, layout.getLineBottom(1) - layout.getLineTop(1));
    272     }
    273 
    274     @Test
    275     public void testBuilder_setJustificationMode() {
    276         StaticLayout.Builder builder = StaticLayout.Builder.obtain(LAYOUT_TEXT, 0,
    277                 LAYOUT_TEXT.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH);
    278         builder.setJustificationMode(Layout.JUSTIFICATION_MODE_INTER_WORD);
    279         StaticLayout layout = builder.build();
    280         // Hard to expect the justification result. Just make sure the final layout is created
    281         // without causing any exceptions.
    282         assertNotNull(layout);
    283     }
    284 
    285     /*
    286      * Get the line number corresponding to the specified vertical position.
    287      *  If you ask for a position above 0, you get 0. above 0 means pixel above the fire line
    288      *  if you ask for a position in the range of the height, return the pixel in line
    289      *  if you ask for a position below the bottom of the text, you get the last line.
    290      *  Test 4 values containing -1, 0, normal number and > count
    291      */
    292     @Test
    293     public void testGetLineForVertical() {
    294         assertEquals(0, mDefaultLayout.getLineForVertical(-1));
    295         assertEquals(0, mDefaultLayout.getLineForVertical(0));
    296         assertTrue(mDefaultLayout.getLineForVertical(50) > 0);
    297         assertEquals(LAST_LINE, mDefaultLayout.getLineForVertical(VERTICAL_BELOW_TEXT));
    298     }
    299 
    300     /**
    301      * Return the number of lines of text in this layout.
    302      */
    303     @Test
    304     public void testGetLineCount() {
    305         assertEquals(LINE_COUNT, mDefaultLayout.getLineCount());
    306     }
    307 
    308     /*
    309      * Return the vertical position of the top of the specified line.
    310      * If the specified line is one beyond the last line, returns the bottom of the last line.
    311      * A line of text contains top and bottom in height. this method just get the top of a line
    312      * Test 4 values containing -1, 0, normal number and > count
    313      */
    314     @Test
    315     public void testGetLineTop() {
    316         assertTrue(mDefaultLayout.getLineTop(0) >= 0);
    317         assertTrue(mDefaultLayout.getLineTop(1) > mDefaultLayout.getLineTop(0));
    318     }
    319 
    320     @Test(expected=ArrayIndexOutOfBoundsException.class)
    321     public void testGetLineTopBeforeFirst() {
    322         mDefaultLayout.getLineTop(-1);
    323     }
    324 
    325     @Test(expected=ArrayIndexOutOfBoundsException.class)
    326     public void testGetLineTopAfterLast() {
    327         mDefaultLayout.getLineTop(LARGER_THAN_LINE_COUNT );
    328     }
    329 
    330     /**
    331      * Return the descent of the specified line.
    332      * This method just like getLineTop, descent means the bottom pixel of the line
    333      * Test 4 values containing -1, 0, normal number and > count
    334      */
    335     @Test
    336     public void testGetLineDescent() {
    337         assertTrue(mDefaultLayout.getLineDescent(0) > 0);
    338         assertTrue(mDefaultLayout.getLineDescent(1) > 0);
    339     }
    340 
    341     @Test(expected=ArrayIndexOutOfBoundsException.class)
    342     public void testGetLineDescentBeforeFirst() {
    343         mDefaultLayout.getLineDescent(-1);
    344     }
    345 
    346     @Test(expected=ArrayIndexOutOfBoundsException.class)
    347     public void testGetLineDescentAfterLast() {
    348         mDefaultLayout.getLineDescent(LARGER_THAN_LINE_COUNT );
    349     }
    350 
    351     /**
    352      * Returns the primary directionality of the paragraph containing the specified line.
    353      * By default, each line should be same
    354      */
    355     @Test
    356     public void testGetParagraphDirection() {
    357         assertEquals(mDefaultLayout.getParagraphDirection(0),
    358                 mDefaultLayout.getParagraphDirection(1));
    359     }
    360 
    361     @Test(expected=ArrayIndexOutOfBoundsException.class)
    362     public void testGetParagraphDirectionBeforeFirst() {
    363         mDefaultLayout.getParagraphDirection(-1);
    364     }
    365 
    366     @Test(expected=ArrayIndexOutOfBoundsException.class)
    367     public void testGetParagraphDirectionAfterLast() {
    368         mDefaultLayout.getParagraphDirection(LARGER_THAN_LINE_COUNT );
    369     }
    370 
    371     /**
    372      * Return the text offset of the beginning of the specified line.
    373      * If the specified line is one beyond the last line, returns the end of the last line.
    374      * Test 4 values containing -1, 0, normal number and > count
    375      * Each line's offset must >= 0
    376      */
    377     @Test
    378     public void testGetLineStart() {
    379         assertTrue(mDefaultLayout.getLineStart(0) >= 0);
    380         assertTrue(mDefaultLayout.getLineStart(1) >= 0);
    381     }
    382 
    383     @Test(expected=ArrayIndexOutOfBoundsException.class)
    384     public void testGetLineStartBeforeFirst() {
    385         mDefaultLayout.getLineStart(-1);
    386     }
    387 
    388     @Test(expected=ArrayIndexOutOfBoundsException.class)
    389     public void testGetLineStartAfterLast() {
    390         mDefaultLayout.getLineStart(LARGER_THAN_LINE_COUNT );
    391     }
    392 
    393     /*
    394      * Returns whether the specified line contains one or more tabs.
    395      */
    396     @Test
    397     public void testGetContainsTab() {
    398         assertTrue(mDefaultLayout.getLineContainsTab(0));
    399         assertFalse(mDefaultLayout.getLineContainsTab(1));
    400     }
    401 
    402     @Test(expected=ArrayIndexOutOfBoundsException.class)
    403     public void testGetContainsTabBeforeFirst() {
    404         mDefaultLayout.getLineContainsTab(-1);
    405     }
    406 
    407     @Test(expected=ArrayIndexOutOfBoundsException.class)
    408     public void testGetContainsTabAfterLast() {
    409         mDefaultLayout.getLineContainsTab(LARGER_THAN_LINE_COUNT );
    410     }
    411 
    412     /**
    413      * Returns an array of directionalities for the specified line.
    414      * The array alternates counts of characters in left-to-right
    415      * and right-to-left segments of the line.
    416      * We can not check the return value, for Directions's field is package private
    417      * So only check it not null
    418      */
    419     @Test
    420     public void testGetLineDirections(){
    421         assertNotNull(mDefaultLayout.getLineDirections(0));
    422         assertNotNull(mDefaultLayout.getLineDirections(1));
    423     }
    424 
    425     @Test(expected = ArrayIndexOutOfBoundsException.class)
    426     public void testGetLineDirectionsBeforeFirst() {
    427         mDefaultLayout.getLineDirections(-1);
    428     }
    429 
    430     @Test(expected = ArrayIndexOutOfBoundsException.class)
    431     public void testGetLineDirectionsAfterLast() {
    432         mDefaultLayout.getLineDirections(LARGER_THAN_LINE_COUNT);
    433     }
    434 
    435     /**
    436      * Returns the (negative) number of extra pixels of ascent padding
    437      * in the top line of the Layout.
    438      */
    439     @Test
    440     public void testGetTopPadding() {
    441         assertTrue(mDefaultLayout.getTopPadding() < 0);
    442     }
    443 
    444     /**
    445      * Returns the number of extra pixels of descent padding in the bottom line of the Layout.
    446      */
    447     @Test
    448     public void testGetBottomPadding() {
    449         assertTrue(mDefaultLayout.getBottomPadding() > 0);
    450     }
    451 
    452     /*
    453      * Returns the number of characters to be ellipsized away, or 0 if no ellipsis is to take place.
    454      * So each line must >= 0
    455      */
    456     @Test
    457     public void testGetEllipsisCount() {
    458         // Multilines (6 lines) and TruncateAt.START so no ellipsis at all
    459         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
    460                 TextUtils.TruncateAt.MIDDLE);
    461 
    462         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
    463         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
    464         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
    465         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
    466         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
    467         assertTrue(mDefaultLayout.getEllipsisCount(5) == 0);
    468 
    469         try {
    470             mDefaultLayout.getEllipsisCount(-1);
    471             fail("should throw ArrayIndexOutOfBoundsException");
    472         } catch (ArrayIndexOutOfBoundsException e) {
    473         }
    474 
    475         try {
    476             mDefaultLayout.getEllipsisCount(LARGER_THAN_LINE_COUNT);
    477             fail("should throw ArrayIndexOutOfBoundsException");
    478         } catch (ArrayIndexOutOfBoundsException e) {
    479         }
    480 
    481         // Multilines (6 lines) and TruncateAt.MIDDLE so no ellipsis at all
    482         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
    483                 TextUtils.TruncateAt.MIDDLE);
    484 
    485         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
    486         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
    487         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
    488         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
    489         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
    490         assertTrue(mDefaultLayout.getEllipsisCount(5) == 0);
    491 
    492         // Multilines (6 lines) and TruncateAt.END so ellipsis only on the last line
    493         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
    494                 TextUtils.TruncateAt.END);
    495 
    496         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
    497         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
    498         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
    499         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
    500         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
    501         assertTrue(mDefaultLayout.getEllipsisCount(5) > 0);
    502 
    503         // Multilines (6 lines) and TruncateAt.MARQUEE so ellipsis only on the last line
    504         mDefaultLayout = createEllipsizeStaticLayout(LAYOUT_TEXT,
    505                 TextUtils.TruncateAt.END);
    506 
    507         assertTrue(mDefaultLayout.getEllipsisCount(0) == 0);
    508         assertTrue(mDefaultLayout.getEllipsisCount(1) == 0);
    509         assertTrue(mDefaultLayout.getEllipsisCount(2) == 0);
    510         assertTrue(mDefaultLayout.getEllipsisCount(3) == 0);
    511         assertTrue(mDefaultLayout.getEllipsisCount(4) == 0);
    512         assertTrue(mDefaultLayout.getEllipsisCount(5) > 0);
    513     }
    514 
    515     /*
    516      * Return the offset of the first character to be ellipsized away
    517      * relative to the start of the line.
    518      * (So 0 if the beginning of the line is ellipsized, not getLineStart().)
    519      */
    520     @Test
    521     public void testGetEllipsisStart() {
    522         mDefaultLayout = createEllipsizeStaticLayout();
    523         assertTrue(mDefaultLayout.getEllipsisStart(0) >= 0);
    524         assertTrue(mDefaultLayout.getEllipsisStart(1) >= 0);
    525 
    526         try {
    527             mDefaultLayout.getEllipsisStart(-1);
    528             fail("should throw ArrayIndexOutOfBoundsException");
    529         } catch (ArrayIndexOutOfBoundsException e) {
    530         }
    531 
    532         try {
    533             mDefaultLayout.getEllipsisStart(LARGER_THAN_LINE_COUNT);
    534             fail("should throw ArrayIndexOutOfBoundsException");
    535         } catch (ArrayIndexOutOfBoundsException e) {
    536         }
    537     }
    538 
    539     /*
    540      * Return the width to which this Layout is ellipsizing
    541      * or getWidth() if it is not doing anything special.
    542      * The constructor's Argument TextUtils.TruncateAt defines which EllipsizedWidth to use
    543      * ellipsizedWidth if argument is not null
    544      * outerWidth if argument is null
    545      */
    546     @Test
    547     public void testGetEllipsizedWidth() {
    548         int ellipsizedWidth = 60;
    549         int outerWidth = 100;
    550         StaticLayout layout = new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(),
    551                 mDefaultPaint, outerWidth, DEFAULT_ALIGN, SPACE_MULTI,
    552                 SPACE_ADD, false, TextUtils.TruncateAt.END, ellipsizedWidth);
    553         assertEquals(ellipsizedWidth, layout.getEllipsizedWidth());
    554 
    555         layout = new StaticLayout(LAYOUT_TEXT, 0, LAYOUT_TEXT.length(),
    556                 mDefaultPaint, outerWidth, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD,
    557                 false, null, ellipsizedWidth);
    558         assertEquals(outerWidth, layout.getEllipsizedWidth());
    559     }
    560 
    561     /**
    562      * scenario description:
    563      * 1. set the text.
    564      * 2. change the text
    565      * 3. Check the text won't change to the StaticLayout
    566     */
    567     @Test
    568     public void testImmutableStaticLayout() {
    569         Editable editable =  Editable.Factory.getInstance().newEditable("123\t\n555");
    570         StaticLayout layout = new StaticLayout(editable, mDefaultPaint,
    571                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    572 
    573         assertEquals(2, layout.getLineCount());
    574         assertTrue(mDefaultLayout.getLineContainsTab(0));
    575 
    576         // change the text
    577         editable.delete(0, editable.length() - 1);
    578 
    579         assertEquals(2, layout.getLineCount());
    580         assertTrue(layout.getLineContainsTab(0));
    581 
    582     }
    583 
    584     // String wrapper for testing not well known implementation of CharSequence.
    585     private class FakeCharSequence implements CharSequence {
    586         private String mStr;
    587 
    588         public FakeCharSequence(String str) {
    589             mStr = str;
    590         }
    591 
    592         @Override
    593         public char charAt(int index) {
    594             return mStr.charAt(index);
    595         }
    596 
    597         @Override
    598         public int length() {
    599             return mStr.length();
    600         }
    601 
    602         @Override
    603         public CharSequence subSequence(int start, int end) {
    604             return mStr.subSequence(start, end);
    605         }
    606 
    607         @Override
    608         public String toString() {
    609             return mStr;
    610         }
    611     };
    612 
    613     private List<CharSequence> buildTestCharSequences(String testString, Normalizer.Form[] forms) {
    614         List<CharSequence> result = new ArrayList<>();
    615 
    616         List<String> normalizedStrings = new ArrayList<>();
    617         for (Normalizer.Form form: forms) {
    618             normalizedStrings.add(Normalizer.normalize(testString, form));
    619         }
    620 
    621         for (String str: normalizedStrings) {
    622             result.add(str);
    623             result.add(new SpannedString(str));
    624             result.add(new SpannableString(str));
    625             result.add(new SpannableStringBuilder(str));  // as a GraphicsOperations implementation.
    626             result.add(new FakeCharSequence(str));  // as a not well known implementation.
    627         }
    628         return result;
    629     }
    630 
    631     private String buildTestMessage(CharSequence seq) {
    632         String normalized;
    633         if (Normalizer.isNormalized(seq, Normalizer.Form.NFC)) {
    634             normalized = "NFC";
    635         } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFD)) {
    636             normalized = "NFD";
    637         } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKC)) {
    638             normalized = "NFKC";
    639         } else if (Normalizer.isNormalized(seq, Normalizer.Form.NFKD)) {
    640             normalized = "NFKD";
    641         } else {
    642             throw new IllegalStateException("Normalized form is not NFC/NFD/NFKC/NFKD");
    643         }
    644 
    645         StringBuilder builder = new StringBuilder();
    646         for (int i = 0; i < seq.length(); ++i) {
    647             builder.append(String.format("0x%04X ", Integer.valueOf(seq.charAt(i))));
    648         }
    649 
    650         return "testString: \"" + seq.toString() + "\"[" + builder.toString() + "]" +
    651                 ", class: " + seq.getClass().getName() +
    652                 ", Normalization: " + normalized;
    653     }
    654 
    655     @Test
    656     public void testGetOffset_ASCII() {
    657         String testStrings[] = { "abcde", "ab\ncd", "ab\tcd", "ab\n\nc", "ab\n\tc" };
    658 
    659         for (String testString: testStrings) {
    660             for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
    661                 StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    662                         DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    663 
    664                 String testLabel = buildTestMessage(seq);
    665 
    666                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    667                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    668                 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
    669                 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
    670                 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
    671                 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
    672 
    673                 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
    674                 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    675                 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
    676                 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
    677                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
    678                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
    679             }
    680         }
    681 
    682         String testString = "ab\r\nde";
    683         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
    684             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    685                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    686 
    687             String testLabel = buildTestMessage(seq);
    688 
    689             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    690             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    691             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
    692             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
    693             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
    694             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
    695             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
    696 
    697             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
    698             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    699             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
    700             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
    701             assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
    702             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
    703             assertEquals(testLabel, 6, layout.getOffsetToRightOf(6));
    704         }
    705     }
    706 
    707     @Test
    708     public void testGetOffset_UNICODE() {
    709         String testStrings[] = new String[] {
    710               // Cyrillic alphabets.
    711               "\u0410\u0411\u0412\u0413\u0414",
    712               // Japanese Hiragana Characters.
    713               "\u3042\u3044\u3046\u3048\u304A",
    714         };
    715 
    716         for (String testString: testStrings) {
    717             for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
    718                 StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    719                         DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    720 
    721                 String testLabel = buildTestMessage(seq);
    722 
    723                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    724                 assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    725                 assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
    726                 assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
    727                 assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
    728                 assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
    729 
    730                 assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
    731                 assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    732                 assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
    733                 assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
    734                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
    735                 assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
    736             }
    737         }
    738     }
    739 
    740     @Test
    741     public void testGetOffset_UNICODE_Normalization() {
    742         // "A" with acute, circumflex, tilde, diaeresis, ring above.
    743         String testString = "\u00C1\u00C2\u00C3\u00C4\u00C5";
    744         Normalizer.Form[] oneUnicodeForms = { Normalizer.Form.NFC, Normalizer.Form.NFKC };
    745         for (CharSequence seq: buildTestCharSequences(testString, oneUnicodeForms)) {
    746             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    747                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    748 
    749             String testLabel = buildTestMessage(seq);
    750 
    751             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    752             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    753             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
    754             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
    755             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
    756             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
    757 
    758             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
    759             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    760             assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
    761             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
    762             assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
    763             assertEquals(testLabel, 5, layout.getOffsetToRightOf(5));
    764         }
    765 
    766         Normalizer.Form[] twoUnicodeForms = { Normalizer.Form.NFD, Normalizer.Form.NFKD };
    767         for (CharSequence seq: buildTestCharSequences(testString, twoUnicodeForms)) {
    768             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    769                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    770 
    771             String testLabel = buildTestMessage(seq);
    772 
    773             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    774             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    775             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2));
    776             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
    777             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
    778             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
    779             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6));
    780             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
    781             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8));
    782             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9));
    783             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10));
    784 
    785             assertEquals(testLabel, 2, layout.getOffsetToRightOf(0));
    786             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    787             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
    788             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
    789             assertEquals(testLabel, 6, layout.getOffsetToRightOf(4));
    790             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
    791             assertEquals(testLabel, 8, layout.getOffsetToRightOf(6));
    792             assertEquals(testLabel, 8, layout.getOffsetToRightOf(7));
    793             assertEquals(testLabel, 10, layout.getOffsetToRightOf(8));
    794             assertEquals(testLabel, 10, layout.getOffsetToRightOf(9));
    795             assertEquals(testLabel, 10, layout.getOffsetToRightOf(10));
    796         }
    797     }
    798 
    799     @Test
    800     public void testGetOffset_UNICODE_SurrogatePairs() {
    801         // Emoticons for surrogate pairs tests.
    802         String testString =
    803                 "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04";
    804         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
    805             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    806                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    807 
    808             String testLabel = buildTestMessage(seq);
    809 
    810             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    811             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    812             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(2));
    813             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
    814             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
    815             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(5));
    816             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(6));
    817             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
    818             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(8));
    819             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(9));
    820             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(10));
    821 
    822             assertEquals(testLabel, 2, layout.getOffsetToRightOf(0));
    823             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    824             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
    825             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
    826             assertEquals(testLabel, 6, layout.getOffsetToRightOf(4));
    827             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
    828             assertEquals(testLabel, 8, layout.getOffsetToRightOf(6));
    829             assertEquals(testLabel, 8, layout.getOffsetToRightOf(7));
    830             assertEquals(testLabel, 10, layout.getOffsetToRightOf(8));
    831             assertEquals(testLabel, 10, layout.getOffsetToRightOf(9));
    832             assertEquals(testLabel, 10, layout.getOffsetToRightOf(10));
    833         }
    834     }
    835 
    836     @Test
    837     public void testGetOffset_UNICODE_Thai() {
    838         // Thai Characters. The expected cursorable boundary is
    839         // | \u0E02 | \u0E2D | \u0E1A | \u0E04\u0E38 | \u0E13 |
    840         String testString = "\u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13";
    841         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
    842             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    843                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    844 
    845             String testLabel = buildTestMessage(seq);
    846 
    847             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    848             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    849             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
    850             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(3));
    851             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
    852             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5));
    853             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
    854 
    855             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
    856             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    857             assertEquals(testLabel, 3, layout.getOffsetToRightOf(2));
    858             assertEquals(testLabel, 5, layout.getOffsetToRightOf(3));
    859             assertEquals(testLabel, 5, layout.getOffsetToRightOf(4));
    860             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
    861             assertEquals(testLabel, 6, layout.getOffsetToRightOf(6));
    862         }
    863     }
    864 
    865     @Test
    866     public void testGetOffset_UNICODE_Arabic() {
    867         // Arabic Characters. The expected cursorable boundary is
    868         // | \u0623 \u064F | \u0633 \u0652 | \u0631 \u064E | \u0629 \u064C |";
    869         String testString = "\u0623\u064F\u0633\u0652\u0631\u064E\u0629\u064C";
    870 
    871         Normalizer.Form[] oneUnicodeForms = { Normalizer.Form.NFC, Normalizer.Form.NFKC };
    872         for (CharSequence seq: buildTestCharSequences(testString, oneUnicodeForms)) {
    873             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    874                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    875 
    876             String testLabel = buildTestMessage(seq);
    877 
    878             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(0));
    879             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
    880             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2));
    881             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
    882             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(4));
    883             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5));
    884             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(6));
    885             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(7));
    886             assertEquals(testLabel, 8, layout.getOffsetToLeftOf(8));
    887 
    888             assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
    889             assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
    890             assertEquals(testLabel, 0, layout.getOffsetToRightOf(2));
    891             assertEquals(testLabel, 2, layout.getOffsetToRightOf(3));
    892             assertEquals(testLabel, 2, layout.getOffsetToRightOf(4));
    893             assertEquals(testLabel, 4, layout.getOffsetToRightOf(5));
    894             assertEquals(testLabel, 4, layout.getOffsetToRightOf(6));
    895             assertEquals(testLabel, 6, layout.getOffsetToRightOf(7));
    896             assertEquals(testLabel, 6, layout.getOffsetToRightOf(8));
    897         }
    898     }
    899 
    900     @Test
    901     public void testGetOffset_UNICODE_Bidi() {
    902         // String having RTL characters and LTR characters
    903 
    904         // LTR Context
    905         // The first and last two characters are LTR characters.
    906         String testString = "\u0061\u0062\u05DE\u05E1\u05E2\u0063\u0064";
    907         // Logical order: [L1] [L2] [R1] [R2] [R3] [L3] [L4]
    908         //               0    1    2    3    4    5    6    7
    909         // Display order: [L1] [L2] [R3] [R2] [R1] [L3] [L4]
    910         //               0    1    2    4    3    5    6    7
    911         // [L?] means ?th LTR character and [R?] means ?th RTL character.
    912         for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) {
    913             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    914                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    915 
    916             String testLabel = buildTestMessage(seq);
    917 
    918             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(0));
    919             assertEquals(testLabel, 0, layout.getOffsetToLeftOf(1));
    920             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(2));
    921             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(3));
    922             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(4));
    923             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(5));
    924             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(6));
    925             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(7));
    926 
    927             assertEquals(testLabel, 1, layout.getOffsetToRightOf(0));
    928             assertEquals(testLabel, 2, layout.getOffsetToRightOf(1));
    929             assertEquals(testLabel, 4, layout.getOffsetToRightOf(2));
    930             assertEquals(testLabel, 5, layout.getOffsetToRightOf(3));
    931             assertEquals(testLabel, 3, layout.getOffsetToRightOf(4));
    932             assertEquals(testLabel, 6, layout.getOffsetToRightOf(5));
    933             assertEquals(testLabel, 7, layout.getOffsetToRightOf(6));
    934             assertEquals(testLabel, 7, layout.getOffsetToRightOf(7));
    935         }
    936 
    937         // RTL Context
    938         // The first and last two characters are RTL characters.
    939         String testString2 = "\u05DE\u05E1\u0063\u0064\u0065\u05DE\u05E1";
    940         // Logical order: [R1] [R2] [L1] [L2] [L3] [R3] [R4]
    941         //               0    1    2    3    4    5    6    7
    942         // Display order: [R4] [R3] [L1] [L2] [L3] [R2] [R1]
    943         //               7    6    5    3    4    2    1    0
    944         // [L?] means ?th LTR character and [R?] means ?th RTL character.
    945         for (CharSequence seq: buildTestCharSequences(testString2, Normalizer.Form.values())) {
    946             StaticLayout layout = new StaticLayout(seq, mDefaultPaint,
    947                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
    948 
    949             String testLabel = buildTestMessage(seq);
    950 
    951             assertEquals(testLabel, 1, layout.getOffsetToLeftOf(0));
    952             assertEquals(testLabel, 2, layout.getOffsetToLeftOf(1));
    953             assertEquals(testLabel, 4, layout.getOffsetToLeftOf(2));
    954             assertEquals(testLabel, 5, layout.getOffsetToLeftOf(3));
    955             assertEquals(testLabel, 3, layout.getOffsetToLeftOf(4));
    956             assertEquals(testLabel, 6, layout.getOffsetToLeftOf(5));
    957             assertEquals(testLabel, 7, layout.getOffsetToLeftOf(6));
    958             assertEquals(testLabel, 7, layout.getOffsetToLeftOf(7));
    959 
    960             assertEquals(testLabel, 0, layout.getOffsetToRightOf(0));
    961             assertEquals(testLabel, 0, layout.getOffsetToRightOf(1));
    962             assertEquals(testLabel, 1, layout.getOffsetToRightOf(2));
    963             assertEquals(testLabel, 4, layout.getOffsetToRightOf(3));
    964             assertEquals(testLabel, 2, layout.getOffsetToRightOf(4));
    965             assertEquals(testLabel, 3, layout.getOffsetToRightOf(5));
    966             assertEquals(testLabel, 5, layout.getOffsetToRightOf(6));
    967             assertEquals(testLabel, 6, layout.getOffsetToRightOf(7));
    968         }
    969     }
    970 
    971     private void moveCursorToRightCursorableOffset(EditorState state) {
    972         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
    973         StaticLayout layout = StaticLayout.Builder.obtain(state.mText, 0, state.mText.length(),
    974                 mDefaultPaint, DEFAULT_OUTER_WIDTH).build();
    975         final int newOffset = layout.getOffsetToRightOf(state.mSelectionStart);
    976         state.mSelectionStart = state.mSelectionEnd = newOffset;
    977     }
    978 
    979     private void moveCursorToLeftCursorableOffset(EditorState state) {
    980         assertEquals("The editor has selection", state.mSelectionStart, state.mSelectionEnd);
    981         StaticLayout layout = StaticLayout.Builder.obtain(state.mText, 0, state.mText.length(),
    982                 mDefaultPaint, DEFAULT_OUTER_WIDTH).build();
    983         final int newOffset = layout.getOffsetToLeftOf(state.mSelectionStart);
    984         state.mSelectionStart = state.mSelectionEnd = newOffset;
    985     }
    986 
    987     @Test
    988     public void testGetOffset_Emoji() {
    989         EditorState state = new EditorState();
    990 
    991         // Emojis
    992         // U+00A9 is COPYRIGHT SIGN.
    993         state.setByString("| U+00A9 U+00A9 U+00A9");
    994         moveCursorToRightCursorableOffset(state);
    995         state.assertEquals("U+00A9 | U+00A9 U+00A9");
    996         moveCursorToRightCursorableOffset(state);
    997         state.assertEquals("U+00A9 U+00A9 | U+00A9");
    998         moveCursorToRightCursorableOffset(state);
    999         state.assertEquals("U+00A9 U+00A9 U+00A9 |");
   1000         moveCursorToRightCursorableOffset(state);
   1001         state.assertEquals("U+00A9 U+00A9 U+00A9 |");
   1002         moveCursorToLeftCursorableOffset(state);
   1003         state.assertEquals("U+00A9 U+00A9 | U+00A9");
   1004         moveCursorToLeftCursorableOffset(state);
   1005         state.assertEquals("U+00A9 | U+00A9 U+00A9");
   1006         moveCursorToLeftCursorableOffset(state);
   1007         state.assertEquals("| U+00A9 U+00A9 U+00A9");
   1008         moveCursorToLeftCursorableOffset(state);
   1009         state.assertEquals("| U+00A9 U+00A9 U+00A9");
   1010 
   1011         // Surrogate pairs
   1012         // U+1F468 is MAN.
   1013         state.setByString("| U+1F468 U+1F468 U+1F468");
   1014         moveCursorToRightCursorableOffset(state);
   1015         state.assertEquals("U+1F468 | U+1F468 U+1F468");
   1016         moveCursorToRightCursorableOffset(state);
   1017         state.assertEquals("U+1F468 U+1F468 | U+1F468");
   1018         moveCursorToRightCursorableOffset(state);
   1019         state.assertEquals("U+1F468 U+1F468 U+1F468 |");
   1020         moveCursorToRightCursorableOffset(state);
   1021         state.assertEquals("U+1F468 U+1F468 U+1F468 |");
   1022         moveCursorToLeftCursorableOffset(state);
   1023         state.assertEquals("U+1F468 U+1F468 | U+1F468");
   1024         moveCursorToLeftCursorableOffset(state);
   1025         state.assertEquals("U+1F468 | U+1F468 U+1F468");
   1026         moveCursorToLeftCursorableOffset(state);
   1027         state.assertEquals("| U+1F468 U+1F468 U+1F468");
   1028         moveCursorToLeftCursorableOffset(state);
   1029         state.assertEquals("| U+1F468 U+1F468 U+1F468");
   1030 
   1031         // Keycaps
   1032         // U+20E3 is COMBINING ENCLOSING KEYCAP.
   1033         state.setByString("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
   1034         moveCursorToRightCursorableOffset(state);
   1035         state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3");
   1036         moveCursorToRightCursorableOffset(state);
   1037         state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3");
   1038         moveCursorToRightCursorableOffset(state);
   1039         state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |");
   1040         moveCursorToRightCursorableOffset(state);
   1041         state.assertEquals("'1' U+20E3 '1' U+20E3 '1' U+20E3 |");
   1042         moveCursorToLeftCursorableOffset(state);
   1043         state.assertEquals("'1' U+20E3 '1' U+20E3 | '1' U+20E3");
   1044         moveCursorToLeftCursorableOffset(state);
   1045         state.assertEquals("'1' U+20E3 | '1' U+20E3 '1' U+20E3");
   1046         moveCursorToLeftCursorableOffset(state);
   1047         state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
   1048         moveCursorToLeftCursorableOffset(state);
   1049         state.assertEquals("| '1' U+20E3 '1' U+20E3 '1' U+20E3");
   1050 
   1051         // Variation selectors
   1052         // U+00A9 is COPYRIGHT SIGN, U+FE0E is VARIATION SELECTOR-15. U+FE0F is VARIATION
   1053         // SELECTOR-16.
   1054         state.setByString("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
   1055         moveCursorToRightCursorableOffset(state);
   1056         state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E");
   1057         moveCursorToRightCursorableOffset(state);
   1058         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E");
   1059         moveCursorToRightCursorableOffset(state);
   1060         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |");
   1061         moveCursorToRightCursorableOffset(state);
   1062         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E |");
   1063         moveCursorToLeftCursorableOffset(state);
   1064         state.assertEquals("U+00A9 U+FE0E U+00A9 U+FE0F | U+00A9 U+FE0E");
   1065         moveCursorToLeftCursorableOffset(state);
   1066         state.assertEquals("U+00A9 U+FE0E | U+00A9 U+FE0F U+00A9 U+FE0E");
   1067         moveCursorToLeftCursorableOffset(state);
   1068         state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
   1069         moveCursorToLeftCursorableOffset(state);
   1070         state.assertEquals("| U+00A9 U+FE0E U+00A9 U+FE0F U+00A9 U+FE0E");
   1071 
   1072         // Keycap + variation selector
   1073         state.setByString("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
   1074         moveCursorToRightCursorableOffset(state);
   1075         state.assertEquals("'1' U+FE0F U+20E3 | '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
   1076         moveCursorToRightCursorableOffset(state);
   1077         state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 | '1' U+FE0F U+20E3");
   1078         moveCursorToRightCursorableOffset(state);
   1079         state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 |");
   1080         moveCursorToRightCursorableOffset(state);
   1081         state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 |");
   1082         moveCursorToLeftCursorableOffset(state);
   1083         state.assertEquals("'1' U+FE0F U+20E3 '1' U+FE0F U+20E3 | '1' U+FE0F U+20E3");
   1084         moveCursorToLeftCursorableOffset(state);
   1085         state.assertEquals("'1' U+FE0F U+20E3 | '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
   1086         moveCursorToLeftCursorableOffset(state);
   1087         state.assertEquals("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
   1088         moveCursorToLeftCursorableOffset(state);
   1089         state.assertEquals("| '1' U+FE0F U+20E3 '1' U+FE0F U+20E3 '1' U+FE0F U+20E3");
   1090 
   1091         // Flags
   1092         // U+1F1E6 U+1F1E8 is Ascension Island flag.
   1093         state.setByString("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
   1094         moveCursorToRightCursorableOffset(state);
   1095         state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
   1096         moveCursorToRightCursorableOffset(state);
   1097         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8");
   1098         moveCursorToRightCursorableOffset(state);
   1099         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |");
   1100         moveCursorToRightCursorableOffset(state);
   1101         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 |");
   1102         moveCursorToLeftCursorableOffset(state);
   1103         state.assertEquals("U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8");
   1104         moveCursorToLeftCursorableOffset(state);
   1105         state.assertEquals("U+1F1E6 U+1F1E8 | U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
   1106         moveCursorToLeftCursorableOffset(state);
   1107         state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
   1108         moveCursorToLeftCursorableOffset(state);
   1109         state.assertEquals("| U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8 U+1F1E6 U+1F1E8");
   1110     }
   1111 
   1112     @Test
   1113     public void testGetOffsetForHorizontal_Multilines() {
   1114         // Emoticons for surrogate pairs tests.
   1115         String testString = "\uD83D\uDE00\uD83D\uDE01\uD83D\uDE02\uD83D\uDE03\uD83D\uDE04";
   1116         final float width = mDefaultPaint.measureText(testString, 0, 6);
   1117         StaticLayout layout = new StaticLayout(testString, mDefaultPaint, (int)width,
   1118                 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
   1119         // We expect the line break to be after the third emoticon, but we allow flexibility of the
   1120         // line break algorithm as long as the break is within the string. These other cases might
   1121         // happen if for example the font has kerning between emoticons.
   1122         final int lineBreakOffset = layout.getOffsetForHorizontal(1, 0.0f);
   1123         assertEquals(0, layout.getLineForOffset(lineBreakOffset - 1));
   1124 
   1125         assertEquals(0, layout.getOffsetForHorizontal(0, 0.0f));
   1126         assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width));
   1127         assertEquals(lineBreakOffset - 2, layout.getOffsetForHorizontal(0, width * 2));
   1128 
   1129         final int lineCount = layout.getLineCount();
   1130         assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width));
   1131         assertEquals(testString.length(), layout.getOffsetForHorizontal(lineCount - 1, width * 2));
   1132     }
   1133 
   1134     @Test
   1135     public void testIsRtlCharAt() {
   1136         {
   1137             String testString = "ab(\u0623\u0624)c\u0625";
   1138             StaticLayout layout = new StaticLayout(testString, mDefaultPaint,
   1139                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
   1140 
   1141             assertFalse(layout.isRtlCharAt(0));
   1142             assertFalse(layout.isRtlCharAt(1));
   1143             assertFalse(layout.isRtlCharAt(2));
   1144             assertTrue(layout.isRtlCharAt(3));
   1145             assertTrue(layout.isRtlCharAt(4));
   1146             assertFalse(layout.isRtlCharAt(5));
   1147             assertFalse(layout.isRtlCharAt(6));
   1148             assertTrue(layout.isRtlCharAt(7));
   1149         }
   1150         {
   1151             String testString = "\u0623\u0624(ab)\u0625c";
   1152             StaticLayout layout = new StaticLayout(testString, mDefaultPaint,
   1153                     DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
   1154 
   1155             assertTrue(layout.isRtlCharAt(0));
   1156             assertTrue(layout.isRtlCharAt(1));
   1157             assertTrue(layout.isRtlCharAt(2));
   1158             assertFalse(layout.isRtlCharAt(3));
   1159             assertFalse(layout.isRtlCharAt(4));
   1160             assertTrue(layout.isRtlCharAt(5));
   1161             assertTrue(layout.isRtlCharAt(6));
   1162             assertFalse(layout.isRtlCharAt(7));
   1163             assertFalse(layout.isRtlCharAt(8));
   1164         }
   1165     }
   1166 
   1167     @Test
   1168     public void testGetHorizontal() {
   1169         String testString = "abc\u0623\u0624\u0625def";
   1170         StaticLayout layout = new StaticLayout(testString, mDefaultPaint,
   1171                 DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
   1172 
   1173         assertEquals(layout.getPrimaryHorizontal(0), layout.getSecondaryHorizontal(0), 0.0f);
   1174         assertTrue(layout.getPrimaryHorizontal(0) < layout.getPrimaryHorizontal(3));
   1175         assertTrue(layout.getPrimaryHorizontal(3) < layout.getSecondaryHorizontal(3));
   1176         assertTrue(layout.getPrimaryHorizontal(4) < layout.getSecondaryHorizontal(3));
   1177         assertEquals(layout.getPrimaryHorizontal(4), layout.getSecondaryHorizontal(4), 0.0f);
   1178         assertEquals(layout.getPrimaryHorizontal(3), layout.getSecondaryHorizontal(6), 0.0f);
   1179         assertEquals(layout.getPrimaryHorizontal(6), layout.getSecondaryHorizontal(3), 0.0f);
   1180         assertEquals(layout.getPrimaryHorizontal(7), layout.getSecondaryHorizontal(7), 0.0f);
   1181     }
   1182 
   1183     @Test
   1184     public void testVeryLargeString() {
   1185         final int MAX_COUNT = 1 << 20;
   1186         final int WORD_SIZE = 32;
   1187         char[] longText = new char[MAX_COUNT];
   1188         for (int n = 0; n < MAX_COUNT; n++) {
   1189             longText[n] = (n % WORD_SIZE) == 0 ? ' ' : 'm';
   1190         }
   1191         String longTextString = new String(longText);
   1192         TextPaint paint = new TestingTextPaint();
   1193         StaticLayout layout = new StaticLayout(longTextString, paint, DEFAULT_OUTER_WIDTH,
   1194                 DEFAULT_ALIGN, SPACE_MULTI, SPACE_ADD, true);
   1195         assertNotNull(layout);
   1196     }
   1197 
   1198     @Test
   1199     public void testNoCrashWhenWordStyleOverlap() {
   1200        // test case where word boundary overlaps multiple style spans
   1201        SpannableStringBuilder text = new SpannableStringBuilder("word boundaries, overlap style");
   1202        // span covers "boundaries"
   1203        text.setSpan(new StyleSpan(Typeface.BOLD),
   1204                    "word ".length(), "word boundaries".length(),
   1205                    Spanned.SPAN_INCLUSIVE_INCLUSIVE);
   1206        mDefaultPaint.setTextLocale(Locale.US);
   1207        StaticLayout layout = StaticLayout.Builder.obtain(text, 0, text.length(),
   1208                mDefaultPaint, DEFAULT_OUTER_WIDTH)
   1209                .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)  // enable hyphenation
   1210                .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
   1211                .build();
   1212        assertNotNull(layout);
   1213     }
   1214 
   1215     @Test
   1216     public void testRespectingIndentsOnEllipsizedText() {
   1217         // test case where word boundary overlaps multiple style spans
   1218         final String text = "words with indents";
   1219 
   1220         // +1 to ensure that we won't wrap in the normal case
   1221         int textWidth = (int) (mDefaultPaint.measureText(text) + 1);
   1222         StaticLayout layout = StaticLayout.Builder.obtain(text, 0, text.length(),
   1223                 mDefaultPaint, textWidth)
   1224                 .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY)  // enable hyphenation
   1225                 .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
   1226                 .setEllipsize(TruncateAt.END)
   1227                 .setEllipsizedWidth(textWidth)
   1228                 .setMaxLines(1)
   1229                 .setIndents(null, new int[] {20})
   1230                 .build();
   1231         assertTrue(layout.getEllipsisStart(0) != 0);
   1232     }
   1233 
   1234     @Test(expected = IndexOutOfBoundsException.class)
   1235     public void testGetPrimary_shouldFail_whenOffsetIsOutOfBounds_withSpannable() {
   1236         final String text = "1\n2\n3";
   1237         final SpannableString spannable = new SpannableString(text);
   1238         spannable.setSpan(new Object(), 0, text.length(), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
   1239         final Layout layout = StaticLayout.Builder.obtain(spannable, 0, spannable.length(),
   1240                 mDefaultPaint, Integer.MAX_VALUE - 1).setMaxLines(2)
   1241                 .setEllipsize(TruncateAt.END).build();
   1242         layout.getPrimaryHorizontal(layout.getText().length());
   1243     }
   1244 
   1245     @Test(expected = IndexOutOfBoundsException.class)
   1246     public void testGetPrimary_shouldFail_whenOffsetIsOutOfBounds_withString() {
   1247         final String text = "1\n2\n3";
   1248         final Layout layout = StaticLayout.Builder.obtain(text, 0, text.length(),
   1249                 mDefaultPaint, Integer.MAX_VALUE - 1).setMaxLines(2)
   1250                 .setEllipsize(TruncateAt.END).build();
   1251         layout.getPrimaryHorizontal(layout.getText().length());
   1252     }
   1253 
   1254     @Test
   1255     public void testNegativeWidth() {
   1256         StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5)
   1257             .setIndents(new int[] { 10 }, new int[] { 10 })
   1258             .setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY).build();
   1259         StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5)
   1260             .setIndents(new int[] { 10 }, new int[] { 10 })
   1261             .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE).build();
   1262         StaticLayout.Builder.obtain("a", 0, 1, new TextPaint(), 5)
   1263             .setIndents(new int[] { 10 }, new int[] { 10 })
   1264             .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED).build();
   1265     }
   1266 
   1267     @Test
   1268     public void testGetLineMax() {
   1269         final float wholeWidth = mDefaultPaint.measureText(LOREM_IPSUM);
   1270         final int lineWidth = (int) (wholeWidth / 10.0f);  // Make 10 lines per paragraph.
   1271         final String multiParaTestString =
   1272                 LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM;
   1273         final Layout layout = StaticLayout.Builder.obtain(multiParaTestString, 0,
   1274                 multiParaTestString.length(), mDefaultPaint, lineWidth)
   1275                 .build();
   1276         for (int i = 0; i < layout.getLineCount(); i++) {
   1277             assertTrue(layout.getLineMax(i) <= lineWidth);
   1278         }
   1279     }
   1280 
   1281     @Test
   1282     public void testIndent() {
   1283         final float wholeWidth = mDefaultPaint.measureText(LOREM_IPSUM);
   1284         final int lineWidth = (int) (wholeWidth / 10.0f);  // Make 10 lines per paragraph.
   1285         final int indentWidth = (int) (lineWidth * 0.3f);  // Make 30% indent.
   1286         final String multiParaTestString =
   1287                 LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM + "\n" + LOREM_IPSUM;
   1288         final Layout layout = StaticLayout.Builder.obtain(multiParaTestString, 0,
   1289                 multiParaTestString.length(), mDefaultPaint, lineWidth)
   1290                 .setIndents(new int[] { indentWidth }, null)
   1291                 .build();
   1292         for (int i = 0; i < layout.getLineCount(); i++) {
   1293             assertTrue(layout.getLineMax(i) <= lineWidth - indentWidth);
   1294         }
   1295     }
   1296 
   1297     private static Bitmap drawToBitmap(Layout l) {
   1298         final Bitmap bmp = Bitmap.createBitmap(l.getWidth(), l.getHeight(), Bitmap.Config.RGB_565);
   1299         final Canvas c = new Canvas(bmp);
   1300 
   1301         c.save();
   1302         c.translate(0, 0);
   1303         l.draw(c);
   1304         c.restore();
   1305         return bmp;
   1306     }
   1307 
   1308     private static String textPaintToString(TextPaint p) {
   1309         return "{"
   1310             + "mTextSize=" + p.getTextSize() + ", "
   1311             + "mTextSkewX=" + p.getTextSkewX() + ", "
   1312             + "mTextScaleX=" + p.getTextScaleX() + ", "
   1313             + "mLetterSpacing=" + p.getLetterSpacing() + ", "
   1314             + "mFlags=" + p.getFlags() + ", "
   1315             + "mTextLocales=" + p.getTextLocales() + ", "
   1316             + "mFontVariationSettings=" + p.getFontVariationSettings() + ", "
   1317             + "mTypeface=" + p.getTypeface() + ", "
   1318             + "mFontFeatureSettings=" + p.getFontFeatureSettings()
   1319             + "}";
   1320     }
   1321 
   1322     private static String directionToString(TextDirectionHeuristic dir) {
   1323         if (dir == TextDirectionHeuristics.LTR) {
   1324             return "LTR";
   1325         } else if (dir == TextDirectionHeuristics.RTL) {
   1326             return "RTL";
   1327         } else if (dir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
   1328             return "FIRSTSTRONG_LTR";
   1329         } else if (dir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
   1330             return "FIRSTSTRONG_RTL";
   1331         } else if (dir == TextDirectionHeuristics.ANYRTL_LTR) {
   1332             return "ANYRTL_LTR";
   1333         } else {
   1334             throw new RuntimeException("Unknown Direction");
   1335         }
   1336     }
   1337 
   1338     static class LayoutParam {
   1339         final int mStrategy;
   1340         final int mFrequency;
   1341         final TextPaint mPaint;
   1342         final TextDirectionHeuristic mDir;
   1343 
   1344         LayoutParam(int strategy, int frequency, TextPaint paint, TextDirectionHeuristic dir) {
   1345             mStrategy = strategy;
   1346             mFrequency = frequency;
   1347             mPaint = new TextPaint(paint);
   1348             mDir = dir;
   1349         }
   1350 
   1351         @Override
   1352         public String toString() {
   1353             return "{"
   1354                 + "mStrategy=" + mStrategy + ", "
   1355                 + "mFrequency=" + mFrequency + ", "
   1356                 + "mPaint=" + textPaintToString(mPaint) + ", "
   1357                 + "mDir=" + directionToString(mDir)
   1358                 + "}";
   1359 
   1360         }
   1361 
   1362         Layout getLayout(CharSequence text, int width) {
   1363             return StaticLayout.Builder.obtain(text, 0, text.length(), mPaint, width)
   1364                 .setBreakStrategy(mStrategy).setHyphenationFrequency(mFrequency)
   1365                 .setTextDirection(mDir).build();
   1366         }
   1367 
   1368         PrecomputedText getPrecomputedText(CharSequence text) {
   1369             PrecomputedText.Params param = new PrecomputedText.Params.Builder(mPaint)
   1370                     .setBreakStrategy(mStrategy)
   1371                     .setHyphenationFrequency(mFrequency)
   1372                     .setTextDirection(mDir).build();
   1373             return PrecomputedText.create(text, param);
   1374         }
   1375     };
   1376 
   1377     void assertSameStaticLayout(CharSequence text, LayoutParam measuredTextParam,
   1378                                 LayoutParam staticLayoutParam) {
   1379         String msg = "StaticLayout for " + staticLayoutParam + " with PrecomputedText"
   1380                 + " created with " + measuredTextParam + " must output the same BMP.";
   1381 
   1382         final float wholeWidth = mDefaultPaint.measureText(text.toString());
   1383         final int lineWidth = (int) (wholeWidth / 10.0f);  // Make 10 lines per paragraph.
   1384 
   1385         // Static layout parameter should be used for the final output.
   1386         final Layout expectedLayout = staticLayoutParam.getLayout(text, lineWidth);
   1387 
   1388         final PrecomputedText mt = measuredTextParam.getPrecomputedText(text);
   1389         final Layout resultLayout = StaticLayout.Builder.obtain(mt, 0, mt.length(),
   1390                 staticLayoutParam.mPaint, lineWidth)
   1391                 .setBreakStrategy(staticLayoutParam.mStrategy)
   1392                 .setHyphenationFrequency(staticLayoutParam.mFrequency)
   1393                 .setTextDirection(staticLayoutParam.mDir).build();
   1394 
   1395         assertEquals(msg, expectedLayout.getHeight(), resultLayout.getHeight(), 0.0f);
   1396 
   1397         final Bitmap expectedBMP = drawToBitmap(expectedLayout);
   1398         final Bitmap resultBMP = drawToBitmap(resultLayout);
   1399 
   1400         assertTrue(msg, resultBMP.sameAs(expectedBMP));
   1401     }
   1402 
   1403     @Test
   1404     public void testPrecomputedText() {
   1405         int[] breaks = {
   1406             Layout.BREAK_STRATEGY_SIMPLE,
   1407             Layout.BREAK_STRATEGY_HIGH_QUALITY,
   1408             Layout.BREAK_STRATEGY_BALANCED,
   1409         };
   1410 
   1411         int[] frequencies = {
   1412             Layout.HYPHENATION_FREQUENCY_NORMAL,
   1413             Layout.HYPHENATION_FREQUENCY_FULL,
   1414             Layout.HYPHENATION_FREQUENCY_NONE,
   1415         };
   1416 
   1417         TextDirectionHeuristic[] dirs = {
   1418             TextDirectionHeuristics.LTR,
   1419             TextDirectionHeuristics.RTL,
   1420             TextDirectionHeuristics.FIRSTSTRONG_LTR,
   1421             TextDirectionHeuristics.FIRSTSTRONG_RTL,
   1422             TextDirectionHeuristics.ANYRTL_LTR,
   1423         };
   1424 
   1425         float[] textSizes = {
   1426             8.0f, 16.0f, 32.0f
   1427         };
   1428 
   1429         LocaleList[] locales = {
   1430             LocaleList.forLanguageTags("en-US"),
   1431             LocaleList.forLanguageTags("ja-JP"),
   1432             LocaleList.forLanguageTags("en-US,ja-JP"),
   1433         };
   1434 
   1435         TextPaint paint = new TextPaint();
   1436 
   1437         // If the PrecomputedText is created with the same argument of the StaticLayout, generate
   1438         // the same bitmap.
   1439         for (int b : breaks) {
   1440             for (int f : frequencies) {
   1441                 for (TextDirectionHeuristic dir : dirs) {
   1442                     for (float textSize : textSizes) {
   1443                         for (LocaleList locale : locales) {
   1444                             paint.setTextSize(textSize);
   1445                             paint.setTextLocales(locale);
   1446 
   1447                             assertSameStaticLayout(LOREM_IPSUM,
   1448                                     new LayoutParam(b, f, paint, dir),
   1449                                     new LayoutParam(b, f, paint, dir));
   1450                         }
   1451                     }
   1452                 }
   1453             }
   1454         }
   1455 
   1456         // If the parameters are different, the output of the static layout must be
   1457         // same bitmap.
   1458         for (int bi = 0; bi < breaks.length; bi++) {
   1459             for (int fi = 0; fi < frequencies.length; fi++) {
   1460                 for (int diri = 0; diri < dirs.length; diri++) {
   1461                     for (int sizei = 0; sizei < textSizes.length; sizei++) {
   1462                         for (int localei = 0; localei < locales.length; localei++) {
   1463                             TextPaint p1 = new TextPaint();
   1464                             TextPaint p2 = new TextPaint();
   1465 
   1466                             p1.setTextSize(textSizes[sizei]);
   1467                             p2.setTextSize(textSizes[(sizei + 1) % textSizes.length]);
   1468 
   1469                             p1.setTextLocales(locales[localei]);
   1470                             p2.setTextLocales(locales[(localei + 1) % locales.length]);
   1471 
   1472                             int b1 = breaks[bi];
   1473                             int b2 = breaks[(bi + 1) % breaks.length];
   1474 
   1475                             int f1 = frequencies[fi];
   1476                             int f2 = frequencies[(fi + 1) % frequencies.length];
   1477 
   1478                             TextDirectionHeuristic dir1 = dirs[diri];
   1479                             TextDirectionHeuristic dir2 = dirs[(diri + 1) % dirs.length];
   1480 
   1481                             assertSameStaticLayout(LOREM_IPSUM,
   1482                                     new LayoutParam(b1, f1, p1, dir1),
   1483                                     new LayoutParam(b2, f2, p2, dir2));
   1484                         }
   1485                     }
   1486                 }
   1487             }
   1488         }
   1489     }
   1490 
   1491 
   1492     @Test
   1493     public void testReplacementFontMetricsTest() {
   1494         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
   1495 
   1496         Typeface tf = new Typeface.Builder(context.getAssets(), "fonts/samplefont.ttf").build();
   1497         assertNotNull(tf);
   1498         TextPaint paint = new TextPaint();
   1499         paint.setTypeface(tf);
   1500 
   1501         ReplacementSpan firstReplacement = mock(ReplacementSpan.class);
   1502         ArgumentCaptor<FontMetricsInt> fm1Captor = ArgumentCaptor.forClass(FontMetricsInt.class);
   1503         when(firstReplacement.getSize(
   1504             any(Paint.class), any(CharSequence.class), anyInt(), anyInt(),
   1505             fm1Captor.capture())).thenReturn(0);
   1506         TextAppearanceSpan firstStyleSpan = new TextAppearanceSpan(
   1507                 null /* family */, Typeface.NORMAL /* style */, 100 /* text size, 1em = 100px */,
   1508                 null /* text color */, null /* link color */);
   1509 
   1510         ReplacementSpan secondReplacement = mock(ReplacementSpan.class);
   1511         ArgumentCaptor<FontMetricsInt> fm2Captor = ArgumentCaptor.forClass(FontMetricsInt.class);
   1512         when(secondReplacement.getSize(
   1513             any(Paint.class), any(CharSequence.class), any(Integer.class), any(Integer.class),
   1514             fm2Captor.capture())).thenReturn(0);
   1515         TextAppearanceSpan secondStyleSpan = new TextAppearanceSpan(
   1516                 null /* family */, Typeface.NORMAL /* style */, 200 /* text size, 1em = 200px */,
   1517                 null /* text color */, null /* link color */);
   1518 
   1519         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello, World\nHello, Android");
   1520         ssb.setSpan(firstStyleSpan, 0, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   1521         ssb.setSpan(firstReplacement, 0, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   1522         ssb.setSpan(secondStyleSpan, 13, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   1523         ssb.setSpan(secondReplacement, 13, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   1524 
   1525         StaticLayout.Builder.obtain(ssb, 0, ssb.length(), paint, Integer.MAX_VALUE).build();
   1526 
   1527         FontMetricsInt firstMetrics = fm1Captor.getValue();
   1528         FontMetricsInt secondMetrics = fm2Captor.getValue();
   1529 
   1530         // The samplefont.ttf has 0.8em ascent and 0.2em descent.
   1531         assertEquals(-100, firstMetrics.ascent);
   1532         assertEquals(20, firstMetrics.descent);
   1533 
   1534         assertEquals(-200, secondMetrics.ascent);
   1535         assertEquals(40, secondMetrics.descent);
   1536     }
   1537 
   1538     @Test
   1539     public void testChangeFontMetricsLineHeightBySpanTest() {
   1540         final TextPaint paint = new TextPaint();
   1541         paint.setTextSize(50);
   1542         final SpannableString spanStr0 = new SpannableString(LOREM_IPSUM);
   1543         // Make sure the final layout contain multiple lines.
   1544         final int width = (int) paint.measureText(spanStr0.toString()) / 5;
   1545         final int expectedHeight0 = 25;
   1546 
   1547         spanStr0.setSpan(new LineHeightSpan.Standard(expectedHeight0), 0, spanStr0.length(),
   1548                 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
   1549         StaticLayout layout0 = StaticLayout.Builder.obtain(spanStr0, 0, spanStr0.length(),
   1550                 paint, width).build();
   1551 
   1552         // We need at least 3 lines for testing.
   1553         assertTrue(layout0.getLineCount() > 2);
   1554         // Omit the first and last line, because their line hight might be different due to padding.
   1555         for (int i = 1; i < layout0.getLineCount() - 1; ++i) {
   1556             assertEquals(expectedHeight0, layout0.getLineBottom(i) - layout0.getLineTop(i));
   1557         }
   1558 
   1559         final SpannableString spanStr1 = new SpannableString(LOREM_IPSUM);
   1560         int expectedHeight1 = 100;
   1561 
   1562         spanStr1.setSpan(new LineHeightSpan.Standard(expectedHeight1), 0, spanStr1.length(),
   1563                 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
   1564         StaticLayout layout1 = StaticLayout.Builder.obtain(spanStr1, 0, spanStr1.length(),
   1565                 paint, width).build();
   1566 
   1567         for (int i = 1; i < layout1.getLineCount() - 1; ++i) {
   1568             assertEquals(expectedHeight1, layout1.getLineBottom(i) - layout1.getLineTop(i));
   1569         }
   1570     }
   1571 
   1572     @Test
   1573     public void testChangeFontMetricsLineHeightBySpanMultipleTimesTest() {
   1574         final TextPaint paint = new TextPaint();
   1575         paint.setTextSize(50);
   1576         final SpannableString spanStr = new SpannableString(LOREM_IPSUM);
   1577         final int width = (int) paint.measureText(spanStr.toString()) / 5;
   1578         final int expectedHeight = 100;
   1579 
   1580         spanStr.setSpan(new LineHeightSpan.Standard(25), 0, spanStr.length(),
   1581                 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
   1582         // Only the last span is effective.
   1583         spanStr.setSpan(new LineHeightSpan.Standard(expectedHeight), 0, spanStr.length(),
   1584                 SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
   1585         StaticLayout layout = StaticLayout.Builder.obtain(spanStr, 0, spanStr.length(),
   1586                 paint, width).build();
   1587 
   1588         assertTrue(layout.getLineCount() > 2);
   1589         for (int i = 1; i < layout.getLineCount() - 1; ++i) {
   1590             assertEquals(expectedHeight, layout.getLineBottom(i) - layout.getLineTop(i));
   1591         }
   1592     }
   1593 
   1594     private class FakeLineBackgroundSpan implements LineBackgroundSpan {
   1595         // Whenever drawBackground() is called, the start and end of
   1596         // the line will be stored into mHistory as an array in the
   1597         // format of [start, end].
   1598         private final List<int[]> mHistory;
   1599 
   1600         FakeLineBackgroundSpan() {
   1601             mHistory = new ArrayList<int[]>();
   1602         }
   1603 
   1604         @Override
   1605         public void drawBackground(Canvas c, Paint p,
   1606                 int left, int right,
   1607                 int top, int baseline, int bottom,
   1608                 CharSequence text, int start, int end,
   1609                 int lnum) {
   1610             mHistory.add(new int[] {start, end});
   1611         }
   1612 
   1613         List<int[]> getHistory() {
   1614             return mHistory;
   1615         }
   1616     }
   1617 
   1618     private void testLineBackgroundSpanInRange(String text, int start, int end) {
   1619         final SpannableString spanStr = new SpannableString(text);
   1620         final FakeLineBackgroundSpan span = new FakeLineBackgroundSpan();
   1621         spanStr.setSpan(span, start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
   1622 
   1623         final TextPaint paint = new TextPaint();
   1624         paint.setTextSize(50);
   1625         final int width = (int) paint.measureText(spanStr.toString()) / 5;
   1626         final StaticLayout layout = StaticLayout.Builder.obtain(spanStr, 0, spanStr.length(),
   1627                 paint, width).build();
   1628 
   1629         // One line is too simple, need more to test.
   1630         assertTrue(layout.getLineCount() > 1);
   1631         drawToBitmap(layout);
   1632         List<int[]> history = span.getHistory();
   1633 
   1634         if (history.size() == 0) {
   1635             // drawBackground() of FakeLineBackgroundSpan was never called.
   1636             // This only happens when the length of the span is zero.
   1637             assertTrue(start >= end);
   1638             return;
   1639         }
   1640 
   1641         // Check if drawBackground() is corrected called for each affected line.
   1642         int lastLineEnd = history.get(0)[0];
   1643         for (int[] lineRange: history) {
   1644             // The range of line must intersect with the span.
   1645             assertTrue(lineRange[0] < end && lineRange[1] > start);
   1646             // Check:
   1647             // 1. drawBackground() is called in the correct sequence.
   1648             // 2. drawBackground() is called only once for each affected line.
   1649             assertEquals(lastLineEnd, lineRange[0]);
   1650             lastLineEnd = lineRange[1];
   1651         }
   1652 
   1653         int[] firstLineRange = history.get(0);
   1654         int[] lastLineRange = history.get(history.size() - 1);
   1655 
   1656         // Check if affected lines match the span coverage.
   1657         assertTrue(firstLineRange[0] <= start && end <= lastLineRange[1]);
   1658     }
   1659 
   1660     @Test
   1661     public void testDrawWithLineBackgroundSpanCoverWholeText() {
   1662         testLineBackgroundSpanInRange(LOREM_IPSUM, 0, LOREM_IPSUM.length());
   1663     }
   1664 
   1665     @Test
   1666     public void testDrawWithLineBackgroundSpanCoverNothing() {
   1667         int i = 0;
   1668         // Zero length Spans.
   1669         testLineBackgroundSpanInRange(LOREM_IPSUM, i, i);
   1670         i = LOREM_IPSUM.length() / 2;
   1671         testLineBackgroundSpanInRange(LOREM_IPSUM, i, i);
   1672     }
   1673 
   1674     @Test
   1675     public void testDrawWithLineBackgroundSpanCoverPart() {
   1676         int start = 0;
   1677         int end = LOREM_IPSUM.length() / 2;
   1678         testLineBackgroundSpanInRange(LOREM_IPSUM, start, end);
   1679 
   1680         start = LOREM_IPSUM.length() / 2;
   1681         end = LOREM_IPSUM.length();
   1682         testLineBackgroundSpanInRange(LOREM_IPSUM, start, end);
   1683     }
   1684 }
   1685