Merge "Store line extra in layouts"
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index c7a5fce..91d8290 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -365,6 +365,7 @@
                 desc += botpad;
 
             ints[DESCENT] = desc;
+            ints[EXTRA] = reflowed.getLineExtra(i);
             objects[0] = reflowed.getLineDirections(i);
 
             final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
@@ -692,6 +693,14 @@
         return mInts.getValue(line, DESCENT);
     }
 
+    /**
+     * @hide
+     */
+    @Override
+    public int getLineExtra(int line) {
+        return mInts.getValue(line, EXTRA);
+    }
+
     @Override
     public int getLineStart(int line) {
         return mInts.getValue(line, START) & START_MASK;
@@ -851,14 +860,15 @@
     private static final int TAB = START;
     private static final int TOP = 1;
     private static final int DESCENT = 2;
+    private static final int EXTRA = 3;
     // HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
-    private static final int HYPHEN = 3;
+    private static final int HYPHEN = 4;
     private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
-    private static final int COLUMNS_NORMAL = 4;
+    private static final int COLUMNS_NORMAL = 5;
 
-    private static final int ELLIPSIS_START = 4;
-    private static final int ELLIPSIS_COUNT = 5;
-    private static final int COLUMNS_ELLIPSIZE = 6;
+    private static final int ELLIPSIS_START = 5;
+    private static final int ELLIPSIS_COUNT = 6;
+    private static final int COLUMNS_ELLIPSIZE = 7;
 
     private static final int START_MASK = 0x1FFFFFFF;
     private static final int DIR_SHIFT  = 30;
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 0f910cc..2dc3f60 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -17,6 +17,7 @@
 package android.text;
 
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Path;
@@ -1466,6 +1467,17 @@
         return getLineTop(line) - (getLineTop(line+1) - getLineDescent(line));
     }
 
+    /**
+     * Return the extra space added as a result of line spacing attributes
+     * {@link #getSpacingAdd()} and {@link #getSpacingMultiplier()}. Default value is {@code zero}.
+     *
+     * @param line the index of the line, the value should be equal or greater than {@code zero}
+     * @hide
+     */
+    public int getLineExtra(@IntRange(from = 0) int line) {
+        return 0;
+    }
+
     public int getOffsetToLeftOf(int offset) {
         return getOffsetToLeftRightOf(offset, true);
     }
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 54f9ea1..dc5553e 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -1006,6 +1006,7 @@
         lines[off + START] = start;
         lines[off + TOP] = v;
         lines[off + DESCENT] = below + extra;
+        lines[off + EXTRA] = extra;
 
         // special case for non-ellipsized last visible line when maxLines is set
         // store the height as if it was ellipsized
@@ -1194,6 +1195,14 @@
         return mLines[mColumns * line + TOP];
     }
 
+    /**
+     * @hide
+     */
+    @Override
+    public int getLineExtra(int line) {
+        return mLines[mColumns * line + EXTRA];
+    }
+
     @Override
     public int getLineDescent(int line) {
         return mLines[mColumns * line + DESCENT];
@@ -1216,6 +1225,9 @@
 
     @Override
     public final Directions getLineDirections(int line) {
+        if (line > getLineCount()) {
+            throw new ArrayIndexOutOfBoundsException();
+        }
         return mLineDirections[line];
     }
 
@@ -1367,16 +1379,17 @@
      */
     private int mMaxLineHeight = -1;
 
-    private static final int COLUMNS_NORMAL = 4;
-    private static final int COLUMNS_ELLIPSIZE = 6;
+    private static final int COLUMNS_NORMAL = 5;
+    private static final int COLUMNS_ELLIPSIZE = 7;
     private static final int START = 0;
     private static final int DIR = START;
     private static final int TAB = START;
     private static final int TOP = 1;
     private static final int DESCENT = 2;
-    private static final int HYPHEN = 3;
-    private static final int ELLIPSIS_START = 4;
-    private static final int ELLIPSIS_COUNT = 5;
+    private static final int EXTRA = 3;
+    private static final int HYPHEN = 4;
+    private static final int ELLIPSIS_START = 5;
+    private static final int ELLIPSIS_COUNT = 6;
 
     private int[] mLines;
     private Directions[] mLineDirections;
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutTest.java b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
index 811bf2c..5ef08e0 100644
--- a/core/tests/coretests/src/android/text/DynamicLayoutTest.java
+++ b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
@@ -18,6 +18,7 @@
 
 import static android.text.Layout.Alignment.ALIGN_NORMAL;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -112,4 +113,77 @@
         assertFalse(layout.getBlocksAlwaysNeedToBeRedrawn().contains(0));
         assertTrue(layout.getBlocksAlwaysNeedToBeRedrawn().isEmpty());
     }
