Refactor MeasuredText

This is 2nd attempt of I58d3020a3fa560d05576e18888fbfe46e2975e8f

The root cause of the crash is passing end offset instead of passing
length. This CL contains that fix and also has a test case for that.

This refactoring contains:
- Add lots of comments.
- Mark private the internal fields and introduce accessors and helper
  methods.
- Factor out the auto grow array implementation to another class.
- Use SynchronizedPool for pool implementation.
- Introduce three build methods for each use case.
- Hide addStyleRun and compute all necessary informations in build method.

Locally verified that this doesn't cause performance regressions.
Here is a raw performance test result on walleye-userdebug.

StaticLayoutPerfTest (median, N=100):
createRandom:          7,846,449 -> 8,003,903 (+2.01%)
createRandom Balanced: 7,810,436 -> 7,919,200 (+1.40%)

TextViewOnMeasurePerfTest (median, N=100):
measure_AtMost:       94,276,376 ->  94,124,658 (-0.16%)
measure_Exactly:      91,629,352 ->  91,617,639 (-0.01%)
measure_Unspecified: 151,006,181 -> 150,957,598 (-0.03%)

Test: bit CtsTextTestCases:*
Test: bit CtsGraphicsTestCases:*
Test: bit CtsWidgetTestCases:*
Test: bit FrameworksCoreTests:android.text.StaticLayoutTest
Bug: 65024629
Bug: 70146381
Change-Id: I772f60444d0fe87f59609977af5ef712cab3eb37
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index c0fc44f..81c82c9 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -21,10 +21,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Paint;
+import android.text.AutoGrowArray.FloatArray;
 import android.text.style.LeadingMarginSpan;
 import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
 import android.text.style.LineHeightSpan;
-import android.text.style.MetricAffectingSpan;
 import android.text.style.TabStopSpan;
 import android.util.Log;
 import android.util.Pools.SynchronizedPool;
@@ -99,8 +99,6 @@
             b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
             b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
             b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
-
-            b.mMeasuredText = MeasuredText.obtain();
             return b;
         }
 
@@ -111,8 +109,6 @@
         private static void recycle(@NonNull Builder b) {
             b.mPaint = null;
             b.mText = null;
-            MeasuredText.recycle(b.mMeasuredText);
-            b.mMeasuredText = null;
             b.mLeftIndents = null;
             b.mRightIndents = null;
             b.mLeftPaddings = null;
@@ -128,7 +124,6 @@
             mRightIndents = null;
             mLeftPaddings = null;
             mRightPaddings = null;
-            mMeasuredText.finish();
         }
 
         public Builder setText(CharSequence source) {
@@ -444,9 +439,6 @@
 
         private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
 
-        // This will go away and be subsumed by native builder code
-        private MeasuredText mMeasuredText;
-
         private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
     }
 
@@ -618,11 +610,7 @@
         TextUtils.TruncateAt ellipsize = b.mEllipsize;
         final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
         LineBreaks lineBreaks = new LineBreaks();  // TODO: move to builder to avoid allocation costs
-        // store span end locations
-        int[] spanEndCache = new int[4];
-        // store fontMetrics per span range
-        // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
-        int[] fmCache = new int[4 * 4];
+        FloatArray widths = new FloatArray();
 
         mLineCount = 0;
         mEllipsized = false;
@@ -634,8 +622,6 @@
         Paint.FontMetricsInt fm = b.mFontMetricsInt;
         int[] chooseHtv = null;
 
-        MeasuredText measured = b.mMeasuredText;
-
         Spanned spanned = null;
         if (source instanceof Spanned)
             spanned = (Spanned) source;
@@ -662,6 +648,7 @@
                 b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
                 indents, mLeftPaddings, mRightPaddings);
 
+        MeasuredText measured = null;
         try {
             int paraEnd;
             for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
@@ -721,13 +708,6 @@
                     }
                 }
 
-                measured.setPara(source, paraStart, paraEnd, textDir);
-                char[] chs = measured.mChars;
-                float[] widths = measured.mWidths;
-                byte[] chdirs = measured.mLevels;
-                int dir = measured.mDir;
-                boolean easy = measured.mEasy;
-
                 // tab stop locations
                 int[] variableTabStops = null;
                 if (spanned != null) {
@@ -743,50 +723,16 @@
                     }
                 }
 
+                measured = MeasuredText.buildForStaticLayout(
+                        paint, source, paraStart, paraEnd, textDir, nativePtr, measured);
+                final char[] chs = measured.getChars();
+                final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
+                final int[] fmCache = measured.getFontMetrics().getRawArray();
+                widths.resize(chs.length);
+
                 // measurement has to be done before performing line breaking
                 // but we don't want to recompute fontmetrics or span ranges the
                 // second time, so we cache those and then use those stored values
