Merge "Support font family update internally." into sc-dev am: 7a6f5495bd

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13534852

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: I8b90a9d4679175e6113f3f4f89f058ac615bf10c
diff --git a/core/java/android/graphics/fonts/FontUpdateRequest.java b/core/java/android/graphics/fonts/FontUpdateRequest.java
index db047f8..f551d6a 100644
--- a/core/java/android/graphics/fonts/FontUpdateRequest.java
+++ b/core/java/android/graphics/fonts/FontUpdateRequest.java
@@ -16,18 +16,33 @@
 
 package android.graphics.fonts;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
+import android.text.FontConfig;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * Represents a font update request. Currently only font install request is supported.
  * @hide
  */
-// TODO: Support font config update.
 public final class FontUpdateRequest implements Parcelable {
 
+    public static final int TYPE_UPDATE_FONT_FILE = 0;
+    public static final int TYPE_UPDATE_FONT_FAMILY = 1;
+
+    @IntDef(prefix = "TYPE_", value = {
+            TYPE_UPDATE_FONT_FILE,
+            TYPE_UPDATE_FONT_FAMILY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {}
+
     public static final Creator<FontUpdateRequest> CREATOR = new Creator<FontUpdateRequest>() {
         @Override
         public FontUpdateRequest createFromParcel(Parcel in) {
@@ -40,39 +55,67 @@
         }
     };
 
-    @NonNull
+    private final @Type int mType;
+    // NonNull if mType == TYPE_UPDATE_FONT_FILE.
+    @Nullable
     private final ParcelFileDescriptor mFd;
-    @NonNull
+    // NonNull if mType == TYPE_UPDATE_FONT_FILE.
+    @Nullable
     private final byte[] mSignature;
+    // NonNull if mType == TYPE_UPDATE_FONT_FAMILY.
+    @Nullable
+    private final FontConfig.FontFamily mFontFamily;
 
     public FontUpdateRequest(@NonNull ParcelFileDescriptor fd, @NonNull byte[] signature) {
+        mType = TYPE_UPDATE_FONT_FILE;
         mFd = fd;
         mSignature = signature;
+        mFontFamily = null;
     }
 
-    private FontUpdateRequest(Parcel in) {
+    public FontUpdateRequest(@NonNull FontConfig.FontFamily fontFamily) {
+        mType = TYPE_UPDATE_FONT_FAMILY;
+        mFd = null;
+        mSignature = null;
+        mFontFamily = fontFamily;
+    }
+
+    protected FontUpdateRequest(Parcel in) {
+        mType = in.readInt();
         mFd = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
         mSignature = in.readBlob();
+        mFontFamily = in.readParcelable(FontConfig.FontFamily.class.getClassLoader());
     }
 
-    @NonNull
+    public @Type int getType() {
+        return mType;
+    }
+
+    @Nullable
     public ParcelFileDescriptor getFd() {
         return mFd;
     }
 
-    @NonNull
+    @Nullable
     public byte[] getSignature() {
         return mSignature;
     }
 
+    @Nullable
+    public FontConfig.FontFamily getFontFamily() {
+        return mFontFamily;
+    }
+
     @Override
     public int describeContents() {
-        return Parcelable.CONTENTS_FILE_DESCRIPTOR;
+        return mFd != null ? mFd.describeContents() : 0;
     }
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mType);
         dest.writeParcelable(mFd, flags);
         dest.writeBlob(mSignature);
+        dest.writeParcelable(mFontFamily, flags);
     }
 }
diff --git a/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
index 017f11c..d514aab 100644
--- a/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
+++ b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
@@ -17,6 +17,8 @@
 package com.android.server.graphics.fonts;
 
 import android.annotation.NonNull;
+import android.graphics.FontListParser;
+import android.text.FontConfig;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Slog;
@@ -30,6 +32,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 
 /* package */ class PersistentSystemFontConfig {
@@ -38,11 +42,13 @@
     private static final String TAG_ROOT = "fontConfig";
     private static final String TAG_LAST_MODIFIED_DATE = "lastModifiedDate";
     private static final String TAG_UPDATED_FONT_DIR = "updatedFontDir";
+    private static final String TAG_FAMILY = "family";
     private static final String ATTR_VALUE = "value";
 
     /* package */ static class Config {
         public long lastModifiedDate;
         public final Set<String> updatedFontDirs = new ArraySet<>();
+        public final List<FontConfig.FontFamily> fontFamilies = new ArrayList<>();
     }
 
     /**
@@ -72,6 +78,11 @@
                     case TAG_UPDATED_FONT_DIR:
                         out.updatedFontDirs.add(getAttribute(parser, ATTR_VALUE));
                         break;
+                    case TAG_FAMILY:
+                        // updatableFontMap is not ready here. We get the base file names by passing
+                        // empty fontDir, and resolve font paths later.
+                        out.fontFamilies.add(FontListParser.readFamily(
+                                parser, "" /* fontDir */, null /* updatableFontMap */));
                     default:
                         Slog.w(TAG, "Skipping unknown tag: " + tag);
                 }