+
+    @Test
+    public void testGetLineExtra_withoutLinespacing() {
+        final SpannableStringBuilder text = new SpannableStringBuilder("a\nb\nc");
+        final TextPaint textPaint = new TextPaint();
+
+        // create a StaticLayout to check against
+        final StaticLayout staticLayout = StaticLayout.Builder.obtain(text, 0,
+                text.length(), textPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .build();
+
+        // create the DynamicLayout
+        final DynamicLayout dynamicLayout = new DynamicLayout(text,
+                textPaint,
+                WIDTH,
+                ALIGN_NORMAL,
+                1f /*spacingMultiplier*/,
+                0 /*spacingAdd*/,
+                false /*includepad*/);
+
+        final int lineCount = staticLayout.getLineCount();
+        assertEquals(lineCount, dynamicLayout.getLineCount());
+        for (int i = 0; i < lineCount; i++) {
+            assertEquals(staticLayout.getLineExtra(i), dynamicLayout.getLineExtra(i));
+        }
+    }
+
+    @Test
+    public void testGetLineExtra_withLinespacing() {
+        final SpannableStringBuilder text = new SpannableStringBuilder("a\nb\nc");
+        final TextPaint textPaint = new TextPaint();
+        final float spacingMultiplier = 2f;
+        final float spacingAdd = 4;
+
+        // create a StaticLayout to check against
+        final StaticLayout staticLayout = StaticLayout.Builder.obtain(text, 0,
+                text.length(), textPaint, WIDTH)
+                .setAlignment(ALIGN_NORMAL)
+                .setIncludePad(false)
+                .setLineSpacing(spacingAdd, spacingMultiplier)
+                .build();
+
+        // create the DynamicLayout
+        final DynamicLayout dynamicLayout = new DynamicLayout(text,
+                textPaint,
+                WIDTH,
+                ALIGN_NORMAL,
+                spacingMultiplier,
+                spacingAdd,
+                false /*includepad*/);
+
+        final int lineCount = staticLayout.getLineCount();
+        assertEquals(lineCount, dynamicLayout.getLineCount());
+        for (int i = 0; i < lineCount - 1; i++) {
+            assertEquals(staticLayout.getLineExtra(i), dynamicLayout.getLineExtra(i));
+        }
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetLineExtra_withNegativeValue() {
+        final DynamicLayout layout = new DynamicLayout("", new TextPaint(), 10 /*width*/,
+                ALIGN_NORMAL, 1.0f /*spacingMultiplier*/, 0f /*spacingAdd*/, false /*includepad*/);
+        layout.getLineExtra(-1);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetLineExtra_withParamGreaterThanLineCount() {
+        final DynamicLayout layout = new DynamicLayout("", new TextPaint(), 10 /*width*/,
+                ALIGN_NORMAL, 1.0f /*spacingMultiplier*/, 0f /*spacingAdd*/, false /*includepad*/);
+        layout.getLineExtra(100);
+    }
 }
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 6d610bb..6b262eb 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -215,6 +215,17 @@
     }
 
     @Test
+    public void testGetLineExtra_returnsZeroByDefault() {
+        final String text = "a\nb\nc\n";
+        final Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
+                mAlign, 100 /* spacingMult*/, 100 /*spacingAdd*/);
+        final int lineCount = text.split("\n").length;
+        for (int i = 0; i < lineCount; i++) {
+            assertEquals(0, layout.getLineExtra(i));
+        }
+    }
+
+    @Test
     public void testGetLineVisibleEnd() {
         Layout layout = new MockLayout(LAYOUT_TEXT, mTextPaint, mWidth,
                 mAlign, mSpacingMult, mSpacingAdd);
diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java
index fb60e38..ad16e89 100644
--- a/core/tests/coretests/src/android/text/StaticLayoutTest.java
+++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java
@@ -107,7 +107,7 @@
 
         Layout l = b.build();
         assertVertMetrics(l, 0, 0,
-                fmi.ascent, fmi.descent);
+                new int[][]{{fmi.ascent, fmi.descent, 0}});
 
         // other quick metrics
         assertEquals(0, l.getLineStart(0));
