Check updatable font dir in FontListParser.

If updatable font dir is provided by caller and fonts exist in the dir,
FontListParser will use the updated fonts.
Currently, updatable font dir is used only in test.

Bug: 173619554
Test: atest TypefaceSystemFallbackTest
Change-Id: I631764fe235d258b6b5cb6d6906c17d48c7e4608
diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
index 82d066f..05ff218 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
@@ -67,6 +67,7 @@
     private static final String TEST_FONT_DIR;
     private static final String TEST_OEM_XML;
     private static final String TEST_OEM_DIR;
+    private static final String TEST_UPDATABLE_FONT_DIR;
 
     private static final float GLYPH_1EM_WIDTH;
     private static final float GLYPH_2EM_WIDTH;
@@ -82,9 +83,11 @@
         TEST_FONTS_XML = new File(cacheDir, "fonts.xml").getAbsolutePath();
         TEST_OEM_DIR = cacheDir.getAbsolutePath() + "/oem_fonts/";
         TEST_OEM_XML = new File(cacheDir, "fonts_customization.xml").getAbsolutePath();
+        TEST_UPDATABLE_FONT_DIR = cacheDir.getAbsolutePath() + "/updatable_fonts/";
 
         new File(TEST_FONT_DIR).mkdirs();
         new File(TEST_OEM_DIR).mkdirs();
+        new File(TEST_UPDATABLE_FONT_DIR).mkdirs();
 
         final AssetManager am =
                 InstrumentationRegistry.getInstrumentation().getContext().getAssets();
@@ -103,18 +106,11 @@
                 InstrumentationRegistry.getInstrumentation().getContext().getAssets();
         for (final String fontFile : TEST_FONT_FILES) {
             final String sourceInAsset = "fonts/" + fontFile;
-            final File outInCache = new File(TEST_FONT_DIR, fontFile);
-            try (InputStream is = am.open(sourceInAsset)) {
-                Files.copy(is, outInCache.toPath(), StandardCopyOption.REPLACE_EXISTING);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-            final File outOemInCache = new File(TEST_OEM_DIR, fontFile);
-            try (InputStream is = am.open(sourceInAsset)) {
-                Files.copy(is, outOemInCache.toPath(), StandardCopyOption.REPLACE_EXISTING);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
+            copyAssetToFile(sourceInAsset, new File(TEST_FONT_DIR, fontFile));
+            copyAssetToFile(sourceInAsset, new File(TEST_OEM_DIR, fontFile));
+        }
+        for (final File fontFile : new File(TEST_UPDATABLE_FONT_DIR).listFiles()) {
+            fontFile.delete();
         }
     }
 
@@ -124,7 +120,20 @@
             final File outInCache = new File(TEST_FONT_DIR, fontFile);
             outInCache.delete();
             final File outOemInCache = new File(TEST_OEM_DIR, fontFile);
-            outInCache.delete();
+            outOemInCache.delete();
+        }
+        for (final File fontFile : new File(TEST_UPDATABLE_FONT_DIR).listFiles()) {
+            fontFile.delete();
+        }
+    }
+
+    private static void copyAssetToFile(String sourceInAsset, File out) {
+        final AssetManager am =
+                InstrumentationRegistry.getInstrumentation().getContext().getAssets();
+        try (InputStream is = am.open(sourceInAsset)) {
+            Files.copy(is, out.toPath(), StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
         }
     }
 
@@ -138,7 +147,7 @@
         }
 
         final FontConfig.Alias[] aliases = SystemFonts.buildSystemFallback(TEST_FONTS_XML,
-                TEST_FONT_DIR, oemCustomization, fallbackMap);
+                TEST_FONT_DIR, TEST_UPDATABLE_FONT_DIR, oemCustomization, fallbackMap);
         Typeface.initSystemDefaultTypefaces(fontMap, fallbackMap, aliases);
     }
 
@@ -835,4 +844,32 @@
                 + "</fonts-modification>";
         readFontCustomization(oemXml);
     }
