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