@@ -97,6 +108,13 @@
             out.attribute(null, ATTR_VALUE, dir);
             out.endTag(null, TAG_UPDATED_FONT_DIR);
         }
+        List<FontConfig.FontFamily> fontFamilies = config.fontFamilies;
+        for (int i = 0; i < fontFamilies.size(); i++) {
+            FontConfig.FontFamily fontFamily = fontFamilies.get(i);
+            out.startTag(null, TAG_FAMILY);
+            FontListParser.writeFamily(out, fontFamily);
+            out.endTag(null, TAG_FAMILY);
+        }
         out.endTag(null, TAG_ROOT);
 
         out.endDocument();
diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
index dac94f6..45f2a38 100644
--- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -31,6 +31,8 @@
 import android.util.Base64;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.File;
@@ -40,6 +42,7 @@
 import java.io.IOException;
 import java.security.SecureRandom;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -118,6 +121,12 @@
      */
     private final ArrayMap<String, FontFileInfo> mFontFileInfoMap = new ArrayMap<>();
 
+    /**
+     * A mutable map containing mapping from font family name to {@link FontConfig.FontFamily}.
+     * The FontFamily entries only reference font files in {@link #mFontFileInfoMap}.
+     */
+    private final ArrayMap<String, FontConfig.FontFamily> mFontFamilyMap = new ArrayMap<>();
+
     UpdatableFontDir(File filesDir, List<File> preinstalledFontDirs, FontFileParser parser,
             FsverityUtil fsverityUtil) {
         this(filesDir, preinstalledFontDirs, parser, fsverityUtil, new File(CONFIG_XML_FILE));
@@ -136,6 +145,7 @@
 
     /* package */ void loadFontFileMap() {
         mFontFileInfoMap.clear();
+        mFontFamilyMap.clear();
         mLastModifiedDate = 0;
         boolean success = false;
         try {
@@ -168,6 +178,13 @@
                 FontFileInfo fontFileInfo = validateFontFile(files[0]);
                 addFileToMapIfNewer(fontFileInfo, true /* deleteOldFile */);
             }
+            // Resolve font file paths.
+            List<FontConfig.FontFamily> fontFamilies = config.fontFamilies;
+            for (int i = 0; i < fontFamilies.size(); i++) {
+                FontConfig.FontFamily fontFamily = fontFamilies.get(i);
+                // Ignore failures as updated fonts may be obsoleted by system OTA update.
+                addFontFamily(fontFamily);
+            }
             success = true;
         } catch (Throwable t) {
             // If something happened during loading system fonts, clear all contents in finally
@@ -177,6 +194,7 @@
             // Delete all files just in case if we find a problematic file.
             if (!success) {
                 mFontFileInfoMap.clear();
+                mFontFamilyMap.clear();
                 mLastModifiedDate = 0;
                 FileUtils.deleteContents(mFilesDir);
             }
@@ -186,10 +204,11 @@
     /* package */ void clearUpdates() throws SystemFontException {
         mFontFileInfoMap.clear();
         FileUtils.deleteContents(mFilesDir);
+        mFontFamilyMap.clear();
 
         mLastModifiedDate = Instant.now().getEpochSecond();
         try (FileOutputStream fos = new FileOutputStream(mConfigFile)) {
-            PersistentSystemFontConfig.writeToXml(fos, getPersistentConfig());
+            PersistentSystemFontConfig.writeToXml(fos, createPersistentConfig());
         } catch (Exception e) {
             throw new SystemFontException(
                     FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG,
@@ -206,17 +225,29 @@
     public void update(List<FontUpdateRequest> requests) throws SystemFontException {
         // Backup the mapping for rollback.
         ArrayMap<String, FontFileInfo> backupMap = new ArrayMap<>(mFontFileInfoMap);
+        ArrayMap<String, FontConfig.FontFamily> backupFamilies = new ArrayMap<>(mFontFamilyMap);
         long backupLastModifiedDate = mLastModifiedDate;
         boolean success = false;
         try {
             for (FontUpdateRequest request : requests) {
-                installFontFile(request.getFd().getFileDescriptor(), request.getSignature());
+                switch (request.getType()) {
+                    case FontUpdateRequest.TYPE_UPDATE_FONT_FILE:
+                        installFontFile(
+                                request.getFd().getFileDescriptor(), request.getSignature());
+                        break;
+                    case FontUpdateRequest.TYPE_UPDATE_FONT_FAMILY:
+                        // TODO: define error code.
+                        if (!addFontFamily(request.getFontFamily())) {
+                            throw new IllegalArgumentException("Invalid font family");
+                        }
+                        break;
+                }
             }
 
             // Write config file.
             mLastModifiedDate = Instant.now().getEpochSecond();
             try (FileOutputStream fos = new FileOutputStream(mTmpConfigFile)) {
-                PersistentSystemFontConfig.writeToXml(fos, getPersistentConfig());
+                PersistentSystemFontConfig.writeToXml(fos, createPersistentConfig());
             } catch (Exception e) {
                 throw new SystemFontException(
                         FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG,
@@ -234,6 +265,8 @@
             if (!success) {
                 mFontFileInfoMap.clear();
                 mFontFileInfoMap.putAll(backupMap);
+                mFontFamilyMap.clear();
+                mFontFamilyMap.putAll(backupFamilies);
                 mLastModifiedDate = backupLastModifiedDate;
             }
         }
@@ -454,12 +487,52 @@
         }
     }
 
-    private PersistentSystemFontConfig.Config getPersistentConfig() {
+    /**
+     * Adds a font family to {@link #mFontFamilyMap} and returns true on success.
+     *
+     * <p>This method only accepts adding or updating a font family with a name.
+     * This is to prevent bad font family update from removing glyphs from font fallback chains.
+     * Unnamed font families are used as other named font family's fallback fonts to guarantee a
+     * complete glyph coverage.
+     */
+    private boolean addFontFamily(FontConfig.FontFamily fontFamily) {
+        if (fontFamily.getName() == null) {
+            Slog.e(TAG, "Name is null.");
+            return false;
+        }
+        FontConfig.FontFamily resolvedFontFamily = resolveFontFiles(fontFamily);
+        if (resolvedFontFamily == null) {
+            Slog.e(TAG, "Required fonts are not available");
+            return false;
+        }
+        mFontFamilyMap.put(resolvedFontFamily.getName(), resolvedFontFamily);
+        return true;
+    }
+
+    @Nullable
+    private FontConfig.FontFamily resolveFontFiles(FontConfig.FontFamily fontFamily) {
+        List<FontConfig.Font> resolvedFonts = new ArrayList<>(fontFamily.getFontList().size());
+        List<FontConfig.Font> fontList = fontFamily.getFontList();
+        for (int i = 0; i < fontList.size(); i++) {
+            FontConfig.Font font = fontList.get(i);
+            FontFileInfo info = mFontFileInfoMap.get(font.getFile().getName());
+            if (info == null) {
+                return null;
+            }
+            resolvedFonts.add(new FontConfig.Font(info.mFile, null, font.getStyle(),
+                    font.getTtcIndex(), font.getFontVariationSettings(), font.getFontFamilyName()));
+        }
+        return new FontConfig.FontFamily(resolvedFonts, fontFamily.getName(),
+                fontFamily.getLocaleList(), fontFamily.getVariant());
+    }
+
+    private PersistentSystemFontConfig.Config createPersistentConfig() {
         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
         config.lastModifiedDate = mLastModifiedDate;
         for (FontFileInfo info : mFontFileInfoMap.values()) {
             config.updatedFontDirs.add(info.getRandomizedFontDir().getName());
         }
+        config.fontFamilies.addAll(mFontFamilyMap.values());
         return config;
     }
 
@@ -471,8 +544,24 @@
         return map;
     }
 
+    @VisibleForTesting
+    Map<String, FontConfig.FontFamily> getFontFamilyMap() {
+        return mFontFamilyMap;
+    }
+
     /* package */ FontConfig getSystemFontConfig() {
-        return SystemFonts.getSystemFontConfig(getFontFileMap(), mLastModifiedDate, mConfigVersion);
+        FontConfig config = SystemFonts.getSystemFontConfig(getFontFileMap(), 0, 0);
+        List<FontConfig.FontFamily> mergedFamilies =
+                new ArrayList<>(config.getFontFamilies().size() + mFontFamilyMap.size());
+        // We should keep the first font family (config.getFontFamilies().get(0)) because it's used
+        // as a fallback font. See SystemFonts.java.
+        mergedFamilies.addAll(config.getFontFamilies());
+        // When building Typeface, a latter font family definition will override the previous font
+        // family definition with the same name. An exception is config.getFontFamilies.get(0),
+        // which will be used as a fallback font without being overridden.
+        mergedFamilies.addAll(mFontFamilyMap.values());
+        return new FontConfig(
+                mergedFamilies, config.getAliases(), mLastModifiedDate, mConfigVersion);
     }
 
     /* package */ int getConfigVersion() {
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/PersistentSystemFontConfigTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/PersistentSystemFontConfigTest.java
index 86054e4..27fce3c 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/PersistentSystemFontConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/PersistentSystemFontConfigTest.java
@@ -18,13 +18,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.graphics.FontListParser;
 import android.platform.test.annotations.Presubmit;
+import android.text.FontConfig;
+import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.ByteArrayInputStream;
@@ -38,13 +42,19 @@
 public final class PersistentSystemFontConfigTest {
 
     @Test
-    public void testWriteRead() throws IOException, XmlPullParserException {
+    public void testWriteRead() throws Exception {
         long expectedModifiedDate = 1234567890;
         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
         config.lastModifiedDate = expectedModifiedDate;
         config.updatedFontDirs.add("~~abc");
         config.updatedFontDirs.add("~~def");
 
+        FontConfig.FontFamily fontFamily = parseFontFamily(
+                "<family name='test'>"
+                + "  <font>test.ttf</font>"
+                + "</family>");
+        config.fontFamilies.add(fontFamily);
+
         try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
             PersistentSystemFontConfig.writeToXml(baos, config);
 
@@ -57,6 +67,7 @@
 
                 assertThat(another.lastModifiedDate).isEqualTo(expectedModifiedDate);
                 assertThat(another.updatedFontDirs).containsExactly("~~abc", "~~def");
+                assertThat(another.fontFamilies).containsExactly(fontFamily);
             }
         }
     }
@@ -75,4 +86,11 @@
         }
     }
 
+    private static FontConfig.FontFamily parseFontFamily(String xml) throws Exception {
+        XmlPullParser parser = Xml.newPullParser();
+        ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
+        parser.setInput(is, "UTF-8");
+        parser.nextTag();
+        return FontListParser.readFamily(parser, "", null);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index cb83b0f..4bbf96f 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -22,12 +22,15 @@
 import static org.junit.Assert.fail;
 
 import android.content.Context;
+import android.graphics.FontListParser;
 import android.graphics.fonts.FontManager;
 import android.graphics.fonts.FontUpdateRequest;
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.Presubmit;
 import android.system.Os;
+import android.text.FontConfig;
+import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -37,7 +40,9 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -49,6 +54,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 @Presubmit
 @SmallTest
@@ -157,7 +163,11 @@
                 newFontUpdateRequest("foo,1", GOOD_SIGNATURE),
                 newFontUpdateRequest("bar,2", GOOD_SIGNATURE),
                 newFontUpdateRequest("foo,3", GOOD_SIGNATURE),
-                newFontUpdateRequest("bar,4", GOOD_SIGNATURE)));
+                newFontUpdateRequest("bar,4", GOOD_SIGNATURE),
+                newAddFontFamilyRequest("<family name='foobar'>"
+                        + "  <font>foo.ttf</font>"
+                        + "  <font>bar.ttf</font>"
+                        + "</family>")));
         // Four font dirs are created.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
         assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis())
@@ -173,6 +183,14 @@
         assertThat(parser.getRevision(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4);
         // Outdated font dir should be deleted.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(2);
+        assertNamedFamilyExists(dir.getSystemFontConfig(), "foobar");
+        assertThat(dir.getFontFamilyMap()).containsKey("foobar");
+        FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar");
+        assertThat(foobar.getFontList()).hasSize(2);
+        assertThat(foobar.getFontList().get(0).getFile())
+                .isEqualTo(dir.getFontFileMap().get("foo.ttf"));
+        assertThat(foobar.getFontList().get(1).getFile())
+                .isEqualTo(dir.getFontFileMap().get("bar.ttf"));
     }
 
     @Test
@@ -184,6 +202,7 @@
                 mConfigFile);
         dir.loadFontFileMap();
         assertThat(dir.getFontFileMap()).isEmpty();