-                int fmCacheCount = 0;
-                int spanEndCacheCount = 0;
-                for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
-                    if (fmCacheCount * 4 >= fmCache.length) {
-                        int[] grow = new int[fmCacheCount * 4 * 2];
-                        System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
-                        fmCache = grow;
-                    }
-
-                    if (spanEndCacheCount >= spanEndCache.length) {
-                        int[] grow = new int[spanEndCacheCount * 2];
-                        System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
-                        spanEndCache = grow;
-                    }
-
-                    if (spanned == null) {
-                        spanEnd = paraEnd;
-                        int spanLen = spanEnd - spanStart;
-                        measured.addStyleRun(paint, spanLen, fm, nativePtr);
-                    } else {
-                        spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
-                                MetricAffectingSpan.class);
-                        int spanLen = spanEnd - spanStart;
-                        MetricAffectingSpan[] spans =
-                                spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
-                        spans = TextUtils.removeEmptySpans(spans, spanned,
-                                MetricAffectingSpan.class);
-                        measured.addStyleRun(paint, spans, spanLen, fm, nativePtr);
-                    }
-
-                    // the order of storage here (top, bottom, ascent, descent) has to match the
-                    // code below where these values are retrieved
-                    fmCache[fmCacheCount * 4 + 0] = fm.top;
-                    fmCache[fmCacheCount * 4 + 1] = fm.bottom;
-                    fmCache[fmCacheCount * 4 + 2] = fm.ascent;
-                    fmCache[fmCacheCount * 4 + 3] = fm.descent;
-                    fmCacheCount++;
-
-                    spanEndCache[spanEndCacheCount] = spanEnd;
-                    spanEndCacheCount++;
-                }
 
                 int breakCount = nComputeLineBreaks(
                         nativePtr,
@@ -809,7 +755,7 @@
                         lineBreaks.ascents,
                         lineBreaks.descents,
                         lineBreaks.flags,
-                        widths);
+                        widths.getRawArray());
 
                 final int[] breaks = lineBreaks.breaks;
                 final float[] lineWidths = lineBreaks.widths;
@@ -832,7 +778,7 @@
                             width += lineWidths[i];
                         } else {
                             for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
-                                width += widths[j];
+                                width += widths.get(j);
                             }
                         }
                         flag |= flags[i] & TAB_MASK;
@@ -896,10 +842,10 @@
                         v = out(source, here, endPos,
                                 ascent, descent, fmTop, fmBottom,
                                 v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
-                                flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd,
-                                includepad, trackpad, addLastLineSpacing, chs, widths, paraStart,
-                                ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint,
-                                moreChars);
+                                flags[breakIndex], needMultiply, measured, bufEnd,
+                                includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
+                                paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
+                                paint, moreChars);
 
                         if (endPos < spanEnd) {
                             // preserve metrics for current span
@@ -927,7 +873,8 @@
 
             if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
                     && mLineCount < mMaximumVisibleLineCount) {
-                measured.setPara(source, bufEnd, bufEnd, textDir);
+                measured = MeasuredText.buildForStaticLayout(
+                        paint, source, bufEnd, bufEnd, textDir, nativePtr, measured);
 
                 paint.getFontMetricsInt(fm);
 
@@ -937,12 +884,15 @@
                         v,
                         spacingmult, spacingadd, null,
                         null, fm, 0,
-                        needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
+                        needMultiply, measured, bufEnd,
                         includepad, trackpad, addLastLineSpacing, null,
                         null, bufStart, ellipsize,
                         ellipsizedWidth, 0, paint, false);
             }
         } finally {
+            if (measured != null) {
+                measured.recycle();
+            }
             nFinish(nativePtr);
         }
     }
@@ -952,8 +902,8 @@
     private int out(final CharSequence text, final int start, final int end, int above, int below,
             int top, int bottom, int v, final float spacingmult, final float spacingadd,
             final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
-            final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
-            final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
+            final int flags, final boolean needMultiply, final MeasuredText measured,
+            final int bufEnd, final boolean includePad, final boolean trackPad,
             final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
             final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
             final float textWidth, final TextPaint paint, final boolean moreChars) {
@@ -961,6 +911,7 @@
         final int off = j * mColumns;
         final int want = off + mColumns + TOP;
         int[] lines = mLines;
+        final int dir = measured.getParagraphDir();
 
         if (want >= lines.length) {
             final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
@@ -986,17 +937,8 @@
         // one bit for start field
         lines[off + TAB] |= flags & TAB_MASK;
         lines[off + HYPHEN] = flags;
-
         lines[off + DIR] |= dir << DIR_SHIFT;
-        // easy means all chars < the first RTL, so no emoji, no nothing
-        // XXX a run with no text or all spaces is easy but might be an empty
-        // RTL paragraph.  Make sure easy is false if this is the case.
-        if (easy) {
-            mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT;
-        } else {
-            mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
-                    start - widthStart, end - start);
-        }
+        mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
 
         final boolean firstLine = (j == 0);
         final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);