+
+
+    @Test
+    public void testBuildSystemFallback_UpdatableFont() {
+        final String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+                + "<familyset>"
+                + "  <family name='test'>"
+                + "    <font weight='400' style='normal'>a3em.ttf</font>"
+                + "  </family>"
+                + "</familyset>";
+        final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
+        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();
+        final FontCustomizationParser.Result oemCustomization =
+                new FontCustomizationParser.Result();
+
+        // Install all2em.ttf as a3em.ttf
+        copyAssetToFile("fonts/all2em.ttf", new File(TEST_UPDATABLE_FONT_DIR, "a3em.ttf"));
+        buildSystemFallback(xml, oemCustomization, fontMap, fallbackMap);
+
+        final Paint paint = new Paint();
+
+        final Typeface sansSerifTypeface = fontMap.get("test");
+        assertNotNull(sansSerifTypeface);
+        paint.setTypeface(sansSerifTypeface);
+        assertEquals(GLYPH_2EM_WIDTH, paint.measureText("a"), 0.0f);
+        assertEquals(GLYPH_2EM_WIDTH, paint.measureText("b"), 0.0f);
+        assertEquals(GLYPH_2EM_WIDTH, paint.measureText("c"), 0.0f);
+    }
 }
diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java
index 0782f8d..73fff72 100644
--- a/graphics/java/android/graphics/FontListParser.java
+++ b/graphics/java/android/graphics/FontListParser.java
@@ -16,6 +16,7 @@
 
 package android.graphics;
 
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.graphics.fonts.FontVariationAxis;
 import android.os.Build;
@@ -25,6 +26,7 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -41,26 +43,26 @@
     /* Parse fallback list (no names) */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
-        return parse(in, "/system/fonts");
+        return parse(in, "/system/fonts", null);
     }
 
     /**
      * Parse the fonts.xml
      */
