• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.graphics.fonts;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import static org.junit.Assert.fail;
23 
24 import android.content.Context;
25 import android.graphics.FontListParser;
26 import android.graphics.fonts.FontManager;
27 import android.graphics.fonts.FontStyle;
28 import android.graphics.fonts.FontUpdateRequest;
29 import android.graphics.fonts.SystemFonts;
30 import android.os.FileUtils;
31 import android.os.ParcelFileDescriptor;
32 import android.platform.test.annotations.Presubmit;
33 import android.system.Os;
34 import android.text.FontConfig;
35 import android.util.Xml;
36 
37 import androidx.test.InstrumentationRegistry;
38 import androidx.test.filters.SmallTest;
39 import androidx.test.runner.AndroidJUnit4;
40 
41 import org.junit.After;
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.xmlpull.v1.XmlPullParser;
46 
47 import java.io.ByteArrayInputStream;
48 import java.io.File;
49 import java.io.FileInputStream;
50 import java.io.FileOutputStream;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.nio.charset.StandardCharsets;
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.Collections;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Set;
61 import java.util.function.Function;
62 import java.util.function.Supplier;
63 import java.util.stream.Collectors;
64 
65 @Presubmit
66 @SmallTest
67 @RunWith(AndroidJUnit4.class)
68 public final class UpdatableFontDirTest {
69 
70     /**
71      * A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files,
72      * this test uses fake font files. A fake font file has its PostScript naem and revision as the
73      * file content.
74      */
75     private static class FakeFontFileParser implements UpdatableFontDir.FontFileParser {
76         @Override
getPostScriptName(File file)77         public String getPostScriptName(File file) throws IOException {
78             String content = FileUtils.readTextFile(file, 100, "");
79             return content.split(",")[2];
80         }
81 
82         @Override
buildFontFileName(File file)83         public String buildFontFileName(File file) throws IOException {
84             String content = FileUtils.readTextFile(file, 100, "");
85             return content.split(",")[0];
86         }
87 
88         @Override
getRevision(File file)89         public long getRevision(File file) throws IOException {
90             String content = FileUtils.readTextFile(file, 100, "");
91             return Long.parseLong(content.split(",")[1]);
92         }
93 
94         @Override
tryToCreateTypeface(File file)95         public void tryToCreateTypeface(File file) throws Throwable {
96         }
97     }
98 
99     // FakeFsverityUtil will successfully set up fake fs-verity if the signature is GOOD_SIGNATURE.
100     private static final String GOOD_SIGNATURE = "Good signature";
101 
102     /** A fake FsverityUtil to keep fake verity bit in memory. */
103     private static class FakeFsverityUtil implements UpdatableFontDir.FsverityUtil {
104         private final Set<String> mHasFsverityPaths = new HashSet<>();
105 
remove(String name)106         public void remove(String name) {
107             mHasFsverityPaths.remove(name);
108         }
109 
110         @Override
hasFsverity(String path)111         public boolean hasFsverity(String path) {
112             return mHasFsverityPaths.contains(path);
113         }
114 
115         @Override
setUpFsverity(String path, byte[] pkcs7Signature)116         public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException {
117             String fakeSignature = new String(pkcs7Signature, StandardCharsets.UTF_8);
118             if (GOOD_SIGNATURE.equals(fakeSignature)) {
119                 mHasFsverityPaths.add(path);
120             } else {
121                 throw new IOException("Failed to set up fake fs-verity");
122             }
123         }
124 
125         @Override
rename(File src, File dest)126         public boolean rename(File src, File dest) {
127             if (src.renameTo(dest)) {
128                 mHasFsverityPaths.remove(src.getAbsolutePath());
129                 mHasFsverityPaths.add(dest.getAbsolutePath());
130                 return true;
131             }
132             return false;
133         }
134     }
135 
136     private static final long CURRENT_TIME = 1234567890L;
137 
138     private File mCacheDir;
139     private File mUpdatableFontFilesDir;
140     private File mConfigFile;
141     private List<File> mPreinstalledFontDirs;
142     private final Supplier<Long> mCurrentTimeSupplier = () -> CURRENT_TIME;
143     private final Function<Map<String, File>, FontConfig> mConfigSupplier =
144             (map) -> SystemFonts.getSystemFontConfig(map, 0, 0);
145     private FakeFontFileParser mParser;
146     private FakeFsverityUtil mFakeFsverityUtil;
147 
148     @SuppressWarnings("ResultOfMethodCallIgnored")
149     @Before
setUp()150     public void setUp() {
151         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
152         mCacheDir = new File(context.getCacheDir(), "UpdatableFontDirTest");
153         FileUtils.deleteContentsAndDir(mCacheDir);
154         mCacheDir.mkdirs();
155         mUpdatableFontFilesDir = new File(mCacheDir, "updatable_fonts");
156         mUpdatableFontFilesDir.mkdir();
157         mPreinstalledFontDirs = new ArrayList<>();
158         mPreinstalledFontDirs.add(new File(mCacheDir, "system_fonts"));
159         mPreinstalledFontDirs.add(new File(mCacheDir, "product_fonts"));
160         for (File dir : mPreinstalledFontDirs) {
161             dir.mkdir();
162         }
163         mConfigFile = new File(mCacheDir, "config.xml");
164         mParser = new FakeFontFileParser();
165         mFakeFsverityUtil = new FakeFsverityUtil();
166     }
167 
168     @After
tearDown()169     public void tearDown() {
170         FileUtils.deleteContentsAndDir(mCacheDir);
171     }
172 
173     @Test
construct()174     public void construct() throws Exception {
175         long expectedModifiedDate = CURRENT_TIME / 2;
176         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
177         config.lastModifiedMillis = expectedModifiedDate;
178         writeConfig(config, mConfigFile);
179         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
180                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
181                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
182         dirForPreparation.loadFontFileMap();
183         assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis())
184                 .isEqualTo(expectedModifiedDate);
185         dirForPreparation.update(Arrays.asList(
186                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
187                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE),
188                 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE),
189                 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE),
190                 newAddFontFamilyRequest("<family name='foobar'>"
191                         + "  <font>foo.ttf</font>"
192                         + "  <font>bar.ttf</font>"
193                         + "</family>")));
194         // Verifies that getLastModifiedTimeMillis() returns the value of currentTimeMillis.
195         assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis())
196                 .isEqualTo(CURRENT_TIME);
197         // Four font dirs are created.
198         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
199         assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis())
200                 .isNotEqualTo(expectedModifiedDate);
201 
202         UpdatableFontDir dir = new UpdatableFontDir(
203                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
204                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
205         dir.loadFontFileMap();
206         assertThat(dir.getPostScriptMap()).containsKey("foo");
207         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(3);
208         assertThat(dir.getPostScriptMap()).containsKey("bar");
209         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(4);
210         // Outdated font dir should be deleted.
211         assertThat(mUpdatableFontFilesDir.list()).hasLength(2);
212         assertNamedFamilyExists(dir.getSystemFontConfig(), "foobar");
213         assertThat(dir.getFontFamilyMap()).containsKey("foobar");
214         FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar");
215         assertThat(foobar.getFontList()).hasSize(2);
216         assertThat(foobar.getFontList().get(0).getFile())
217                 .isEqualTo(dir.getPostScriptMap().get("foo"));
218         assertThat(foobar.getFontList().get(1).getFile())
219                 .isEqualTo(dir.getPostScriptMap().get("bar"));
220     }
221 
222     @Test
construct_empty()223     public void construct_empty() {
224         UpdatableFontDir dir = new UpdatableFontDir(
225                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
226                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
227         dir.loadFontFileMap();
228         assertThat(dir.getPostScriptMap()).isEmpty();
229         assertThat(dir.getFontFamilyMap()).isEmpty();
230     }
231 
232     @Test
construct_missingFsverity()233     public void construct_missingFsverity() throws Exception {
234         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
235                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
236                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
237         dirForPreparation.loadFontFileMap();
238         dirForPreparation.update(Arrays.asList(
239                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
240                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE),
241                 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE),
242                 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE),
243                 newAddFontFamilyRequest("<family name='foobar'>"
244                         + "  <font>foo.ttf</font>"
245                         + "  <font>bar.ttf</font>"
246                         + "</family>")));
247         // Four font dirs are created.
248         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
249 
250         mFakeFsverityUtil.remove(
251                 dirForPreparation.getPostScriptMap().get("foo").getAbsolutePath());
252         UpdatableFontDir dir = new UpdatableFontDir(
253                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
254                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
255         dir.loadFontFileMap();
256         assertThat(dir.getPostScriptMap()).isEmpty();
257         // All font dirs (including dir for "bar.ttf") should be deleted.
258         assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
259         assertThat(dir.getFontFamilyMap()).isEmpty();
260     }
261 
262     @Test
construct_fontNameMismatch()263     public void construct_fontNameMismatch() throws Exception {
264         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
265                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
266                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
267         dirForPreparation.loadFontFileMap();
268         dirForPreparation.update(Arrays.asList(
269                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
270                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE),
271                 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE),
272                 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE),
273                 newAddFontFamilyRequest("<family name='foobar'>"
274                         + "  <font>foo.ttf</font>"
275                         + "  <font>bar.ttf</font>"
276                         + "</family>")));
277         // Four font dirs are created.
278         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
279 
280         // Overwrite "foo.ttf" with wrong contents.
281         FileUtils.stringToFile(dirForPreparation.getPostScriptMap().get("foo"), "bar,4");
282 
283         UpdatableFontDir dir = new UpdatableFontDir(
284                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
285                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
286         dir.loadFontFileMap();
287         assertThat(dir.getPostScriptMap()).isEmpty();
288         // All font dirs (including dir for "bar.ttf") should be deleted.
289         assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
290         assertThat(dir.getFontFamilyMap()).isEmpty();
291     }
292 
293     @Test
construct_olderThanPreinstalledFont()294     public void construct_olderThanPreinstalledFont() throws Exception {
295         Function<Map<String, File>, FontConfig> configSupplier = (map) -> {
296             FontConfig.Font fooFont = new FontConfig.Font(
297                     new File(mPreinstalledFontDirs.get(0), "foo.ttf"), null, "foo",
298                     new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
299             FontConfig.Font barFont = new FontConfig.Font(
300                     new File(mPreinstalledFontDirs.get(1), "bar.ttf"), null, "bar",
301                     new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
302 
303             FontConfig.FontFamily family = new FontConfig.FontFamily(
304                     Arrays.asList(fooFont, barFont), "sans-serif", null,
305                     FontConfig.FontFamily.VARIANT_DEFAULT);
306             return new FontConfig(Collections.singletonList(family),
307                     Collections.emptyList(), 0, 1);
308         };
309 
310         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
311                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
312                 mConfigFile, mCurrentTimeSupplier, configSupplier);
313         dirForPreparation.loadFontFileMap();
314         dirForPreparation.update(Arrays.asList(
315                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
316                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE),
317                 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE),
318                 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE),
319                 newAddFontFamilyRequest("<family name='foobar'>"
320                         + "  <font>foo.ttf</font>"
321                         + "  <font>bar.ttf</font>"
322                         + "</family>")));
323         // Four font dirs are created.
324         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
325 
326         // Add preinstalled fonts.
327         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "foo.ttf"), "foo,5,foo");
328         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,1,bar");
329         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,2,bar");
330         UpdatableFontDir dir = new UpdatableFontDir(
331                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
332                 mConfigFile, mCurrentTimeSupplier, configSupplier);
333         dir.loadFontFileMap();
334         // For foo.ttf, preinstalled font (revision 5) should be used.
335         assertThat(dir.getPostScriptMap()).doesNotContainKey("foo");
336         // For bar.ttf, updated font (revision 4) should be used.
337         assertThat(dir.getPostScriptMap()).containsKey("bar");
338         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(4);
339         // Outdated font dir should be deleted.
340         // We don't delete bar.ttf in this case, because it's normal that OTA updates preinstalled
341         // fonts.
342         assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
343         // Font family depending on obsoleted font should be removed.
344         assertThat(dir.getFontFamilyMap()).isEmpty();
345     }
346 
347     @Test
construct_failedToLoadConfig()348     public void construct_failedToLoadConfig() throws Exception {
349         UpdatableFontDir dir = new UpdatableFontDir(
350                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
351                 new File("/dev/null"), mCurrentTimeSupplier, mConfigSupplier);
352         dir.loadFontFileMap();
353         assertThat(dir.getPostScriptMap()).isEmpty();
354         assertThat(dir.getFontFamilyMap()).isEmpty();
355     }
356 
357     @Test
construct_afterBatchFailure()358     public void construct_afterBatchFailure() throws Exception {
359         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
360                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
361                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
362         dirForPreparation.loadFontFileMap();
363         dirForPreparation.update(Arrays.asList(
364                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
365                 newAddFontFamilyRequest("<family name='foobar'>"
366                         + "  <font>foo.ttf</font>"
367                         + "</family>")));
368         try {
369             dirForPreparation.update(Arrays.asList(
370                     newFontUpdateRequest("foo.ttf,2,foo", GOOD_SIGNATURE),
371                     newFontUpdateRequest("bar.ttf,2,bar", "Invalid signature"),
372                     newAddFontFamilyRequest("<family name='foobar'>"
373                             + "  <font>foo.ttf</font>"
374                             + "  <font>bar.ttf</font>"
375                             + "</family>")));
376             fail("Batch update with invalid signature should fail");
377         } catch (FontManagerService.SystemFontException e) {
378             // Expected
379         }
380 
381         UpdatableFontDir dir = new UpdatableFontDir(
382                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
383                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
384         dir.loadFontFileMap();
385         // The state should be rolled back as a whole if one of the update requests fail.
386         assertThat(dir.getPostScriptMap()).containsKey("foo");
387         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
388         assertThat(dir.getFontFamilyMap()).containsKey("foobar");
389         FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar");
390         assertThat(foobar.getFontList()).hasSize(1);
391         assertThat(foobar.getFontList().get(0).getFile())
392                 .isEqualTo(dir.getPostScriptMap().get("foo"));
393     }
394 
395     @Test
loadFontFileMap_twice()396     public void loadFontFileMap_twice() throws Exception {
397         UpdatableFontDir dir = new UpdatableFontDir(
398                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
399                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
400         dir.loadFontFileMap();
401         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
402                 GOOD_SIGNATURE)));
403         assertThat(dir.getPostScriptMap()).containsKey("test");
404         File fontFile = dir.getPostScriptMap().get("test");
405         dir.loadFontFileMap();
406         assertThat(dir.getPostScriptMap().get("test")).isEqualTo(fontFile);
407     }
408 
409     @Test
installFontFile()410     public void installFontFile() throws Exception {
411         UpdatableFontDir dir = new UpdatableFontDir(
412                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
413                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
414         dir.loadFontFileMap();
415 
416         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
417                 GOOD_SIGNATURE)));
418         assertThat(dir.getPostScriptMap()).containsKey("test");
419         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1);
420         File fontFile = dir.getPostScriptMap().get("test");
421         assertThat(Os.stat(fontFile.getAbsolutePath()).st_mode & 0777).isEqualTo(0644);
422         File fontDir = fontFile.getParentFile();
423         assertThat(Os.stat(fontDir.getAbsolutePath()).st_mode & 0777).isEqualTo(0711);
424     }
425 
426     @Test
installFontFile_upgrade()427     public void installFontFile_upgrade() throws Exception {
428         UpdatableFontDir dir = new UpdatableFontDir(
429                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
430                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
431         dir.loadFontFileMap();
432 
433         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
434                 GOOD_SIGNATURE)));
435         Map<String, File> mapBeforeUpgrade = dir.getPostScriptMap();
436         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
437                 GOOD_SIGNATURE)));
438         assertThat(dir.getPostScriptMap()).containsKey("test");
439         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2);
440         assertThat(mapBeforeUpgrade).containsKey("test");
441         assertWithMessage("Older fonts should not be deleted until next loadFontFileMap")
442                 .that(mParser.getRevision(mapBeforeUpgrade.get("test"))).isEqualTo(1);
443         // Check that updatedFontDirs is pruned.
444         assertWithMessage("config.updatedFontDirs should only list latest active dirs")
445                 .that(readConfig(mConfigFile).updatedFontDirs)
446                 .containsExactly(dir.getPostScriptMap().get("test").getParentFile().getName());
447     }
448 
449     @Test
installFontFile_systemFontHasPSNameDifferentFromFileName()450     public void installFontFile_systemFontHasPSNameDifferentFromFileName() throws Exception {
451 
452         // Setup the environment that the system installed font file named "foo.ttf" has PostScript
453         // name "bar".
454         File file = new File(mPreinstalledFontDirs.get(0), "foo.ttf");
455         FileUtils.stringToFile(file, "foo.ttf,1,bar");
456         UpdatableFontDir dir = new UpdatableFontDir(
457                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
458                 mConfigFile, mCurrentTimeSupplier, (map) -> {
459             FontConfig.Font font = new FontConfig.Font(
460                     file, null, "bar", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT),
461                     0, null, null);
462             FontConfig.FontFamily family = new FontConfig.FontFamily(
463                     Collections.singletonList(font), "sans-serif", null,
464                     FontConfig.FontFamily.VARIANT_DEFAULT);
465             return new FontConfig(Collections.singletonList(family),
466                     Collections.emptyList(), 0, 1);
467         });
468         dir.loadFontFileMap();
469 
470         dir.update(Collections.singletonList(newFontUpdateRequest("bar.ttf,2,bar",
471                 GOOD_SIGNATURE)));
472         assertThat(dir.getPostScriptMap()).containsKey("bar");
473         assertThat(dir.getPostScriptMap().size()).isEqualTo(1);
474         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2);
475         File fontFile = dir.getPostScriptMap().get("bar");
476         assertThat(Os.stat(fontFile.getAbsolutePath()).st_mode & 0777).isEqualTo(0644);
477         File fontDir = fontFile.getParentFile();
478         assertThat(Os.stat(fontDir.getAbsolutePath()).st_mode & 0777).isEqualTo(0711);
479     }
480 
481     @Test
installFontFile_sameVersion()482     public void installFontFile_sameVersion() throws Exception {
483         UpdatableFontDir dir = new UpdatableFontDir(
484                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
485                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
486         dir.loadFontFileMap();
487 
488         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
489                 GOOD_SIGNATURE)));
490         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
491                 GOOD_SIGNATURE)));
492         assertThat(dir.getPostScriptMap()).containsKey("test");
493         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1);
494     }
495 
496     @Test
installFontFile_downgrade()497     public void installFontFile_downgrade() throws Exception {
498         UpdatableFontDir dir = new UpdatableFontDir(
499                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
500                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
501         dir.loadFontFileMap();
502 
503         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
504                 GOOD_SIGNATURE)));
505         try {
506             dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
507                     GOOD_SIGNATURE)));
508             fail("Expect SystemFontException");
509         } catch (FontManagerService.SystemFontException e) {
510             assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING);
511         }
512         assertThat(dir.getPostScriptMap()).containsKey("test");
513         assertWithMessage("Font should not be downgraded to an older revision")
514                 .that(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2);
515         // Check that updatedFontDirs is not updated.
516         assertWithMessage("config.updatedFontDirs should only list latest active dirs")
517                 .that(readConfig(mConfigFile).updatedFontDirs)
518                 .containsExactly(dir.getPostScriptMap().get("test").getParentFile().getName());
519     }
520 
521     @Test
installFontFile_multiple()522     public void installFontFile_multiple() throws Exception {
523         UpdatableFontDir dir = new UpdatableFontDir(
524                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
525                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
526         dir.loadFontFileMap();
527 
528         dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
529                 GOOD_SIGNATURE)));
530         dir.update(Collections.singletonList(newFontUpdateRequest("bar.ttf,2,bar",
531                 GOOD_SIGNATURE)));
532         assertThat(dir.getPostScriptMap()).containsKey("foo");
533         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
534         assertThat(dir.getPostScriptMap()).containsKey("bar");
535         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2);
536     }
537 
538     @Test
installFontFile_batch()539     public void installFontFile_batch() throws Exception {
540         UpdatableFontDir dir = new UpdatableFontDir(
541                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
542                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
543         dir.loadFontFileMap();
544 
545         dir.update(Arrays.asList(
546                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
547                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE)));
548         assertThat(dir.getPostScriptMap()).containsKey("foo");
549         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
550         assertThat(dir.getPostScriptMap()).containsKey("bar");
551         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2);
552     }
553 
554     @Test
installFontFile_invalidSignature()555     public void installFontFile_invalidSignature() throws Exception {
556         UpdatableFontDir dir = new UpdatableFontDir(
557                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
558                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
559         dir.loadFontFileMap();
560 
561         try {
562             dir.update(
563                     Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
564                             "Invalid signature")));
565             fail("Expect SystemFontException");
566         } catch (FontManagerService.SystemFontException e) {
567             assertThat(e.getErrorCode())
568                     .isEqualTo(FontManager.RESULT_ERROR_VERIFICATION_FAILURE);
569         }
570         assertThat(dir.getPostScriptMap()).isEmpty();
571     }
572 
573     @Test
installFontFile_preinstalled_upgrade()574     public void installFontFile_preinstalled_upgrade() throws Exception {
575         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"),
576                 "test.ttf,1,test");
577         UpdatableFontDir dir = new UpdatableFontDir(
578                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
579                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
580         dir.loadFontFileMap();
581 
582         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
583                 GOOD_SIGNATURE)));
584         assertThat(dir.getPostScriptMap()).containsKey("test");
585         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2);
586     }
587 
588     @Test
installFontFile_preinstalled_sameVersion()589     public void installFontFile_preinstalled_sameVersion() throws Exception {
590         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"),
591                 "test.ttf,1,test");
592         UpdatableFontDir dir = new UpdatableFontDir(
593                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
594                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
595         dir.loadFontFileMap();
596 
597         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
598                 GOOD_SIGNATURE)));
599         assertThat(dir.getPostScriptMap()).containsKey("test");
600         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1);
601     }
602 
603     @Test
installFontFile_preinstalled_downgrade()604     public void installFontFile_preinstalled_downgrade() throws Exception {
605         File file = new File(mPreinstalledFontDirs.get(0), "test.ttf");
606         FileUtils.stringToFile(file, "test.ttf,2,test");
607         UpdatableFontDir dir = new UpdatableFontDir(
608                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
609                 mConfigFile, mCurrentTimeSupplier, (map) -> {
610             FontConfig.Font font = new FontConfig.Font(
611                     file, null, "test", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null,
612                     null);
613             FontConfig.FontFamily family = new FontConfig.FontFamily(
614                     Collections.singletonList(font), "sans-serif", null,
615                     FontConfig.FontFamily.VARIANT_DEFAULT);
616             return new FontConfig(Collections.singletonList(family), Collections.emptyList(), 0, 1);
617         });
618         dir.loadFontFileMap();
619 
620         try {
621             dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
622                     GOOD_SIGNATURE)));
623             fail("Expect SystemFontException");
624         } catch (FontManagerService.SystemFontException e) {
625             assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING);
626         }
627         assertThat(dir.getPostScriptMap()).isEmpty();
628     }
629 
630     @Test
installFontFile_failedToWriteConfigXml()631     public void installFontFile_failedToWriteConfigXml() throws Exception {
632         long expectedModifiedDate = 1234567890;
633         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"),
634                 "test.ttf,1,test");
635 
636         File readonlyDir = new File(mCacheDir, "readonly");
637         assertThat(readonlyDir.mkdir()).isTrue();
638         File readonlyFile = new File(readonlyDir, "readonly_config.xml");
639 
640         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
641         config.lastModifiedMillis = expectedModifiedDate;
642         writeConfig(config, readonlyFile);
643 
644         assertThat(readonlyDir.setWritable(false, false)).isTrue();
645         try {
646             UpdatableFontDir dir = new UpdatableFontDir(
647                     mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
648                     readonlyFile, mCurrentTimeSupplier, mConfigSupplier);
649             dir.loadFontFileMap();
650 
651             try {
652                 dir.update(
653                         Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
654                                 GOOD_SIGNATURE)));
655             } catch (FontManagerService.SystemFontException e) {
656                 assertThat(e.getErrorCode())
657                         .isEqualTo(FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG);
658             }
659             assertThat(dir.getSystemFontConfig().getLastModifiedTimeMillis())
660                     .isEqualTo(expectedModifiedDate);
661             assertThat(dir.getPostScriptMap()).isEmpty();
662         } finally {
663             assertThat(readonlyDir.setWritable(true, true)).isTrue();
664         }
665     }
666 
667     @Test
installFontFile_failedToParsePostScript()668     public void installFontFile_failedToParsePostScript() throws Exception {
669         UpdatableFontDir dir = new UpdatableFontDir(
670                 mUpdatableFontFilesDir,
671                 new UpdatableFontDir.FontFileParser() {
672 
673                     @Override
674                     public String getPostScriptName(File file) throws IOException {
675                         return null;
676                     }
677 
678                     @Override
679                     public String buildFontFileName(File file) throws IOException {
680                         return null;
681                     }
682 
683                     @Override
684                     public long getRevision(File file) throws IOException {
685                         return 0;
686                     }
687 
688                     @Override
689                     public void tryToCreateTypeface(File file) throws IOException {
690                     }
691                 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
692         dir.loadFontFileMap();
693 
694         try {
695             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
696                     GOOD_SIGNATURE)));
697             fail("Expect SystemFontException");
698         } catch (FontManagerService.SystemFontException e) {
699             assertThat(e.getErrorCode())
700                     .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_NAME);
701         }
702         assertThat(dir.getPostScriptMap()).isEmpty();
703     }
704 
705     @Test
installFontFile_failedToParsePostScriptName_invalidFont()706     public void installFontFile_failedToParsePostScriptName_invalidFont() throws Exception {
707         UpdatableFontDir dir = new UpdatableFontDir(
708                 mUpdatableFontFilesDir,
709                 new UpdatableFontDir.FontFileParser() {
710                     @Override
711                     public String getPostScriptName(File file) throws IOException {
712                         throw new IOException();
713                     }
714 
715                     @Override
716                     public String buildFontFileName(File file) throws IOException {
717                         throw new IOException();
718                     }
719 
720                     @Override
721                     public long getRevision(File file) throws IOException {
722                         return 0;
723                     }
724 
725                     @Override
726                     public void tryToCreateTypeface(File file) throws IOException {
727                     }
728                 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
729         dir.loadFontFileMap();
730 
731         try {
732             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
733                     GOOD_SIGNATURE)));
734             fail("Expect SystemFontException");
735         } catch (FontManagerService.SystemFontException e) {
736             assertThat(e.getErrorCode())
737                     .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_FILE);
738         }
739         assertThat(dir.getPostScriptMap()).isEmpty();
740     }
741 
742     @Test
installFontFile_failedToCreateTypeface()743     public void installFontFile_failedToCreateTypeface() throws Exception {
744         UpdatableFontDir dir = new UpdatableFontDir(
745                 mUpdatableFontFilesDir,
746                 new UpdatableFontDir.FontFileParser() {
747                     @Override
748                     public String getPostScriptName(File file) throws IOException {
749                         return mParser.getPostScriptName(file);
750                     }
751 
752                     @Override
753                     public String buildFontFileName(File file) throws IOException {
754                         return mParser.buildFontFileName(file);
755                     }
756 
757                     @Override
758                     public long getRevision(File file) throws IOException {
759                         return mParser.getRevision(file);
760                     }
761 
762                     @Override
763                     public void tryToCreateTypeface(File file) throws IOException {
764                         throw new IOException();
765                     }
766                 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
767         dir.loadFontFileMap();
768 
769         try {
770             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
771                     GOOD_SIGNATURE)));
772             fail("Expect SystemFontException");
773         } catch (FontManagerService.SystemFontException e) {
774             assertThat(e.getErrorCode())
775                     .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_FILE);
776         }
777         assertThat(dir.getPostScriptMap()).isEmpty();
778     }
779 
780     @Test
installFontFile_renameToPsNameFailure()781     public void installFontFile_renameToPsNameFailure() throws Exception {
782         UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() {
783 
784             @Override
785             public boolean hasFsverity(String path) {
786                 return mFakeFsverityUtil.hasFsverity(path);
787             }
788 
789             @Override
790             public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException {
791                 mFakeFsverityUtil.setUpFsverity(path, pkcs7Signature);
792             }
793 
794             @Override
795             public boolean rename(File src, File dest) {
796                 return false;
797             }
798         };
799         UpdatableFontDir dir = new UpdatableFontDir(
800                 mUpdatableFontFilesDir, mParser, fakeFsverityUtil,
801                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
802         dir.loadFontFileMap();
803 
804         try {
805             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
806                     GOOD_SIGNATURE)));
807             fail("Expect SystemFontException");
808         } catch (FontManagerService.SystemFontException e) {
809             assertThat(e.getErrorCode())
810                     .isEqualTo(FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE);
811         }
812         assertThat(dir.getPostScriptMap()).isEmpty();
813     }
814 
815     @Test
installFontFile_batchFailure()816     public void installFontFile_batchFailure() throws Exception {
817         UpdatableFontDir dir = new UpdatableFontDir(
818                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
819                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
820         dir.loadFontFileMap();
821 
822         dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
823                 GOOD_SIGNATURE)));
824         try {
825             dir.update(Arrays.asList(
826                     newFontUpdateRequest("foo.ttf,2,foo", GOOD_SIGNATURE),
827                     newFontUpdateRequest("bar.ttf,2,bar", "Invalid signature")));
828             fail("Batch update with invalid signature should fail");
829         } catch (FontManagerService.SystemFontException e) {
830             // Expected
831         }
832         // The state should be rolled back as a whole if one of the update requests fail.
833         assertThat(dir.getPostScriptMap()).containsKey("foo");
834         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
835     }
836 
837     @Test
addFontFamily()838     public void addFontFamily() throws Exception {
839         UpdatableFontDir dir = new UpdatableFontDir(
840                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
841                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
842         dir.loadFontFileMap();
843 
844         dir.update(Arrays.asList(
845                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
846                 newAddFontFamilyRequest("<family name='test'>"
847                         + "  <font>test.ttf</font>"
848                         + "</family>")));
849         assertThat(dir.getPostScriptMap()).containsKey("test");
850         assertThat(dir.getFontFamilyMap()).containsKey("test");
851         FontConfig.FontFamily test = dir.getFontFamilyMap().get("test");
852         assertThat(test.getFontList()).hasSize(1);
853         assertThat(test.getFontList().get(0).getFile())
854                 .isEqualTo(dir.getPostScriptMap().get("test"));
855     }
856 
857     @Test
addFontFamily_noName()858     public void addFontFamily_noName() throws Exception {
859         UpdatableFontDir dir = new UpdatableFontDir(
860                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
861                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
862         dir.loadFontFileMap();
863 
864         List<FontUpdateRequest> requests = Arrays.asList(
865                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
866                 newAddFontFamilyRequest("<family lang='en'>"
867                         + "  <font>test.ttf</font>"
868                         + "</family>"));
869         try {
870             dir.update(requests);
871             fail("Expect NullPointerException");
872         } catch (NullPointerException e) {
873             // Expect
874         }
875     }
876 
877     @Test
addFontFamily_fontNotAvailable()878     public void addFontFamily_fontNotAvailable() throws Exception {
879         UpdatableFontDir dir = new UpdatableFontDir(
880                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
881                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
882         dir.loadFontFileMap();
883 
884         try {
885             dir.update(Arrays.asList(newAddFontFamilyRequest("<family name='test'>"
886                     + "  <font>test.ttf</font>"
887                     + "</family>")));
888             fail("Expect SystemFontException");
889         } catch (FontManagerService.SystemFontException e) {
890             assertThat(e.getErrorCode())
891                     .isEqualTo(FontManager.RESULT_ERROR_FONT_NOT_FOUND);
892         }
893     }
894 
895     @Test
getSystemFontConfig()896     public void getSystemFontConfig() throws Exception {
897         UpdatableFontDir dir = new UpdatableFontDir(
898                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
899                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
900         dir.loadFontFileMap();
901         // We assume we have monospace.
902         assertNamedFamilyExists(dir.getSystemFontConfig(), "monospace");
903 
904         dir.update(Arrays.asList(
905                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
906                 // Updating an existing font family.
907                 newAddFontFamilyRequest("<family name='monospace'>"
908                         + "  <font>test.ttf</font>"
909                         + "</family>"),
910                 // Adding a new font family.
911                 newAddFontFamilyRequest("<family name='test'>"
912                         + "  <font>test.ttf</font>"
913                         + "</family>")));
914         FontConfig fontConfig = dir.getSystemFontConfig();
915         assertNamedFamilyExists(fontConfig, "monospace");
916         FontConfig.FontFamily monospace = getLastFamily(fontConfig, "monospace");
917         assertThat(monospace.getFontList()).hasSize(1);
918         assertThat(monospace.getFontList().get(0).getFile())
919                 .isEqualTo(dir.getPostScriptMap().get("test"));
920         assertNamedFamilyExists(fontConfig, "test");
921         assertThat(getLastFamily(fontConfig, "test").getFontList())
922                 .isEqualTo(monospace.getFontList());
923     }
924 
925     @Test
getSystemFontConfig_preserveFirstFontFamily()926     public void getSystemFontConfig_preserveFirstFontFamily() throws Exception {
927         UpdatableFontDir dir = new UpdatableFontDir(
928                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
929                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
930         dir.loadFontFileMap();
931         assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty();
932         FontConfig.FontFamily firstFontFamily = dir.getSystemFontConfig().getFontFamilies().get(0);
933         assertThat(firstFontFamily.getName()).isNotEmpty();
934 
935         dir.update(Arrays.asList(
936                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
937                 newAddFontFamilyRequest("<family name='" + firstFontFamily.getName() + "'>"
938                         + "  <font>test.ttf</font>"
939                         + "</family>")));
940         FontConfig fontConfig = dir.getSystemFontConfig();
941         assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty();
942         assertThat(fontConfig.getFontFamilies().get(0)).isEqualTo(firstFontFamily);
943         FontConfig.FontFamily updated = getLastFamily(fontConfig, firstFontFamily.getName());
944         assertThat(updated.getFontList()).hasSize(1);
945         assertThat(updated.getFontList().get(0).getFile())
946                 .isEqualTo(dir.getPostScriptMap().get("test"));
947         assertThat(updated).isNotEqualTo(firstFontFamily);
948     }
949 
950     @Test
deleteAllFiles()951     public void deleteAllFiles() throws Exception {
952         FakeFontFileParser parser = new FakeFontFileParser();
953         FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
954         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
955                 mUpdatableFontFilesDir, parser, fakeFsverityUtil,
956                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
957         dirForPreparation.loadFontFileMap();
958         dirForPreparation.update(Collections.singletonList(
959                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE)));
960         assertThat(mConfigFile.exists()).isTrue();
961         assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
962 
963         UpdatableFontDir.deleteAllFiles(mUpdatableFontFilesDir, mConfigFile);
964         assertThat(mConfigFile.exists()).isFalse();
965         assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
966     }
967 
newFontUpdateRequest(String content, String signature)968     private FontUpdateRequest newFontUpdateRequest(String content, String signature)
969             throws Exception {
970         File file = File.createTempFile("font", "ttf", mCacheDir);
971         FileUtils.stringToFile(file, content);
972         return new FontUpdateRequest(
973                 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY),
974                 signature.getBytes());
975     }
976 
newAddFontFamilyRequest(String xml)977     private static FontUpdateRequest newAddFontFamilyRequest(String xml) throws Exception {
978         XmlPullParser mParser = Xml.newPullParser();
979         ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
980         mParser.setInput(is, "UTF-8");
981         mParser.nextTag();
982 
983         FontConfig.FontFamily fontFamily = FontListParser.readFamily(mParser, "", null, true);
984         List<FontUpdateRequest.Font> fonts = new ArrayList<>();
985         for (FontConfig.Font font : fontFamily.getFontList()) {
986             String name = font.getFile().getName();
987             String psName = name.substring(0, name.length() - 4);  // drop suffix
988             FontUpdateRequest.Font updateFont = new FontUpdateRequest.Font(
989                     psName, font.getStyle(), font.getTtcIndex(), font.getFontVariationSettings());
990             fonts.add(updateFont);
991         }
992         FontUpdateRequest.Family family = new FontUpdateRequest.Family(fontFamily.getName(), fonts);
993         return new FontUpdateRequest(family);
994     }
995 
readConfig(File file)996     private static PersistentSystemFontConfig.Config readConfig(File file) throws Exception {
997         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
998         try (InputStream is = new FileInputStream(file)) {
999             PersistentSystemFontConfig.loadFromXml(is, config);
1000         }
1001         return config;
1002     }
1003 
writeConfig(PersistentSystemFontConfig.Config config, File file)1004     private static void writeConfig(PersistentSystemFontConfig.Config config,
1005             File file) throws IOException {
1006         try (FileOutputStream fos = new FileOutputStream(file)) {
1007             PersistentSystemFontConfig.writeToXml(fos, config);
1008         }
1009     }
1010 
1011     // Returns the last family with the given name, which will be used for creating Typeface.
getLastFamily(FontConfig fontConfig, String familyName)1012     private static FontConfig.FontFamily getLastFamily(FontConfig fontConfig, String familyName) {
1013         List<FontConfig.FontFamily> fontFamilies = fontConfig.getFontFamilies();
1014         for (int i = fontFamilies.size() - 1; i >= 0; i--) {
1015             if (familyName.equals(fontFamilies.get(i).getName())) {
1016                 return fontFamilies.get(i);
1017             }
1018         }
1019         return null;
1020     }
1021 
assertNamedFamilyExists(FontConfig fontConfig, String familyName)1022     private static void assertNamedFamilyExists(FontConfig fontConfig, String familyName) {
1023         assertThat(fontConfig.getFontFamilies().stream()
1024                 .map(FontConfig.FontFamily::getName)
1025                 .collect(Collectors.toSet())).contains(familyName);
1026     }
1027 }
1028