Introduce TextLine#measureAllbounds

This new method computes the horizontal character bounds by making
use of the character advances returned from native layer. It'll be
used in TextView to populate character bounds, and it's much efficent
compared to calling getPrimaryHorizontal for each character.

Bug: 233922052
Test: atest android.text.TextLineTest
Change-Id: Icdd53e61e2d1513b2231affb19bb00ea5d938d48
diff --git a/core/api/current.txt b/core/api/current.txt
index bb32647..8d703d0 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -14928,6 +14928,8 @@
     method public android.graphics.PathEffect getPathEffect();
     method public float getRunAdvance(char[], int, int, int, int, boolean, int);
     method public float getRunAdvance(CharSequence, int, int, int, int, boolean, int);
+    method public float getRunCharacterAdvance(@NonNull char[], int, int, int, int, boolean, int, @Nullable float[], int);
+    method public float getRunCharacterAdvance(@NonNull CharSequence, int, int, int, int, boolean, int, @Nullable float[], int);
     method public android.graphics.Shader getShader();
     method @ColorInt public int getShadowLayerColor();
     method @ColorLong public long getShadowLayerColorLong();
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index e39231c..7394c22 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -408,14 +408,14 @@
                     final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
 
                     if (targetIsInThisSegment && sameDirection) {
-                        return h + measureRun(segStart, offset, j, runIsRtl, fmi);
+                        return h + measureRun(segStart, offset, j, runIsRtl, fmi, null, 0);
                     }
 
-                    final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi);
+                    final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, null, 0);
                     h += sameDirection ? segmentWidth : -segmentWidth;
 
                     if (targetIsInThisSegment) {
-                        return h + measureRun(segStart, offset, j, runIsRtl, null);
+                        return h + measureRun(segStart, offset, j, runIsRtl, null, null, 0);
                     }
 
                     if (j != runLimit) {  // charAt(j) == TAB_CHAR
@@ -437,6 +437,116 @@
     }
 
     /**
+     * Return the signed horizontal bounds of the characters in the line.
+     *
+     * The length of the returned array equals to 2 * mLen. The left bound of the i th character
+     * is stored at index 2 * i. And the right bound of the i th character is stored at index
+     * (2 * i + 1).
+     *
+     * Check the following examples. LX(e.g. L0, L1, ...) denotes a character which has LTR BiDi
+     * property. On the other hand, RX(e.g. R0, R1, ...) denotes a character which has RTL BiDi
+     * property. Assuming all character has 1em width.
+     *
+     * Example 1: All LTR chars within LTR context
+     *   Input Text (logical)  :   L0 L1 L2 L3
+     *   Input Text (visual)   :   L0 L1 L2 L3
+     *   Output :  [0em, 1em, 1em, 2em, 2em, 3em, 3em, 4em]
+     *
+     * Example 2: All RTL chars within RTL context.
+     *   Input Text (logical)  :   R0 R1 R2 R3
+     *   Input Text (visual)   :   R3 R2 R1 R0
+     *   Output :  [-1em, 0em, -2em, -1em, -3em, -2em, -4em, -3em]
+
+     *
+     * Example 3: BiDi chars within LTR context.
+     *   Input Text (logical)  :   L0 L1 R2 R3 L4 L5
+     *   Input Text (visual)   :   L0 L1 R3 R2 L4 L5
+     *   Output :  [0em, 1em, 1em, 2em, 3em, 4em, 2em, 3em, 4em, 5em, 5em, 6em]
+
+     *
+     * Example 4: BiDi chars within RTL context.
+     *   Input Text (logical)  :   L0 L1 R2 R3 L4 L5
+     *   Input Text (visual)   :   L4 L5 R3 R2 L0 L1
+     *   Output :  [-2em, -1em, -1em, 0em, -3em, -2em, -4em, -3em, -6em, -5em, -5em, -4em]
+     *
+     * @param bounds the array to receive the character bounds data. Its length should be at least
+     *               2 times of the line length.
+     * @param advances the array to receive the character advance data, nullable. If provided, its
+     *                 length should be equal or larger than the line length.
+     *
+     * @throws IllegalArgumentException if the given {@code bounds} is null.
+     * @throws IndexOutOfBoundsException if the given {@code bounds} or {@code advances} doesn't
+     * have enough space to hold the result.
+     */
+    public void measureAllBounds(@NonNull float[] bounds, @Nullable float[] advances) {
+        if (bounds == null) {
+            throw new IllegalArgumentException("bounds can't be null");
+        }
+        if (bounds.length < 2 * mLen) {
+            throw new IndexOutOfBoundsException("bounds doesn't have enough space to receive the "
+                    + "result, needed: " + (2 * mLen) + " had: " + bounds.length);
+        }
+        if (advances == null) {
+            advances = new float[mLen];
+        }
+        if (advances.length < mLen) {
+            throw new IndexOutOfBoundsException("advance doesn't have enough space to receive the "
+                    + "result, needed: " + mLen + " had: " + advances.length);
+        }
+        float h = 0;
+        for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) {
+            final int runStart = mDirections.getRunStart(runIndex);
+            if (runStart > mLen) break;
+            final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
+            final boolean runIsRtl = mDirections.isRunRtl(runIndex);
+
+            int segStart = runStart;
+            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
+                if (j == runLimit || charAt(j) == TAB_CHAR) {
+                    final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
+
+                    final float segmentWidth =
+                            measureRun(segStart, j, j, runIsRtl, null, advances, segStart);
+
+                    final float oldh = h;
+                    h += sameDirection ? segmentWidth : -segmentWidth;
+                    float currh = sameDirection ? oldh : h;
+                    for (int offset = segStart; offset < j && offset < mLen; ++offset) {
+                        if (runIsRtl) {
+                            bounds[2 * offset + 1] = currh;
+                            currh -= advances[offset];
+                            bounds[2 * offset] = currh;
+                        } else {
+                            bounds[2 * offset] = currh;
+                            currh += advances[offset];
+                            bounds[2 * offset + 1] = currh;
+                        }
+                    }
+
+                    if (j != runLimit) {  // charAt(j) == TAB_CHAR
+                        final float leftX;
+                        final float rightX;
+                        if (runIsRtl) {
+                            rightX = h;
+                            h = mDir * nextTab(h * mDir);
+                            leftX = h;
+                        } else {
+                            leftX = h;
+                            h = mDir * nextTab(h * mDir);
+                            rightX = h;
+                        }
+                        bounds[2 * j] = leftX;
+                        bounds[2 * j + 1] = rightX;
+                        advances[j] = rightX - leftX;
+                    }
+
+                    segStart = j + 1;
+                }
+            }
+        }
+    }
+
+    /**
      * @see #measure(int, boolean, FontMetricsInt)
      * @return The measure results for all possible offsets
      */