+        assertThat(dir.getFontFamilyMap()).isEmpty();
     }
 
     @Test
@@ -198,7 +217,11 @@
                 newFontUpdateRequest("foo,1", GOOD_SIGNATURE),
                 newFontUpdateRequest("bar,2", GOOD_SIGNATURE),
                 newFontUpdateRequest("foo,3", GOOD_SIGNATURE),
-                newFontUpdateRequest("bar,4", GOOD_SIGNATURE)));
+                newFontUpdateRequest("bar,4", GOOD_SIGNATURE),
+                newAddFontFamilyRequest("<family name='foobar'>"
+                        + "  <font>foo.ttf</font>"
+                        + "  <font>bar.ttf</font>"
+                        + "</family>")));
         // Four font dirs are created.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
 
@@ -211,6 +234,7 @@
         assertThat(dir.getFontFileMap()).isEmpty();
         // All font dirs (including dir for "bar.ttf") should be deleted.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
+        assertThat(dir.getFontFamilyMap()).isEmpty();
     }
 
     @Test
@@ -225,7 +249,11 @@
                 newFontUpdateRequest("foo,1", GOOD_SIGNATURE),
                 newFontUpdateRequest("bar,2", GOOD_SIGNATURE),
                 newFontUpdateRequest("foo,3", GOOD_SIGNATURE),
