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