@@ -464,15 +574,15 @@
                 if (j == runLimit || charAt(j) == TAB_CHAR) {
                     final  float oldh = h;
                     final boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
-                    final float w = measureRun(segStart, j, j, runIsRtl, fmi);
+                    final float w = measureRun(segStart, j, j, runIsRtl, fmi, null, 0);
                     h += advance ? w : -w;
 
                     final float baseh = advance ? oldh : h;
                     FontMetricsInt crtfmi = advance ? fmi : null;
                     for (int offset = segStart; offset <= j && offset <= mLen; ++offset) {
                         if (target[offset] >= segStart && target[offset] < j) {
-                            measurement[offset] =
-                                    baseh + measureRun(segStart, offset, j, runIsRtl, crtfmi);
+                            measurement[offset] = baseh
+                                    + measureRun(segStart, offset, j, runIsRtl, crtfmi, null, 0);
                         }
                     }
 
@@ -518,14 +628,14 @@
             boolean needWidth) {
 
         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, limit, limit, runIsRtl, null);
+            float w = -measureRun(start, limit, limit, runIsRtl, null, null, 0);
             handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
-                    y, bottom, null, false);
+                    y, bottom, null, false, null, 0);
             return w;
         }
 
         return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
-                y, bottom, null, needWidth);
+                y, bottom, null, needWidth, null, 0);
     }
 
     /**
@@ -538,12 +648,15 @@
      * @param runIsRtl true if the run is right-to-left
      * @param fmi receives metrics information about the requested
      * run, can be null.
+     * @param advances receives the advance information about the requested run, can be null.
+     * @param advancesIndex the start index to fill in the advance information.
      * @return the signed width from the start of the run to the leading edge
      * of the character at offset, based on the run (not paragraph) direction
      */
     private float measureRun(int start, int offset, int limit, boolean runIsRtl,
-            FontMetricsInt fmi) {
-        return handleRun(start, offset, limit, runIsRtl, null, null, 0, 0, 0, 0, fmi, true);
+            @Nullable FontMetricsInt fmi, @Nullable float[] advances, int advancesIndex) {
+        return handleRun(start, offset, limit, runIsRtl, null, null, 0, 0, 0, 0, fmi, true,
+                advances, advancesIndex);
     }
 
     /**
@@ -562,13 +675,14 @@
             int limit, boolean runIsRtl, float x, boolean needWidth) {
 
         if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
-            float w = -measureRun(start, limit, limit, runIsRtl, null);
-            handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, false);
+            float w = -measureRun(start, limit, limit, runIsRtl, null, null, 0);
+            handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null,
+                    false, null, 0);
             return w;
         }
 
         return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null,
-                needWidth);
+                needWidth, null, 0);
     }
 
 
@@ -908,14 +1022,16 @@
     }
 
     private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
-            boolean runIsRtl, int offset) {
+            boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex) {
         if (mCharsValid) {
-            return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
+            return wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
+                    runIsRtl, offset, advances, advancesIndex);
         } else {
             final int delta = mStart;
-            if (mComputed == null) {
-                return wp.getRunAdvance(mText, delta + start, delta + end,
-                        delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
+            if (mComputed == null || advances != null) {
+                return wp.getRunCharacterAdvance(mText, delta + start, delta + end,
+                        delta + contextStart, delta + contextEnd, runIsRtl,
+                        delta + offset, advances, advancesIndex);
             } else {
                 return mComputed.getWidth(start + delta, end + delta);
             }
@@ -940,6 +1056,8 @@
      * @param needWidth true if the width of the run is needed
      * @param offset the offset for the purpose of measuring
      * @param decorations the list of locations and paremeters for drawing decorations
+     * @param advances receives the advance information about the requested run, can be null.
+     * @param advancesIndex the start index to fill in the advance information.
      * @return the signed width of the run based on the run direction; only
      * valid if needWidth is true
      */
@@ -947,7 +1065,8 @@
             int contextStart, int contextEnd, boolean runIsRtl,
             Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom,
             FontMetricsInt fmi, boolean needWidth, int offset,
-            @Nullable ArrayList<DecorationInfo> decorations) {
+            @Nullable ArrayList<DecorationInfo> decorations,
+            @Nullable float[] advances, int advancesIndex) {
 
         if (mIsJustifying) {
             wp.setWordSpacing(mAddedWidthForJustify);
@@ -967,7 +1086,8 @@
         final int numDecorations = decorations == null ? 0 : decorations.size();
         if (needWidth || ((c != null || consumer != null) && (wp.bgColor != 0
                 || numDecorations != 0 || runIsRtl))) {
-            totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
+            totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset,
+                    advances, advancesIndex);
         }
 
         final float leftX, rightX;
@@ -1009,10 +1129,10 @@
 
                     final int decorationStart = Math.max(info.start, start);
                     final int decorationEnd = Math.min(info.end, offset);
-                    float decorationStartAdvance = getRunAdvance(
-                            wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
-                    float decorationEndAdvance = getRunAdvance(
-                            wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
+                    float decorationStartAdvance = getRunAdvance(wp, start, end, contextStart,
+                            contextEnd, runIsRtl, decorationStart, null, 0);
+                    float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart,
+                            contextEnd, runIsRtl, decorationEnd, null, 0);
                     final float decorationXLeft, decorationXRight;
                     if (runIsRtl) {
                         decorationXLeft = rightX - decorationEndAdvance;
@@ -1179,19 +1299,27 @@
      * @param bottom the bottom of the line
      * @param fmi receives metrics information, can be null
      * @param needWidth true if the width is required
+     * @param advances receives the advance information about the requested run, can be null.
+     * @param advancesIndex the start index to fill in the advance information.
      * @return the signed width of the run based on the run direction; only
      * valid if needWidth is true
      */
     private float handleRun(int start, int measureLimit,
             int limit, boolean runIsRtl, Canvas c,
             TextShaper.GlyphsConsumer consumer, float x, int top, int y,
-            int bottom, FontMetricsInt fmi, boolean needWidth) {
+            int bottom, FontMetricsInt fmi, boolean needWidth,
+            @Nullable float[] advances, int advancesIndex) {
 
         if (measureLimit < start || measureLimit > limit) {
             throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
                     + "start (" + start + ") and limit (" + limit + ") bounds");
         }
 
+        if (advances != null && advances.length - advancesIndex < measureLimit - start) {
+            throw new IndexOutOfBoundsException("advances doesn't have enough space to receive the "
+                    + "result");
+        }
+
         // Case of an empty line, make sure we update fmi according to mPaint
         if (start == measureLimit) {
             final TextPaint wp = mWorkPaint;
@@ -1218,7 +1346,7 @@
             wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
             wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
             return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top,
-                    y, bottom, fmi, needWidth, measureLimit, null);
+                    y, bottom, fmi, needWidth, measureLimit, null, advances, advancesIndex);
         }
 
         // Shaping needs to take into account context up to metric boundaries,
@@ -1257,8 +1385,16 @@
             }
 
             if (replacement != null) {
-                x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
-                        bottom, fmi, needWidth || mlimit < measureLimit);
+                final float width = handleReplacement(replacement, wp, i, mlimit, runIsRtl, c,
+                        x, top, y, bottom, fmi, needWidth || mlimit < measureLimit);
+                x += width;
+                if (advances != null) {
+                    // For replacement, the entire width is assigned to the first character.
+                    advances[advancesIndex + i - start] = runIsRtl ? -width : width;
+                    for (int j = i + 1; j < mlimit; ++j) {
+                        advances[advancesIndex + j - start] = 0.0f;
+                    }
+                }
                 continue;
             }
 