-                newFontUpdateRequest("bar,4", GOOD_SIGNATURE)));
+                newFontUpdateRequest("bar,4", GOOD_SIGNATURE),
+                newAddFontFamilyRequest("<family name='foobar'>"
+                        + "  <font>foo.ttf</font>"
+                        + "  <font>bar.ttf</font>"
+                        + "</family>")));
         // Four font dirs are created.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
 
@@ -239,6 +267,7 @@
         assertThat(dir.getFontFileMap()).isEmpty();
         // All font dirs (including dir for "bar.ttf") should be deleted.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
+        assertThat(dir.getFontFamilyMap()).isEmpty();
     }
 
     @Test
@@ -253,7 +282,11 @@
                 newFontUpdateRequest("foo,1", GOOD_SIGNATURE),
                 newFontUpdateRequest("bar,2", GOOD_SIGNATURE),
                 newFontUpdateRequest("foo,3", GOOD_SIGNATURE),
-                newFontUpdateRequest("bar,4", GOOD_SIGNATURE)));
+                newFontUpdateRequest("bar,4", GOOD_SIGNATURE),
+                newAddFontFamilyRequest("<family name='foobar'>"
+                        + "  <font>foo.ttf</font>"
+                        + "  <font>bar.ttf</font>"
+                        + "</family>")));
         // Four font dirs are created.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
 
