Support fallback linespacing in DynamicLayout

Bug: 28963299
Test: bit FrameworksCoreTests:android.text.
Change-Id: I132499d5927b26fb45522ffee99bac12aca3721f
diff --git a/api/current.txt b/api/current.txt
index 74adf5c..b95dc19 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -41417,6 +41417,7 @@
     method public android.text.DynamicLayout.Builder setJustificationMode(int);
     method public android.text.DynamicLayout.Builder setLineSpacing(float, float);
     method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic);
+    method public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean);
   }
 
   public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable {
diff --git a/api/system-current.txt b/api/system-current.txt
index 1841ce0..7036c90 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -45019,6 +45019,7 @@
     method public android.text.DynamicLayout.Builder setJustificationMode(int);
     method public android.text.DynamicLayout.Builder setLineSpacing(float, float);
     method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic);
+    method public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean);
   }
 
   public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable {
diff --git a/api/test-current.txt b/api/test-current.txt
index 2bb96fe..170261a 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -41681,6 +41681,7 @@
     method public android.text.DynamicLayout.Builder setJustificationMode(int);
     method public android.text.DynamicLayout.Builder setLineSpacing(float, float);
     method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic);
+    method public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean);
   }
 
   public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable {
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 661b608..5e40935 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -80,6 +80,7 @@
             b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER;
             b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION;
             b.mIncludePad = true;
+            b.mFallbackLineSpacing = false;
             b.mEllipsizedWidth = width;
             b.mEllipsize = null;
             b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
@@ -172,6 +173,25 @@
         }
 
         /**
+         * Set whether to respect the ascent and descent of the fallback fonts that are used in
+         * displaying the text (which is needed to avoid text from consecutive lines running into
+         * each other). If set, fallback fonts that end up getting used can increase the ascent
+         * and descent of the lines that they are used on.
+         *
+         * <p>For backward compatibility reasons, the default is {@code false}, but setting this to
+         * true is strongly recommended. It is required to be true if text could be in languages
+         * like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
+         *
+         * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts
+         * @return this builder, useful for chaining
+         */
+        @NonNull
+        public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) {
+            mFallbackLineSpacing = useLineSpacingFromFallbacks;
+            return this;
+        }
+
+        /**
          * Set the width as used for ellipsizing purposes, if it differs from the normal layout
          * width. The default is the {@code width} passed to {@link #obtain}.
          *
@@ -270,6 +290,7 @@
         private float mSpacingMult;
         private float mSpacingAdd;
         private boolean mIncludePad;
+        private boolean mFallbackLineSpacing;
         private int mBreakStrategy;
         private int mHyphenationFrequency;
         private int mJustificationMode;
@@ -320,7 +341,7 @@
                          @IntRange(from = 0) int ellipsizedWidth) {
         this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
                 spacingmult, spacingadd, includepad,
-                StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE,
+                Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE,
                 Layout.JUSTIFICATION_MODE_NONE, ellipsize, ellipsizedWidth);
     }
 
@@ -388,6 +409,7 @@
 
     private void generate(@NonNull Builder b) {
         mBase = b.mBase;
+        mFallbackLineSpacing = b.mFallbackLineSpacing;
         if (b.mEllipsize != null) {
             mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
             mEllipsizedWidth = b.mEllipsizedWidth;
@@ -573,6 +595,7 @@
                 .setWidth(getWidth())
                 .setTextDirection(getTextDirectionHeuristic())
                 .setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
+                .setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
                 .setEllipsizedWidth(mEllipsizedWidth)
                 .setEllipsize(mEllipsizeAt)
                 .setBreakStrategy(mBreakStrategy)
@@ -1033,10 +1056,11 @@
         private void reflow(CharSequence s, int where, int before, int after) {
             DynamicLayout ml = mLayout.get();
 
-            if (ml != null)
+            if (ml != null) {
                 ml.reflow(s, where, before, after);
-            else if (s instanceof Spannable)
+            } else if (s instanceof Spannable) {
                 ((Spannable) s).removeSpan(this);
+            }
         }
 
         public void beforeTextChanged(CharSequence s, int where, int before, int after) {
@@ -1093,6 +1117,7 @@
     private CharSequence mDisplay;
     private ChangeWatcher mWatcher;
     private boolean mIncludePad;
+    private boolean mFallbackLineSpacing;
     private boolean mEllipsize;
     private int mEllipsizedWidth;
     private TextUtils.TruncateAt mEllipsizeAt;
diff --git a/core/tests/coretests/src/android/text/DynamicLayoutTest.java b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
index 5ef08e0..ed6bfbf 100644
--- a/core/tests/coretests/src/android/text/DynamicLayoutTest.java
+++ b/core/tests/coretests/src/android/text/DynamicLayoutTest.java
@@ -186,4 +186,77 @@
                 ALIGN_NORMAL, 1.0f /*spacingMultiplier*/, 0f /*spacingAdd*/, false /*includepad*/);
         layout.getLineExtra(100);
     }