@@ -1300,7 +1436,8 @@
                             adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
                     x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c,
                             consumer, x, top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
-                            Math.min(activeEnd, mlimit), mDecorations);
+                            Math.min(activeEnd, mlimit), mDecorations,
+                            advances, advancesIndex + activeStart - start);
 
                     activeStart = j;
                     activePaint.set(wp);
@@ -1327,7 +1464,8 @@
                     adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
             x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x,
                     top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
-                    Math.min(activeEnd, mlimit), mDecorations);
+                    Math.min(activeEnd, mlimit), mDecorations,
+                    advances, advancesIndex + activeStart - start);
         }
 
         return x - originalX;
diff --git a/core/tests/coretests/src/android/text/TextLineTest.java b/core/tests/coretests/src/android/text/TextLineTest.java
index 412d6ec..e3bcc8d 100644
--- a/core/tests/coretests/src/android/text/TextLineTest.java
+++ b/core/tests/coretests/src/android/text/TextLineTest.java
@@ -26,6 +26,7 @@
 import android.graphics.Typeface;
 import android.platform.test.annotations.Presubmit;
 import android.text.Layout.TabStops;
+import android.text.style.AbsoluteSizeSpan;
 import android.text.style.ReplacementSpan;
 import android.text.style.TabStopSpan;
 
@@ -97,7 +98,7 @@
             InstrumentationRegistry.getInstrumentation().getTargetContext().getAssets(),
             "fonts/StaticLayoutLineBreakingTestFont.ttf");
 