@@ -124,14 +124,14 @@
      * Top and bottom padding are affected, as is the line descent and height.
      */
     @Test
-    public void testGetters2() {
+    public void testLineMetrics_withPadding() {
         LayoutBuilder b = builder()
             .setIncludePad(true);
         FontMetricsInt fmi = b.paint.getFontMetricsInt();
 
         Layout l = b.build();
         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
-                fmi.top, fmi.bottom);
+                new int[][]{{fmi.top, fmi.bottom, 0}});
     }
 
     /**
@@ -139,16 +139,18 @@
      * Ascent of top line and descent of bottom line are affected.
      */
     @Test
-    public void testGetters3() {
+    public void testLineMetrics_withPaddingAndWidth() {
         LayoutBuilder b = builder()
             .setIncludePad(true)
             .setWidth(50);
         FontMetricsInt fmi = b.paint.getFontMetricsInt();
 
-        Layout l =  b.build();
+        Layout l = b.build();
         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
-            fmi.top, fmi.descent,
-            fmi.ascent, fmi.bottom);
+                new int[][]{
+                        {fmi.top, fmi.descent, 0},
+                        {fmi.ascent, fmi.bottom, 0}
+                });
     }
 
     /**
@@ -156,7 +158,7 @@
      * First line ascent is top, bottom line descent is bottom.
      */
     @Test
-    public void testGetters4() {
+    public void testLineMetrics_withThreeLines() {
         LayoutBuilder b = builder()
             .setText("This is a longer test")
             .setIncludePad(true)
@@ -165,9 +167,11 @@
 
         Layout l = b.build();
         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
-                fmi.top, fmi.descent,
-                fmi.ascent, fmi.descent,
-                fmi.ascent, fmi.bottom);
+                new int[][]{
+                        {fmi.top, fmi.descent, 0},
+                        {fmi.ascent, fmi.descent, 0},
+                        {fmi.ascent, fmi.bottom, 0}
+                });
     }
 
     /**
@@ -176,7 +180,7 @@
      * even be non-zero leading.
      */
     @Test
-    public void testGetters5() {
+    public void testLineMetrics_withLargeText() {
         LayoutBuilder b = builder()
             .setText("This is a longer test")
             .setIncludePad(true)
@@ -193,9 +197,11 @@
         // using leading, this will fail.
         Layout l = b.build();
         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
-                fmi.top, fmi.descent,
-                fmi.ascent, fmi.descent,
-                fmi.ascent, fmi.bottom);
+                new int[][]{
+                        {fmi.top, fmi.descent, 0},
+                        {fmi.ascent, fmi.descent, 0},
+                        {fmi.ascent, fmi.bottom, 0}
+                });
     }
 
     /**
@@ -203,7 +209,7 @@
      * to 3 lines.
      */
     @Test
-    public void testGetters6() {
+    public void testLineMetrics_withSpacingAdd() {
         int spacingAdd = 2; // int so expressions return int
         LayoutBuilder b = builder()
             .setText("This is a longer test")
@@ -214,9 +220,11 @@
 
         Layout l = b.build();
         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
-                fmi.top, fmi.descent + spacingAdd,
-                fmi.ascent, fmi.descent + spacingAdd,
-                fmi.ascent, fmi.bottom);
+                new int[][]{
+                        {fmi.top, fmi.descent + spacingAdd, spacingAdd},
+                        {fmi.ascent, fmi.descent + spacingAdd, spacingAdd},
+                        {fmi.ascent, fmi.bottom, 0}
+                });
     }
 
     /**
@@ -224,7 +232,7 @@
      * spacingMult = 1.5, wrapping to 3 lines.
      */
     @Test