@@ -274,6 +307,8 @@
         // We don't delete bar.ttf in this case, because it's normal that OTA updates preinstalled
         // fonts.
         assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
+        // Font family depending on obsoleted font should be removed.
+        assertThat(dir.getFontFamilyMap()).isEmpty();
     }
 
     @Test
@@ -285,6 +320,7 @@
                 new File("/dev/null"));
         dir.loadFontFileMap();
         assertThat(dir.getFontFileMap()).isEmpty();
+        assertThat(dir.getFontFamilyMap()).isEmpty();
     }
 
     @Test
@@ -295,12 +331,19 @@
                 mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil,
                 mConfigFile);
         dirForPreparation.loadFontFileMap();
-        dirForPreparation.update(
-                Collections.singletonList(newFontUpdateRequest("foo,1", GOOD_SIGNATURE)));
+        dirForPreparation.update(Arrays.asList(
+                newFontUpdateRequest("foo,1", GOOD_SIGNATURE),
+                newAddFontFamilyRequest("<family name='foobar'>"
+                        + "  <font>foo.ttf</font>"
+                        + "</family>")));
         try {
             dirForPreparation.update(Arrays.asList(
                     newFontUpdateRequest("foo,2", GOOD_SIGNATURE),
-                    newFontUpdateRequest("bar,2", "Invalid signature")));
+                    newFontUpdateRequest("bar,2", "Invalid signature"),
+                    newAddFontFamilyRequest("<family name='foobar'>"
+                            + "  <font>foo.ttf</font>"
+                            + "  <font>bar.ttf</font>"
+                            + "</family>")));
             fail("Batch update with invalid signature should fail");
         } catch (FontManagerService.SystemFontException e) {
             // Expected
@@ -313,6 +356,11 @@
         // The state should be rolled back as a whole if one of the update requests fail.
         assertThat(dir.getFontFileMap()).containsKey("foo.ttf");
         assertThat(parser.getRevision(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(1);
+        assertThat(dir.getFontFamilyMap()).containsKey("foobar");
+        FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar");
+        assertThat(foobar.getFontList()).hasSize(1);
+        assertThat(foobar.getFontList().get(0).getFile())
+                .isEqualTo(dir.getFontFileMap().get("foo.ttf"));
     }
 
     @Test
@@ -364,7 +412,7 @@
         dir.update(Collections.singletonList(newFontUpdateRequest("test,2", GOOD_SIGNATURE)));
         try {
             dir.update(Collections.singletonList(newFontUpdateRequest("test,1", GOOD_SIGNATURE)));
-            fail("Expect IllegalArgumentException");
+            fail("Expect SystemFontException");
         } catch (FontManagerService.SystemFontException e) {
             assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING);
         }
@@ -440,7 +488,7 @@
 
         try {
             dir.update(Collections.singletonList(newFontUpdateRequest("test,1", GOOD_SIGNATURE)));
-            fail("Expect IllegalArgumentException");
+            fail("Expect SystemFontException");
         } catch (FontManagerService.SystemFontException e) {
             assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING);
         }