-    private TextLine getTextLine(String str, TextPaint paint, TabStops tabStops) {
+    private TextLine getTextLine(CharSequence str, TextPaint paint, TabStops tabStops) {
         Layout layout =
                 StaticLayout.Builder.obtain(str, 0, str.length(), paint, Integer.MAX_VALUE)
                     .build();
@@ -109,7 +110,7 @@
         return tl;
     }
 
-    private TextLine getTextLine(String str, TextPaint paint) {
+    private TextLine getTextLine(CharSequence str, TextPaint paint) {
         return getTextLine(str, paint, null);
     }
 
@@ -316,14 +317,275 @@
         assertTrue(span.mIsUsed);
     }
 
+    @Test
+    public void testMeasureAllBounds_LTR() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("IIIIIV", paint);
+        float[] bounds = new float[12];
+        float[] advances = new float[6];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 20.0f, 20.0f, 30.0f, 30.0f, 40.0f,
+                40.0f, 50.0f, 50.0f, 100.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 50.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_LTR_StyledText() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+        SpannableString text = new SpannableString("IIIIIV");
+        text.setSpan(new AbsoluteSizeSpan(5), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        TextLine tl = getTextLine(text, paint);
+        float[] bounds = new float[12];
+        float[] advances = new float[6];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 15.0f, 15.0f, 20.0f, 20.0f, 30.0f,
+                30.0f, 40.0f, 40.0f, 90.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 5.0f, 5.0f, 10.0f, 10.0f, 50.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_RTL() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("\u05D0\u05D0\u05D0\u05D0\u05D0\u05D1", paint);
+        float[] bounds = new float[12];
+        float[] advances = new float[6];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {-10.0f, 0.0f, -20.0f, -10.0f, -30.0f, -20.0f, -40.0f, -30.0f,
+                -50.0f, -40.0f, -100.0f, -50.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 50.0f}, advances, 0.0f);
+    }
+
+
+    @Test
+    public void testMeasureAllBounds_RTL_StyledText() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+        SpannableString text = new SpannableString("\u05D0\u05D0\u05D0\u05D0\u05D0\u05D1");
+        text.setSpan(new AbsoluteSizeSpan(5), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        TextLine tl = getTextLine(text, paint);
+        float[] bounds = new float[12];
+        float[] advances = new float[6];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {-10.0f, 0.0f, -15.0f, -10.0f, -20.0f, -15.0f,
+                -30.0f, -20.0f, -40.0f, -30.0f, -90.0f, -40.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 5.0f, 5.0f, 10.0f, 10.0f, 50.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_BiDi() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("II\u05D0\u05D0II", paint);
+        float[] bounds = new float[12];
+        float[] advances = new float[6];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 20.0f, 30.0f, 40.0f, 20.0f, 30.0f,
+                40.0f, 50.0f, 50.0f, 60.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_BiDi2() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("I" + RLI + "I\u05D0\u05D0" + PDI + "I", paint);
+        float[] bounds = new float[14];
+        float[] advances = new float[7];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 10.0f, 30.0f, 40.0f, 20.0f, 30.0f,
+                10.0f, 20.0f, 40.0f, 40.0f, 40.0f, 50.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 0.0f, 10.0f, 10.0f, 10.0f, 0.0f, 10.0f}, advances,
+                0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_BiDi3() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("\u05D0" + LRI + "\u05D0II" + PDI + "\u05D0", paint);
+        float[] bounds = new float[14];
+        float[] advances = new float[7];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {-10.0f, 0.0f, -10.0f, -10.0f, -40.0f, -30.0f,
+                -30.0f, -20.0f, -20.0f, -10.0f, -40.0f, -40.0f, -50.0f, -40.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 0.0f, 10.0f, 10.0f, 10.0f, 0.0f, 10.0f}, advances,
+                0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_styled_BiDi() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        SpannableString text = new SpannableString("II\u05D0\u05D0II");
+        text.setSpan(new AbsoluteSizeSpan(5), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        TextLine tl = getTextLine(text, paint);
+        float[] bounds = new float[12];
+        float[] advances = new float[6];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 15.0f, 25.0f, 30.0f,
+                15.0f, 25.0f, 30.0f, 40.0f, 40.0f, 50.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 5.0f, 5.0f, 10.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_Tab_LTR() {
+        final Object[] spans = { new TabStopSpan.Standard(100) };
+        final TabStops stops = new TabStops(100, spans);
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("II\tII", paint, stops);
+        float[] bounds = new float[10];
+        float[] advances = new float[5];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 20.0f, 20.0f, 100.0f, 100.0f, 110.0f,
+                110.0f, 120.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 10.0f, 80.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_Tab_RTL() {
+        final Object[] spans = { new TabStopSpan.Standard(100) };
+        final TabStops stops = new TabStops(100, spans);
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("\u05D0\u05D0\t\u05D0\u05D0", paint, stops);
+        float[] bounds = new float[10];
+        float[] advances = new float[5];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {-10.0f, 0.0f, -20.0f, -10.0f, -100.0f, -20.0f,
+                -110.0f, -100.0f, -120.0f, -110.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 10.0f, 80.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_Tab_BiDi() {
+        final Object[] spans = { new TabStopSpan.Standard(100) };
+        final TabStops stops = new TabStops(100, spans);
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("I\u05D0\tI\u05D0", paint, stops);
+        float[] bounds = new float[10];
+        float[] advances = new float[5];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 20.0f, 20.0f, 100.0f,
+                100.0f, 110.0f, 110.0f, 120.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 10.0f, 80.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_Tab_BiDi2() {
+        final Object[] spans = { new TabStopSpan.Standard(100) };
+        final TabStops stops = new TabStops(100, spans);
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        TextLine tl = getTextLine("\u05D0I\t\u05D0I", paint, stops);
+        float[] bounds = new float[10];
+        float[] advances = new float[5];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {-10.0f, 0.0f, -20.0f, -10.0f, -100.0f, -20.0f,
+                -110.0f, -100.0f, -120.0f, -110.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 10.0f, 80.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_replacement_LTR() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        SpannableString text = new SpannableString("IIIII");
+        text.setSpan(new TestReplacementSpan(5), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        TextLine tl = getTextLine(text, paint);
+        float[] bounds = new float[10];
+        float[] advances = new float[5];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 15.0f, 15.0f, 15.0f,
+                15.0f, 25.0f, 25.0f, 35.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 5.0f, 0.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_replacement_RTL() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        SpannableString text = new SpannableString("\u05D0\u05D0\u05D0\u05D0\u05D0");
+        text.setSpan(new TestReplacementSpan(5), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        TextLine tl = getTextLine(text, paint);
+        float[] bounds = new float[10];
+        float[] advances = new float[5];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {-10.0f, 0.0f, -15.0f, -10.0f, -15.0f, -15.0f,
+                -25.0f, -15.0f, -35.0f, -25.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 5.0f, 0.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
+    @Test
+    public void testMeasureAllBounds_replacement_BiDi() {
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(TYPEFACE);
+        paint.setTextSize(10.0f);  // make 1em = 10px
+
+        SpannableString text = new SpannableString("II\u05D0\u05D0II");
+        text.setSpan(new TestReplacementSpan(5), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        TextLine tl = getTextLine(text, paint);
+        float[] bounds = new float[12];
+        float[] advances = new float[6];
+        tl.measureAllBounds(bounds, advances);
+        assertArrayEquals(new float[] {0.0f, 10.0f, 10.0f, 15.0f, 15.0f, 15.0f,
+                15.0f, 25.0f, 25.0f, 35.0f, 35.0f, 45.0f}, bounds, 0.0f);
+        assertArrayEquals(new float[] {10.0f, 5.0f, 0.0f, 10.0f, 10.0f, 10.0f}, advances, 0.0f);
+    }
+
     private static class TestReplacementSpan extends ReplacementSpan {
         boolean mIsUsed;
+        private final int mWidth;
+
+        TestReplacementSpan() {
+            mWidth = 0;
+        }
+
+        TestReplacementSpan(int width) {
+            mWidth = width;
+        }
 
         @Override
         public int getSize(Paint paint, CharSequence text, int start, int end,
                 Paint.FontMetricsInt fm) {
             mIsUsed = true;
-            return 0;
+            return mWidth;
         }
 
         @Override
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 451b99e..1a80ab3 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -3136,6 +3136,128 @@
         return result;
     }
 
+
+    /**
+     * Measure the advance of each character within a run of text and also return the cursor
+     * position within the run.
+     *
+     * @see #getRunAdvance(char[], int, int, int, int, boolean, int) for more details.
+     *
+     * @param text the text to measure. Cannot be null.
+     * @param start the index of the start of the range to measure
+     * @param end the index + 1 of the end of the range to measure
+     * @param contextStart the index of the start of the shaping context
+     * @param contextEnd the index + 1 of the end of the shaping context
+     * @param isRtl whether the run is in RTL direction
+     * @param offset index of caret position
+     * @param advances the array that receives the computed character advances
+     * @param advancesIndex the start index from which the advances array is filled
+     * @return width measurement between start and offset
+     * @throws IndexOutOfBoundsException if a) contextStart or contextEnd is out of array's range
+     * or contextStart is larger than contextEnd,
+     * b) start or end is not within the range [contextStart, contextEnd), or start is larger than
+     * end,
+     * c) offset is not within the range [start, end),
+     * d) advances.length - advanceIndex is smaller than the length of the run, which equals to
+     * end - start.
+     *
+     */
+    public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart,
+            int contextEnd, boolean isRtl, int offset,
+            @Nullable float[] advances, int advancesIndex) {
+        if (text == null) {
+            throw new IllegalArgumentException("text cannot be null");
+        }
+        if (contextStart < 0 || contextEnd > text.length) {
+            throw new IndexOutOfBoundsException("Invalid Context Range: " + contextStart + ", "
+                    + contextEnd + " must be in 0, " + text.length);
+        }
+
+        if (start < contextStart || contextEnd < end) {
+            throw new IndexOutOfBoundsException("Invalid start/end range: " + start + ", " + end
+                    + " must be in " + contextStart + ", " + contextEnd);
+        }
+
+        if (offset < start || end < offset) {
+            throw new IndexOutOfBoundsException("Invalid offset position: " + offset
+                    + " must be in " + start + ", " + end);
+        }
+
+        if (advances != null && advances.length < advancesIndex - start + end) {
+            throw new IndexOutOfBoundsException("Given array doesn't have enough space to receive "
+                    + "the result, advances.length: " + advances.length + " advanceIndex: "
+                    + advancesIndex + " needed space: " + (offset - start));
+        }
+
+        if (end == start) {
+            return 0.0f;
+        }
+
+        return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
+                isRtl, offset, advances, advancesIndex);
+    }
+
+    /**
+     * @see #getRunCharacterAdvance(char[], int, int, int, int, boolean, int, float[], int)
+     *
+     * @param text the text to measure. Cannot be null.
+     * @param start the index of the start of the range to measure
+     * @param end the index + 1 of the end of the range to measure
+     * @param contextStart the index of the start of the shaping context
+     * @param contextEnd the index + 1 of the end of the shaping context
+     * @param isRtl whether the run is in RTL direction
+     * @param offset index of caret position
+     * @param advances the array that receives the computed character advances
+     * @param advancesIndex the start index from which the advances array is filled
+     * @return width measurement between start and offset
+     * @throws IndexOutOfBoundsException if a) contextStart or contextEnd is out of array's range
+     * or contextStart is larger than contextEnd,
+     * b) start or end is not within the range [contextStart, contextEnd), or end is larger than
+     * start,
+     * c) offset is not within the range [start, end),
+     * d) advances.length - advanceIndex is smaller than the run length, which equals to
+     * end - start.
+     */
+    public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end,
+            int contextStart, int contextEnd, boolean isRtl, int offset,
+            @Nullable float[] advances, int advancesIndex) {
+        if (text == null) {
+            throw new IllegalArgumentException("text cannot be null");
+        }
+        if (contextStart < 0 || contextEnd > text.length()) {
+            throw new IndexOutOfBoundsException("Invalid Context Range: " + contextStart + ", "
+                    + contextEnd + " must be in 0, " + text.length());
+        }
+
+        if (start < contextStart || contextEnd < end) {
+            throw new IndexOutOfBoundsException("Invalid start/end range: " + start + ", " + end
+                    + " must be in " + contextStart + ", " + contextEnd);
+        }
+
+        if (offset < start || end < offset) {
+            throw new IndexOutOfBoundsException("Invalid offset position: " + offset
+                    + " must be in " + start + ", " + end);
+        }
+
+        if (advances != null && advances.length < advancesIndex - start + end) {
+            throw new IndexOutOfBoundsException("Given array doesn't have enough space to receive "
+                    + "the result, advances.length: " + advances.length + " advanceIndex: "
+                    + advancesIndex + " needed space: " + (offset - start));
+        }
+
+        if (end == start) {
+            return 0.0f;
+        }
+
+        char[] buf = TemporaryBuffer.obtain(contextEnd - contextStart);
+        TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+        final float result = getRunCharacterAdvance(buf, start - contextStart, end - contextStart,
+                0, contextEnd - contextStart, isRtl, offset - contextStart,
+                advances, advancesIndex);
+        TemporaryBuffer.recycle(buf);
+        return result;
+    }
+
     /**
      * Get the character offset within the string whose position is closest to the specified
      * horizontal position.
@@ -3247,6 +3369,9 @@
     private static native boolean nHasGlyph(long paintPtr, int bidiFlags, String string);
     private static native float nGetRunAdvance(long paintPtr, char[] text, int start, int end,
             int contextStart, int contextEnd, boolean isRtl, int offset);
+    private static native float nGetRunCharacterAdvance(long paintPtr, char[] text, int start,
+            int end, int contextStart, int contextEnd, boolean isRtl, int offset, float[] advances,
+            int advancesIndex);
     private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end,
             int contextStart, int contextEnd, boolean isRtl, float advance);
     private static native void nGetFontMetricsIntForText(long paintPtr, char[] text,
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 0aa1465..ed453b1 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -497,16 +497,29 @@
         return true;
     }
 
-    static jfloat doRunAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[],
-            jint start, jint count, jint bufSize, jboolean isRtl, jint offset) {
+    static jfloat doRunAdvance(JNIEnv* env, const Paint* paint, const Typeface* typeface,
+                               const jchar buf[], jint start, jint count, jint bufSize,
+                               jboolean isRtl, jint offset, jfloatArray advances,
+                               jint advancesIndex) {
+        if (advances) {
+            size_t advancesLength = env->GetArrayLength(advances);
+            if ((size_t)(count + advancesIndex) > advancesLength) {
+                doThrowAIOOBE(env);
+                return 0;
+            }
+        }
         minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
-        if (offset == start + count) {
+        if (offset == start + count && advances == nullptr) {
             return MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
                     bufSize, nullptr);
         }
         std::unique_ptr<float[]> advancesArray(new float[count]);
         MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
                 advancesArray.get());
+
+        if (advances) {
+            env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get());
+        }
         return minikin::getRunAdvance(advancesArray.get(), buf, start, count, offset);
     }
 
@@ -515,9 +528,23 @@
         const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
         const Typeface* typeface = paint->getAndroidTypeface();
         ScopedCharArrayRO textArray(env, text);
-        jfloat result = doRunAdvance(paint, typeface, textArray.get() + contextStart,
-                start - contextStart, end - start, contextEnd - contextStart, isRtl,
-                offset - contextStart);
+        jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
+                                     start - contextStart, end - start, contextEnd - contextStart,
+                                     isRtl, offset - contextStart, nullptr, 0);
+        return result;
+    }
+
+    static jfloat getRunCharacterAdvance___CIIIIZI_FI_F(JNIEnv* env, jclass, jlong paintHandle,
+                                                        jcharArray text, jint start, jint end,
+                                                        jint contextStart, jint contextEnd,
+                                                        jboolean isRtl, jint offset,
+                                                        jfloatArray advances, jint advancesIndex) {
+        const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+        const Typeface* typeface = paint->getAndroidTypeface();
+        ScopedCharArrayRO textArray(env, text);
+        jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
+                                     start - contextStart, end - start, contextEnd - contextStart,
+                                     isRtl, offset - contextStart, advances, advancesIndex);
         return result;
     }
 
@@ -1034,113 +1061,112 @@
 }; // namespace PaintGlue
 
 static const JNINativeMethod methods[] = {
-    {"nGetNativeFinalizer", "()J", (void*) PaintGlue::getNativeFinalizer},
-    {"nInit","()J", (void*) PaintGlue::init},
-    {"nInitWithPaint","(J)J", (void*) PaintGlue::initWithPaint},
-    {"nBreakText","(J[CIIFI[F)I", (void*) PaintGlue::breakTextC},
-    {"nBreakText","(JLjava/lang/String;ZFI[F)I", (void*) PaintGlue::breakTextS},
-    {"nGetTextAdvances","(J[CIIIII[FI)F",
-            (void*) PaintGlue::getTextAdvances___CIIIII_FI},
-    {"nGetTextAdvances","(JLjava/lang/String;IIIII[FI)F",
-            (void*) PaintGlue::getTextAdvances__StringIIIII_FI},
+        {"nGetNativeFinalizer", "()J", (void*)PaintGlue::getNativeFinalizer},
+        {"nInit", "()J", (void*)PaintGlue::init},
+        {"nInitWithPaint", "(J)J", (void*)PaintGlue::initWithPaint},
+        {"nBreakText", "(J[CIIFI[F)I", (void*)PaintGlue::breakTextC},
+        {"nBreakText", "(JLjava/lang/String;ZFI[F)I", (void*)PaintGlue::breakTextS},
+        {"nGetTextAdvances", "(J[CIIIII[FI)F", (void*)PaintGlue::getTextAdvances___CIIIII_FI},
+        {"nGetTextAdvances", "(JLjava/lang/String;IIIII[FI)F",
+         (void*)PaintGlue::getTextAdvances__StringIIIII_FI},
 
-    {"nGetTextRunCursor", "(J[CIIIII)I", (void*) PaintGlue::getTextRunCursor___C},
-    {"nGetTextRunCursor", "(JLjava/lang/String;IIIII)I",
-            (void*) PaintGlue::getTextRunCursor__String},
-    {"nGetTextPath", "(JI[CIIFFJ)V", (void*) PaintGlue::getTextPath___C},
-    {"nGetTextPath", "(JILjava/lang/String;IIFFJ)V", (void*) PaintGlue::getTextPath__String},
-    {"nGetStringBounds", "(JLjava/lang/String;IIILandroid/graphics/Rect;)V",
-            (void*) PaintGlue::getStringBounds },
-    {"nGetCharArrayBounds", "(J[CIIILandroid/graphics/Rect;)V",
-            (void*) PaintGlue::getCharArrayBounds },
-    {"nHasGlyph", "(JILjava/lang/String;)Z", (void*) PaintGlue::hasGlyph },
-    {"nGetRunAdvance", "(J[CIIIIZI)F", (void*) PaintGlue::getRunAdvance___CIIIIZI_F},
-    {"nGetOffsetForAdvance", "(J[CIIIIZF)I",
-            (void*) PaintGlue::getOffsetForAdvance___CIIIIZF_I},
-    {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
-      (void*)PaintGlue::getFontMetricsIntForText___C},
-    {"nGetFontMetricsIntForText",
-      "(JLjava/lang/String;IIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
-      (void*)PaintGlue::getFontMetricsIntForText___String},
+        {"nGetTextRunCursor", "(J[CIIIII)I", (void*)PaintGlue::getTextRunCursor___C},
+        {"nGetTextRunCursor", "(JLjava/lang/String;IIIII)I",
+         (void*)PaintGlue::getTextRunCursor__String},
+        {"nGetTextPath", "(JI[CIIFFJ)V", (void*)PaintGlue::getTextPath___C},
+        {"nGetTextPath", "(JILjava/lang/String;IIFFJ)V", (void*)PaintGlue::getTextPath__String},
+        {"nGetStringBounds", "(JLjava/lang/String;IIILandroid/graphics/Rect;)V",
+         (void*)PaintGlue::getStringBounds},
+        {"nGetCharArrayBounds", "(J[CIIILandroid/graphics/Rect;)V",
+         (void*)PaintGlue::getCharArrayBounds},
+        {"nHasGlyph", "(JILjava/lang/String;)Z", (void*)PaintGlue::hasGlyph},
+        {"nGetRunAdvance", "(J[CIIIIZI)F", (void*)PaintGlue::getRunAdvance___CIIIIZI_F},
+        {"nGetRunCharacterAdvance", "(J[CIIIIZI[FI)F",
+         (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
+        {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
+        {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
+         (void*)PaintGlue::getFontMetricsIntForText___C},
+        {"nGetFontMetricsIntForText",
+         "(JLjava/lang/String;IIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
+         (void*)PaintGlue::getFontMetricsIntForText___String},
 
-    // --------------- @FastNative ----------------------
+        // --------------- @FastNative ----------------------
 
-    {"nSetTextLocales","(JLjava/lang/String;)I", (void*) PaintGlue::setTextLocales},
-    {"nSetFontFeatureSettings","(JLjava/lang/String;)V",
-                (void*) PaintGlue::setFontFeatureSettings},
-    {"nGetFontMetrics", "(JLandroid/graphics/Paint$FontMetrics;)F",
-                (void*)PaintGlue::getFontMetrics},
-    {"nGetFontMetricsInt", "(JLandroid/graphics/Paint$FontMetricsInt;)I",
-            (void*)PaintGlue::getFontMetricsInt},
+        {"nSetTextLocales", "(JLjava/lang/String;)I", (void*)PaintGlue::setTextLocales},
+        {"nSetFontFeatureSettings", "(JLjava/lang/String;)V",
+         (void*)PaintGlue::setFontFeatureSettings},
+        {"nGetFontMetrics", "(JLandroid/graphics/Paint$FontMetrics;)F",
+         (void*)PaintGlue::getFontMetrics},
+        {"nGetFontMetricsInt", "(JLandroid/graphics/Paint$FontMetricsInt;)I",
+         (void*)PaintGlue::getFontMetricsInt},
 
-    // --------------- @CriticalNative ------------------
+        // --------------- @CriticalNative ------------------
 
-    {"nReset","(J)V", (void*) PaintGlue::reset},
-    {"nSet","(JJ)V", (void*) PaintGlue::assign},
-    {"nGetFlags","(J)I", (void*) PaintGlue::getFlags},
-    {"nSetFlags","(JI)V", (void*) PaintGlue::setFlags},
-    {"nGetHinting","(J)I", (void*) PaintGlue::getHinting},
-    {"nSetHinting","(JI)V", (void*) PaintGlue::setHinting},
-    {"nSetAntiAlias","(JZ)V", (void*) PaintGlue::setAntiAlias},
-    {"nSetSubpixelText","(JZ)V", (void*) PaintGlue::setSubpixelText},
-    {"nSetLinearText","(JZ)V", (void*) PaintGlue::setLinearText},
-    {"nSetUnderlineText","(JZ)V", (void*) PaintGlue::setUnderlineText},
-    {"nSetStrikeThruText","(JZ)V", (void*) PaintGlue::setStrikeThruText},
-    {"nSetFakeBoldText","(JZ)V", (void*) PaintGlue::setFakeBoldText},
-    {"nSetFilterBitmap","(JZ)V", (void*) PaintGlue::setFilterBitmap},
-    {"nSetDither","(JZ)V", (void*) PaintGlue::setDither},
-    {"nGetStyle","(J)I", (void*) PaintGlue::getStyle},
-    {"nSetStyle","(JI)V", (void*) PaintGlue::setStyle},
-    {"nSetColor","(JI)V", (void*) PaintGlue::setColor},
-    {"nSetColor","(JJJ)V", (void*) PaintGlue::setColorLong},
-    {"nSetAlpha","(JI)V", (void*) PaintGlue::setAlpha},
-    {"nGetStrokeWidth","(J)F", (void*) PaintGlue::getStrokeWidth},
-    {"nSetStrokeWidth","(JF)V", (void*) PaintGlue::setStrokeWidth},
-    {"nGetStrokeMiter","(J)F", (void*) PaintGlue::getStrokeMiter},
-    {"nSetStrokeMiter","(JF)V", (void*) PaintGlue::setStrokeMiter},
-    {"nGetStrokeCap","(J)I", (void*) PaintGlue::getStrokeCap},
-    {"nSetStrokeCap","(JI)V", (void*) PaintGlue::setStrokeCap},
-    {"nGetStrokeJoin","(J)I", (void*) PaintGlue::getStrokeJoin},
-    {"nSetStrokeJoin","(JI)V", (void*) PaintGlue::setStrokeJoin},
-    {"nGetFillPath","(JJJ)Z", (void*) PaintGlue::getFillPath},
-    {"nSetShader","(JJ)J", (void*) PaintGlue::setShader},
-    {"nSetColorFilter","(JJ)J", (void*) PaintGlue::setColorFilter},
-    {"nSetXfermode","(JI)V", (void*) PaintGlue::setXfermode},
-    {"nSetPathEffect","(JJ)J", (void*) PaintGlue::setPathEffect},
-    {"nSetMaskFilter","(JJ)J", (void*) PaintGlue::setMaskFilter},
-    {"nSetTypeface","(JJ)V", (void*) PaintGlue::setTypeface},
-    {"nGetTextAlign","(J)I", (void*) PaintGlue::getTextAlign},
-    {"nSetTextAlign","(JI)V", (void*) PaintGlue::setTextAlign},
-    {"nSetTextLocalesByMinikinLocaleListId","(JI)V",
-            (void*) PaintGlue::setTextLocalesByMinikinLocaleListId},
-    {"nIsElegantTextHeight","(J)Z", (void*) PaintGlue::isElegantTextHeight},
-    {"nSetElegantTextHeight","(JZ)V", (void*) PaintGlue::setElegantTextHeight},
-    {"nGetTextSize","(J)F", (void*) PaintGlue::getTextSize},
-    {"nSetTextSize","(JF)V", (void*) PaintGlue::setTextSize},
-    {"nGetTextScaleX","(J)F", (void*) PaintGlue::getTextScaleX},
-    {"nSetTextScaleX","(JF)V", (void*) PaintGlue::setTextScaleX},
-    {"nGetTextSkewX","(J)F", (void*) PaintGlue::getTextSkewX},
-    {"nSetTextSkewX","(JF)V", (void*) PaintGlue::setTextSkewX},
-    {"nGetLetterSpacing","(J)F", (void*) PaintGlue::getLetterSpacing},
-    {"nSetLetterSpacing","(JF)V", (void*) PaintGlue::setLetterSpacing},
-    {"nGetWordSpacing","(J)F", (void*) PaintGlue::getWordSpacing},
-    {"nSetWordSpacing","(JF)V", (void*) PaintGlue::setWordSpacing},
-    {"nGetStartHyphenEdit", "(J)I", (void*) PaintGlue::getStartHyphenEdit},
-    {"nGetEndHyphenEdit", "(J)I", (void*) PaintGlue::getEndHyphenEdit},
-    {"nSetStartHyphenEdit", "(JI)V", (void*) PaintGlue::setStartHyphenEdit},
-    {"nSetEndHyphenEdit", "(JI)V", (void*) PaintGlue::setEndHyphenEdit},
-    {"nAscent","(J)F", (void*) PaintGlue::ascent},
-    {"nDescent","(J)F", (void*) PaintGlue::descent},
-    {"nGetUnderlinePosition","(J)F", (void*) PaintGlue::getUnderlinePosition},
-    {"nGetUnderlineThickness","(J)F", (void*) PaintGlue::getUnderlineThickness},
-    {"nGetStrikeThruPosition","(J)F", (void*) PaintGlue::getStrikeThruPosition},
-    {"nGetStrikeThruThickness","(J)F", (void*) PaintGlue::getStrikeThruThickness},
-    {"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer},
-    {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer},
-    {"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement},
+        {"nReset", "(J)V", (void*)PaintGlue::reset},
+        {"nSet", "(JJ)V", (void*)PaintGlue::assign},
+        {"nGetFlags", "(J)I", (void*)PaintGlue::getFlags},
+        {"nSetFlags", "(JI)V", (void*)PaintGlue::setFlags},
+        {"nGetHinting", "(J)I", (void*)PaintGlue::getHinting},
+        {"nSetHinting", "(JI)V", (void*)PaintGlue::setHinting},
+        {"nSetAntiAlias", "(JZ)V", (void*)PaintGlue::setAntiAlias},
+        {"nSetSubpixelText", "(JZ)V", (void*)PaintGlue::setSubpixelText},
+        {"nSetLinearText", "(JZ)V", (void*)PaintGlue::setLinearText},
+        {"nSetUnderlineText", "(JZ)V", (void*)PaintGlue::setUnderlineText},
+        {"nSetStrikeThruText", "(JZ)V", (void*)PaintGlue::setStrikeThruText},
+        {"nSetFakeBoldText", "(JZ)V", (void*)PaintGlue::setFakeBoldText},
+        {"nSetFilterBitmap", "(JZ)V", (void*)PaintGlue::setFilterBitmap},
+        {"nSetDither", "(JZ)V", (void*)PaintGlue::setDither},
+        {"nGetStyle", "(J)I", (void*)PaintGlue::getStyle},
+        {"nSetStyle", "(JI)V", (void*)PaintGlue::setStyle},
+        {"nSetColor", "(JI)V", (void*)PaintGlue::setColor},
+        {"nSetColor", "(JJJ)V", (void*)PaintGlue::setColorLong},
+        {"nSetAlpha", "(JI)V", (void*)PaintGlue::setAlpha},
+        {"nGetStrokeWidth", "(J)F", (void*)PaintGlue::getStrokeWidth},
+        {"nSetStrokeWidth", "(JF)V", (void*)PaintGlue::setStrokeWidth},
+        {"nGetStrokeMiter", "(J)F", (void*)PaintGlue::getStrokeMiter},
+        {"nSetStrokeMiter", "(JF)V", (void*)PaintGlue::setStrokeMiter},
+        {"nGetStrokeCap", "(J)I", (void*)PaintGlue::getStrokeCap},
+        {"nSetStrokeCap", "(JI)V", (void*)PaintGlue::setStrokeCap},
+        {"nGetStrokeJoin", "(J)I", (void*)PaintGlue::getStrokeJoin},
+        {"nSetStrokeJoin", "(JI)V", (void*)PaintGlue::setStrokeJoin},
+        {"nGetFillPath", "(JJJ)Z", (void*)PaintGlue::getFillPath},
+        {"nSetShader", "(JJ)J", (void*)PaintGlue::setShader},
+        {"nSetColorFilter", "(JJ)J", (void*)PaintGlue::setColorFilter},
+        {"nSetXfermode", "(JI)V", (void*)PaintGlue::setXfermode},
+        {"nSetPathEffect", "(JJ)J", (void*)PaintGlue::setPathEffect},
+        {"nSetMaskFilter", "(JJ)J", (void*)PaintGlue::setMaskFilter},
+        {"nSetTypeface", "(JJ)V", (void*)PaintGlue::setTypeface},
+        {"nGetTextAlign", "(J)I", (void*)PaintGlue::getTextAlign},
+        {"nSetTextAlign", "(JI)V", (void*)PaintGlue::setTextAlign},
+        {"nSetTextLocalesByMinikinLocaleListId", "(JI)V",
+         (void*)PaintGlue::setTextLocalesByMinikinLocaleListId},
+        {"nIsElegantTextHeight", "(J)Z", (void*)PaintGlue::isElegantTextHeight},
+        {"nSetElegantTextHeight", "(JZ)V", (void*)PaintGlue::setElegantTextHeight},
+        {"nGetTextSize", "(J)F", (void*)PaintGlue::getTextSize},
+        {"nSetTextSize", "(JF)V", (void*)PaintGlue::setTextSize},
+        {"nGetTextScaleX", "(J)F", (void*)PaintGlue::getTextScaleX},
+        {"nSetTextScaleX", "(JF)V", (void*)PaintGlue::setTextScaleX},
+        {"nGetTextSkewX", "(J)F", (void*)PaintGlue::getTextSkewX},
+        {"nSetTextSkewX", "(JF)V", (void*)PaintGlue::setTextSkewX},
+        {"nGetLetterSpacing", "(J)F", (void*)PaintGlue::getLetterSpacing},
+        {"nSetLetterSpacing", "(JF)V", (void*)PaintGlue::setLetterSpacing},
+        {"nGetWordSpacing", "(J)F", (void*)PaintGlue::getWordSpacing},
+        {"nSetWordSpacing", "(JF)V", (void*)PaintGlue::setWordSpacing},
+        {"nGetStartHyphenEdit", "(J)I", (void*)PaintGlue::getStartHyphenEdit},
+        {"nGetEndHyphenEdit", "(J)I", (void*)PaintGlue::getEndHyphenEdit},
+        {"nSetStartHyphenEdit", "(JI)V", (void*)PaintGlue::setStartHyphenEdit},
+        {"nSetEndHyphenEdit", "(JI)V", (void*)PaintGlue::setEndHyphenEdit},
+        {"nAscent", "(J)F", (void*)PaintGlue::ascent},
+        {"nDescent", "(J)F", (void*)PaintGlue::descent},
+        {"nGetUnderlinePosition", "(J)F", (void*)PaintGlue::getUnderlinePosition},
+        {"nGetUnderlineThickness", "(J)F", (void*)PaintGlue::getUnderlineThickness},
+        {"nGetStrikeThruPosition", "(J)F", (void*)PaintGlue::getStrikeThruPosition},
+        {"nGetStrikeThruThickness", "(J)F", (void*)PaintGlue::getStrikeThruThickness},
+        {"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer},
+        {"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer},
+        {"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement},
 };
 
-
 int register_android_graphics_Paint(JNIEnv* env) {
     return RegisterMethodsOrDie(env, "android/graphics/Paint", methods, NELEM(methods));
 }