-    public void testGetters7() {
+    public void testLineMetrics_withSpacingMult() {
         LayoutBuilder b = builder()
             .setText("This is a longer test")
             .setIncludePad(true)
@@ -236,9 +244,13 @@
 
         Layout l = b.build();
         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
-                fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
-                fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
-                fmi.ascent, fmi.bottom);
+                new int[][]{
+                        {fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
+                                s.scale(fmi.descent - fmi.top)},
+                        {fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
+                                s.scale(fmi.descent - fmi.ascent)},
+                        {fmi.ascent, fmi.bottom, 0}
+                });
     }
 
     /**
@@ -246,7 +258,7 @@
      * spacingMult = 0.8 when wrapping to 3 lines.
      */
     @Test
-    public void testGetters8() {
+    public void testLineMetrics_withUnitIntervalSpacingMult() {
         LayoutBuilder b = builder()
             .setText("This is a longer test")
             .setIncludePad(true)
@@ -258,9 +270,25 @@
 
         Layout l = b.build();
         assertVertMetrics(l, fmi.top - fmi.ascent, fmi.bottom - fmi.descent,
-                fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
-                fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
-                fmi.ascent, fmi.bottom);
+                new int[][]{
+                        {fmi.top, fmi.descent + s.scale(fmi.descent - fmi.top),
+                                s.scale(fmi.descent - fmi.top)},
+                        {fmi.ascent, fmi.descent + s.scale(fmi.descent - fmi.ascent),
+                                s.scale(fmi.descent - fmi.ascent)},
+                        {fmi.ascent, fmi.bottom, 0}
+                });
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetLineExtra_withNegativeValue() {
+        final Layout layout = builder().build();
+        layout.getLineExtra(-1);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void testGetLineExtra_withParamGreaterThanLineCount() {
+        final Layout layout = builder().build();
+        layout.getLineExtra(100);
     }
 
     // ----- test utility classes and methods -----
@@ -341,26 +369,39 @@
         }
     }
 
-    private void assertVertMetrics(Layout l, int topPad, int botPad, int... values) {
+    /**
+     * Assert vertical metrics such as top, bottom, ascent, descent.
+     * @param l layout instance
+     * @param topPad top padding
+     * @param botPad bottom padding
+     * @param values values for each line where first is ascent, second is descent, and last one is
+     *               extra
+     */
+    private void assertVertMetrics(Layout l, int topPad, int botPad, int[][] values) {
         assertTopBotPadding(l, topPad, botPad);
         assertLinesMetrics(l, values);
     }
 
-    private void assertLinesMetrics(Layout l, int... values) {
-        // sanity check
-        if ((values.length & 0x1) != 0) {
-            throw new IllegalArgumentException(String.valueOf(values.length));
-        }
-
-        int lines = values.length >> 1;
+    /**
+     * Check given expected values against the Layout values.
+     * @param l layout instance
+     * @param values values for each line where first is ascent, second is descent, and last one is
+     *               extra
+     */
+    private void assertLinesMetrics(Layout l, int[][] values) {
+        final int lines = values.length;
         assertEquals(lines, l.getLineCount());
 
         int t = 0;
-        for (int i = 0, n = 0; i < lines; ++i, n += 2) {
-            int a = values[n];
-            int d = values[n+1];
+        for (int i = 0, n = 0; i < lines; ++i, n += 3) {
+            if (values[i].length != 3) {
+                throw new IllegalArgumentException(String.valueOf(values.length));
+            }
+            int a = values[i][0];
+            int d = values[i][1];
+            int extra = values[i][2];
             int h = -a + d;
-            assertLineMetrics(l, i, t, a, d, h);
+            assertLineMetrics(l, i, t, a, d, h, extra);
             t += h;
         }
 
@@ -368,12 +409,13 @@
     }
 
     private void assertLineMetrics(Layout l, int line,
-            int top, int ascent, int descent, int height) {
+            int top, int ascent, int descent, int height, int extra) {
         String info = "line " + line;
         assertEquals(info, top, l.getLineTop(line));
         assertEquals(info, ascent, l.getLineAscent(line));
         assertEquals(info, descent, l.getLineDescent(line));
         assertEquals(info, height, l.getLineBottom(line) - top);
+        assertEquals(info, extra, l.getLineExtra(line));
     }
 
     private void assertTopBotPadding(Layout l, int topPad, int botPad) {