+
+    @Test
+    public void testFallbackLineSpacing() {
+        // All glyphs in the fonts are 1em wide.
+        final String[] testFontFiles = {
+            // ascent == 1em, descent == 2em, only supports 'a' and space
+            "ascent1em-descent2em.ttf",
+            // ascent == 3em, descent == 4em, only supports 'b'
+            "ascent3em-descent4em.ttf"
+        };
+        final String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+                + "<familyset>"
+                + "  <family name='sans-serif'>"
+                + "    <font weight='400' style='normal'>ascent1em-descent2em.ttf</font>"
+                + "  </family>"
+                + "  <family>"
+                + "    <font weight='400' style='normal'>ascent3em-descent4em.ttf</font>"
+                + "  </family>"
+                + "</familyset>";
+
+        try (FontFallbackSetup setup =
+                new FontFallbackSetup("DynamicLayout", testFontFiles, xml)) {
+            final TextPaint paint = setup.getPaintFor("sans-serif");
+            final int textSize = 100;
+            paint.setTextSize(textSize);
+            assertEquals(-textSize, paint.ascent(), 0.0f);
+            assertEquals(2 * textSize, paint.descent(), 0.0f);
+
+            final int paraWidth = 5 * textSize;
+            final String text = "aaaaa aabaa aaaaa"; // This should result in three lines.
+
+            // Old line spacing. All lines should get their ascent and descents from the first font.
+            DynamicLayout layout = DynamicLayout.Builder
+                    .obtain(text, paint, paraWidth)
+                    .setIncludePad(false)
+                    .setUseLineSpacingFromFallbacks(false)
+                    .build();
+            assertEquals(3, layout.getLineCount());
+            assertEquals(-textSize, layout.getLineAscent(0));
+            assertEquals(2 * textSize, layout.getLineDescent(0));
+            assertEquals(-textSize, layout.getLineAscent(1));
+            assertEquals(2 * textSize, layout.getLineDescent(1));
+            assertEquals(-textSize, layout.getLineAscent(2));
+            assertEquals(2 * textSize, layout.getLineDescent(2));
+
+            // New line spacing. The second line has a 'b', so it needs more ascent and descent.
+            layout = DynamicLayout.Builder
+                    .obtain(text, paint, paraWidth)
+                    .setIncludePad(false)
+                    .setUseLineSpacingFromFallbacks(true)
+                    .build();
+            assertEquals(3, layout.getLineCount());
+            assertEquals(-textSize, layout.getLineAscent(0));
+            assertEquals(2 * textSize, layout.getLineDescent(0));
+            assertEquals(-3 * textSize, layout.getLineAscent(1));
+            assertEquals(4 * textSize, layout.getLineDescent(1));
+            assertEquals(-textSize, layout.getLineAscent(2));
+            assertEquals(2 * textSize, layout.getLineDescent(2));
+
+            // The default is the old line spacing, for backward compatibility.
+            layout = DynamicLayout.Builder
+                    .obtain(text, paint, paraWidth)
+                    .setIncludePad(false)
+                    .build();
+            assertEquals(3, layout.getLineCount());
+            assertEquals(-textSize, layout.getLineAscent(0));
+            assertEquals(2 * textSize, layout.getLineDescent(0));
+            assertEquals(-textSize, layout.getLineAscent(1));
+            assertEquals(2 * textSize, layout.getLineDescent(1));
+            assertEquals(-textSize, layout.getLineAscent(2));
+            assertEquals(2 * textSize, layout.getLineDescent(2));
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/text/FontFallbackSetup.java b/core/tests/coretests/src/android/text/FontFallbackSetup.java
new file mode 100644
index 0000000..bcf2514
--- /dev/null
+++ b/core/tests/coretests/src/android/text/FontFallbackSetup.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.FontFamily;
+import android.graphics.Typeface;
+import android.support.test.InstrumentationRegistry;
+import android.util.ArrayMap;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
+public class FontFallbackSetup implements AutoCloseable {
+    private final String[] mTestFontFiles;
+    private final String mXml;
+    private final String mTestFontsDir;
+    final ArrayMap<String, Typeface> mFontMap = new ArrayMap<>();
+
+    public FontFallbackSetup(@NonNull String testSubDir, @NonNull String[] testFontFiles,
+            @NonNull String xml) {
+        mTestFontFiles = testFontFiles;
+        mXml = xml;
+
+        final Context targetCtx = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final File cacheDir = new File(targetCtx.getCacheDir(), testSubDir);
+        if (!cacheDir.isDirectory()) {
+            final boolean dirsCreated = cacheDir.mkdirs();
+            if (!dirsCreated) {
+                throw new RuntimeException("Creating test directories for fonts failed.");
+            }
+        }
+        mTestFontsDir = cacheDir.getAbsolutePath() + "/";
+
+        final String testFontsXml = new File(mTestFontsDir, "fonts.xml").getAbsolutePath();
+        final AssetManager am =
+                InstrumentationRegistry.getInstrumentation().getContext().getAssets();
+        for (String fontFile : mTestFontFiles) {
+            final String sourceInAsset = "fonts/" + fontFile;
+            final File outInCache = new File(mTestFontsDir, fontFile);
+            try (InputStream is = am.open(sourceInAsset)) {
+                Files.copy(is, outInCache.toPath(), StandardCopyOption.REPLACE_EXISTING);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        try (FileOutputStream fos = new FileOutputStream(testFontsXml)) {
+            fos.write(mXml.getBytes(Charset.forName("UTF-8")));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();
+        Typeface.buildSystemFallback(testFontsXml, mTestFontsDir, mFontMap, fallbackMap);
+    }
+
+    @NonNull
+    public TextPaint getPaintFor(@NonNull String fontName) {
+        final Typeface testTypeface = mFontMap.get(fontName);
+        final TextPaint paint = new TextPaint();
+        paint.setTypeface(testTypeface);
+        return paint;
+    }
+
+    @Override
+    public void close() {
+        for (String fontFile : mTestFontFiles) {
+            final File outInCache = new File(mTestFontsDir, fontFile);
+            outInCache.delete();
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java
index 1bf32ca..ab4b119 100644
--- a/core/tests/coretests/src/android/text/StaticLayoutTest.java
+++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java
@@ -21,33 +21,20 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
-import android.content.Context;
-import android.content.res.AssetManager;
 import android.graphics.Canvas;
-import android.graphics.FontFamily;
 import android.graphics.Paint.FontMetricsInt;
-import android.graphics.Typeface;
 import android.os.LocaleList;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.text.Layout.Alignment;
 import android.text.method.EditorState;
 import android.text.style.LocaleSpan;
-import android.util.ArrayMap;
 import android.util.Log;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.StandardCopyOption;
 import java.text.Normalizer;
 import java.util.ArrayList;
 import java.util.List;
@@ -813,57 +800,6 @@
         assertEquals(31, paint.getHyphenEdit());
     }
 
-    private String getTestFontsDir() {
-        final Context targetCtx = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        final File cacheDir = new File(targetCtx.getCacheDir(), "StaticLayoutTest");
-        if (!cacheDir.isDirectory()) {
-            final boolean dirsCreated = cacheDir.mkdirs();
-            if (!dirsCreated) {
-                throw new RuntimeException("Creating test directories for fonts failed.");
-            }
-        }
-        return cacheDir.getAbsolutePath() + "/";
-    }
-
-    private TextPaint setupPaintForFallbackFonts(String[] fontFiles, String xml) {
-        final String testFontsDir = getTestFontsDir();
-        final String testFontsXml = new File(testFontsDir, "fonts.xml").getAbsolutePath();
-        final AssetManager am =
-                InstrumentationRegistry.getInstrumentation().getContext().getAssets();
-        for (String fontFile : fontFiles) {
-            final String sourceInAsset = "fonts/" + fontFile;
-            final File outInCache = new File(testFontsDir, fontFile);
-            try (InputStream is = am.open(sourceInAsset)) {
-                Files.copy(is, outInCache.toPath(), StandardCopyOption.REPLACE_EXISTING);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        try (FileOutputStream fos = new FileOutputStream(testFontsXml)) {
-            fos.write(xml.getBytes(Charset.forName("UTF-8")));
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-
-        final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
-        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();
-        Typeface.buildSystemFallback(testFontsXml, testFontsDir, fontMap, fallbackMap);
-
-        final TextPaint paint = new TextPaint();
-        final Typeface testTypeface = fontMap.get("sans-serif");
-        paint.setTypeface(testTypeface);
-        return paint;
-    }
-
-    void destroyFallbackFonts(String[] fontFiles) {
-        final String testFontsDir = getTestFontsDir();
-        for (String fontFile : fontFiles) {
-            final File outInCache = new File(testFontsDir, fontFile);
-            outInCache.delete();
-        }
-    }
-
     @Test
     public void testFallbackLineSpacing() {
         // All glyphs in the fonts are 1em wide.
@@ -883,8 +819,9 @@
                 + "  </family>"
                 + "</familyset>";
 
-        try {
-            final TextPaint paint = setupPaintForFallbackFonts(testFontFiles, xml);
+        try (FontFallbackSetup setup =
+                new FontFallbackSetup("StaticLayout", testFontFiles, xml)) {
+            final TextPaint paint = setup.getPaintFor("sans-serif");
             final int textSize = 100;
             paint.setTextSize(textSize);
             assertEquals(-textSize, paint.ascent(), 0.0f);
@@ -933,8 +870,6 @@
             assertEquals(2 * textSize, layout.getLineDescent(1));
             assertEquals(-textSize, layout.getLineAscent(2));
             assertEquals(2 * textSize, layout.getLineDescent(2));
-        } finally {
-            destroyFallbackFonts(testFontFiles);
         }
     }
 }