-    public static FontConfig parse(InputStream in, String fontDir)
-            throws XmlPullParserException, IOException {
+    public static FontConfig parse(InputStream in, String fontDir,
+            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
         try {
             XmlPullParser parser = Xml.newPullParser();
             parser.setInput(in, null);
             parser.nextTag();
-            return readFamilies(parser, fontDir);
+            return readFamilies(parser, fontDir, updatableFontDir);
         } finally {
             in.close();
         }
     }
 
-    private static FontConfig readFamilies(XmlPullParser parser, String fontDir)
-            throws XmlPullParserException, IOException {
+    private static FontConfig readFamilies(XmlPullParser parser, String fontDir,
+            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
         List<FontConfig.Family> families = new ArrayList<>();
         List<FontConfig.Alias> aliases = new ArrayList<>();
 
@@ -69,7 +71,7 @@
             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
             String tag = parser.getName();
             if (tag.equals("family")) {
-                families.add(readFamily(parser, fontDir));
+                families.add(readFamily(parser, fontDir, updatableFontDir));
             } else if (tag.equals("alias")) {
                 aliases.add(readAlias(parser));
             } else {
@@ -83,8 +85,8 @@
     /**
      * Reads a family element
      */
-    public static FontConfig.Family readFamily(XmlPullParser parser, String fontDir)
-            throws XmlPullParserException, IOException {
+    public static FontConfig.Family readFamily(XmlPullParser parser, String fontDir,
+            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
         final String name = parser.getAttributeValue(null, "name");
         final String lang = parser.getAttributeValue("", "lang");
         final String variant = parser.getAttributeValue(null, "variant");
@@ -93,7 +95,7 @@
             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
             final String tag = parser.getName();
             if (tag.equals("font")) {
-                fonts.add(readFont(parser, fontDir));
+                fonts.add(readFont(parser, fontDir, updatableFontDir));
             } else {
                 skip(parser);
             }
@@ -114,8 +116,8 @@
     private static final Pattern FILENAME_WHITESPACE_PATTERN =
             Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
 
-    private static FontConfig.Font readFont(XmlPullParser parser, String fontDir)
-            throws XmlPullParserException, IOException {
+    private static FontConfig.Font readFont(XmlPullParser parser, String fontDir,
+            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
         String indexStr = parser.getAttributeValue(null, "index");
         int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
         List<FontVariationAxis> axes = new ArrayList<FontVariationAxis>();
@@ -137,10 +139,22 @@
             }
         }
         String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
-        return new FontConfig.Font(fontDir + sanitizedName, index, axes.toArray(
+        String fontName = findFontFile(sanitizedName, fontDir, updatableFontDir);
+        return new FontConfig.Font(fontName, index, axes.toArray(
                 new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor);
     }
 
+    private static String findFontFile(String fileName, String fontDir,
+            @Nullable String updatableFontDir) {
+        if (updatableFontDir != null) {
+            String updatableFontName = updatableFontDir + fileName;
+            if (new File(updatableFontName).exists()) {
+                return updatableFontName;
+            }
+        }
+        return fontDir + fileName;
+    }
+
     private static FontVariationAxis readAxis(XmlPullParser parser)
             throws XmlPullParserException, IOException {
         String tagStr = parser.getAttributeValue(null, "tag");
diff --git a/graphics/java/android/graphics/fonts/FontCustomizationParser.java b/graphics/java/android/graphics/fonts/FontCustomizationParser.java
index 0291d74..f95da82 100644
--- a/graphics/java/android/graphics/fonts/FontCustomizationParser.java
+++ b/graphics/java/android/graphics/fonts/FontCustomizationParser.java
@@ -97,7 +97,7 @@
             throw new IllegalArgumentException("customizationType must be specified");
         }
         if (customizationType.equals("new-named-family")) {
-            out.mAdditionalNamedFamilies.add(FontListParser.readFamily(parser, fontDir));
+            out.mAdditionalNamedFamilies.add(FontListParser.readFamily(parser, fontDir, null));
         } else {
             throw new IllegalArgumentException("Unknown customizationType=" + customizationType);
         }
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
index fb6ea99..16a53c2 100644
--- a/graphics/java/android/graphics/fonts/SystemFonts.java
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -91,7 +91,9 @@
         final FontCustomizationParser.Result oemCustomization =
                 readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
         Map<String, FontFamily[]> map = new ArrayMap<>();
-        buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", oemCustomization, map);
+        // TODO: use updated fonts
+        buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", null /* updatableFontDir */,
+                oemCustomization, map);
         Set<Font> res = new HashSet<>();
         for (FontFamily[] families : map.values()) {
             for (FontFamily family : families) {
@@ -226,13 +228,7 @@
     }
 
     /**
-     * Build the system fallback from xml file.
-     *
-     * @param xmlPath A full path string to the fonts.xml file.
-     * @param fontDir A full path string to the system font directory. This must end with
-     *                slash('/').
-     * @param fallbackMap An output system fallback map. Caller must pass empty map.
-     * @return a list of aliases
+     * @see #buildSystemFallback(String, String, String, FontCustomizationParser.Result, Map)
      * @hide
      */
     @VisibleForTesting
@@ -240,9 +236,31 @@
             @NonNull String fontDir,
             @NonNull FontCustomizationParser.Result oemCustomization,
             @NonNull Map<String, FontFamily[]> fallbackMap) {
+        return buildSystemFallback(xmlPath, fontDir, null /* updatableFontDir */,
+                oemCustomization, fallbackMap);
+    }
+
+    /**
+     * Build the system fallback from xml file.
+     *
+     * @param xmlPath A full path string to the fonts.xml file.
+     * @param fontDir A full path string to the system font directory. This must end with
+     *                slash('/').
+     * @param updatableFontDir A full path string to the updatable system font directory. This
+     *                           must end with slash('/').
+     * @param fallbackMap An output system fallback map. Caller must pass empty map.
+     * @return a list of aliases
+     * @hide
+     */
+    @VisibleForTesting
+    public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
+            @NonNull String fontDir,
+            @Nullable String updatableFontDir,
+            @NonNull FontCustomizationParser.Result oemCustomization,
+            @NonNull Map<String, FontFamily[]> fallbackMap) {
         try {
             final FileInputStream fontsIn = new FileInputStream(xmlPath);
-            final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir);
+            final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir, updatableFontDir);
 
             final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
             final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
@@ -306,11 +324,17 @@
     /** @hide */
     public static @NonNull Pair<FontConfig.Alias[], Map<String, FontFamily[]>>
             initializePreinstalledFonts() {
+        return initializeSystemFonts(null);
+    }
+
+    /** @hide */
+    public static Pair<FontConfig.Alias[], Map<String, FontFamily[]>>
+            initializeSystemFonts(@Nullable String updatableFontDir) {
         final FontCustomizationParser.Result oemCustomization =
                 readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
         Map<String, FontFamily[]> map = new ArrayMap<>();
         FontConfig.Alias[] aliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
-                oemCustomization, map);
+                updatableFontDir, oemCustomization, map);
         synchronized (LOCK) {
             sFamilyMap = map;
         }