@@ -599,6 +647,127 @@
         assertThat(parser.getRevision(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(1);
     }
 
+    @Test
+    public void addFontFamily() throws Exception {
+        FakeFontFileParser parser = new FakeFontFileParser();
+        FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
+        UpdatableFontDir dir = new UpdatableFontDir(
+                mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil,
+                mConfigFile);
+        dir.loadFontFileMap();
+
+        dir.update(Arrays.asList(
+                newFontUpdateRequest("test,1", GOOD_SIGNATURE),
+                newAddFontFamilyRequest("<family name='test'>"
+                        + "  <font>test.ttf</font>"
+                        + "</family>")));
+        assertThat(dir.getFontFileMap()).containsKey("test.ttf");
+        assertThat(dir.getFontFamilyMap()).containsKey("test");
+        FontConfig.FontFamily test = dir.getFontFamilyMap().get("test");
+        assertThat(test.getFontList()).hasSize(1);
+        assertThat(test.getFontList().get(0).getFile())
+                .isEqualTo(dir.getFontFileMap().get("test.ttf"));
+    }
+
+    @Test
+    public void addFontFamily_noName() throws Exception {
+        FakeFontFileParser parser = new FakeFontFileParser();
+        FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
+        UpdatableFontDir dir = new UpdatableFontDir(
+                mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil,
+                mConfigFile);
+        dir.loadFontFileMap();
+
+        try {
+            dir.update(Arrays.asList(
+                    newFontUpdateRequest("test,1", GOOD_SIGNATURE),
+                    newAddFontFamilyRequest("<family lang='en'>"
+                            + "  <font>test.ttf</font>"
+                            + "</family>")));
+            fail("Expect IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // Expect
+        }
+    }
+
+    @Test
+    public void addFontFamily_fontNotAvailable() throws Exception {
+        FakeFontFileParser parser = new FakeFontFileParser();
+        FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
+        UpdatableFontDir dir = new UpdatableFontDir(
+                mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil,
+                mConfigFile);
+        dir.loadFontFileMap();
+
+        try {
+            dir.update(Arrays.asList(newAddFontFamilyRequest("<family name='test'>"
+                    + "  <font>test.ttf</font>"
+                    + "</family>")));
+            fail("Expect IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            // Expect
+        }
+    }
+
+    @Test
+    public void getSystemFontConfig() throws Exception {
+        FakeFontFileParser parser = new FakeFontFileParser();
+        FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
+        UpdatableFontDir dir = new UpdatableFontDir(
+                mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil,
+                mConfigFile);
+        dir.loadFontFileMap();
+        // We assume we have monospace.
+        assertNamedFamilyExists(dir.getSystemFontConfig(), "monospace");
+
+        dir.update(Arrays.asList(
+                newFontUpdateRequest("test,1", GOOD_SIGNATURE),
+                // Updating an existing font family.
+                newAddFontFamilyRequest("<family name='monospace'>"
+                        + "  <font>test.ttf</font>"
+                        + "</family>"),
+                // Adding a new font family.
+                newAddFontFamilyRequest("<family name='test'>"
+                        + "  <font>test.ttf</font>"
+                        + "</family>")));
+        FontConfig fontConfig = dir.getSystemFontConfig();
+        assertNamedFamilyExists(fontConfig, "monospace");
+        FontConfig.FontFamily monospace = getLastFamily(fontConfig, "monospace");
+        assertThat(monospace.getFontList()).hasSize(1);
+        assertThat(monospace.getFontList().get(0).getFile())
+                .isEqualTo(dir.getFontFileMap().get("test.ttf"));
+        assertNamedFamilyExists(fontConfig, "test");
+        assertThat(getLastFamily(fontConfig, "test").getFontList())
+                .isEqualTo(monospace.getFontList());
+    }
+
+    @Test
+    public void getSystemFontConfig_preserveFirstFontFamily() throws Exception {
+        FakeFontFileParser parser = new FakeFontFileParser();
+        FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
+        UpdatableFontDir dir = new UpdatableFontDir(
+                mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil,
+                mConfigFile);
+        dir.loadFontFileMap();
+        assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty();
+        FontConfig.FontFamily firstFontFamily = dir.getSystemFontConfig().getFontFamilies().get(0);
+        assertThat(firstFontFamily.getName()).isNotEmpty();
+
+        dir.update(Arrays.asList(
+                newFontUpdateRequest("test,1", GOOD_SIGNATURE),
+                newAddFontFamilyRequest("<family name='" + firstFontFamily.getName() + "'>"
+                        + "  <font>test.ttf</font>"
+                        + "</family>")));
+        FontConfig fontConfig = dir.getSystemFontConfig();
+        assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty();
+        assertThat(fontConfig.getFontFamilies().get(0)).isEqualTo(firstFontFamily);
+        FontConfig.FontFamily updated = getLastFamily(fontConfig, firstFontFamily.getName());
+        assertThat(updated.getFontList()).hasSize(1);
+        assertThat(updated.getFontList().get(0).getFile())
+                .isEqualTo(dir.getFontFileMap().get("test.ttf"));
+        assertThat(updated).isNotEqualTo(firstFontFamily);
+    }
+
     private FontUpdateRequest newFontUpdateRequest(String content, String signature)
             throws Exception {
         File file = File.createTempFile("font", "ttf", mCacheDir);
@@ -608,10 +777,36 @@
                 signature.getBytes());
     }
 
+    private static FontUpdateRequest newAddFontFamilyRequest(String xml) throws Exception {
+        XmlPullParser parser = Xml.newPullParser();
+        ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
+        parser.setInput(is, "UTF-8");
+        parser.nextTag();
+        FontConfig.FontFamily fontFamily = FontListParser.readFamily(parser, "", null);
+        return new FontUpdateRequest(fontFamily);
+    }
+
     private void writeConfig(PersistentSystemFontConfig.Config config,
             File file) throws IOException {
         try (FileOutputStream fos = new FileOutputStream(file)) {
             PersistentSystemFontConfig.writeToXml(fos, config);
         }
     }
+
+    // Returns the last family with the given name, which will be used for creating Typeface.
+    private static FontConfig.FontFamily getLastFamily(FontConfig fontConfig, String familyName) {
+        List<FontConfig.FontFamily> fontFamilies = fontConfig.getFontFamilies();
+        for (int i = fontFamilies.size() - 1; i >= 0; i--) {
+            if (familyName.equals(fontFamilies.get(i).getName())) {
+                return fontFamilies.get(i);
+            }
+        }
+        return null;
+    }
+
+    private static void assertNamedFamilyExists(FontConfig fontConfig, String familyName) {
+        assertThat(fontConfig.getFontFamilies().stream()
+                .map(FontConfig.FontFamily::getName)
+                .collect(Collectors.toSet())).contains(familyName);
+    }
 }