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
isFromTrustedProvider(String path, byte[] signature)111         public boolean isFromTrustedProvider(String path, byte[] signature) {
112             if (!mHasFsverityPaths.contains(path)) {
113                 return false;
114             }
115             String fakeSignature = new String(signature, StandardCharsets.UTF_8);
116             return GOOD_SIGNATURE.equals(fakeSignature);
117         }
118 
119         @Override
setUpFsverity(String path)120         public void setUpFsverity(String path) throws IOException {
121             mHasFsverityPaths.add(path);
122         }
123 
124         @Override
rename(File src, File dest)125         public boolean rename(File src, File dest) {
126             if (src.renameTo(dest)) {
127                 mHasFsverityPaths.remove(src.getAbsolutePath());
128                 mHasFsverityPaths.add(dest.getAbsolutePath());
129                 return true;
130             }
131             return false;
132         }
133     }
134 
135     private static final long CURRENT_TIME = 1234567890L;
136 
137     private File mCacheDir;
138     private File mUpdatableFontFilesDir;
139     private File mConfigFile;
140     private List<File> mPreinstalledFontDirs;
141     private final Supplier<Long> mCurrentTimeSupplier = () -> CURRENT_TIME;
142     private final Function<Map<String, File>, FontConfig> mConfigSupplier =
143             (map) -> SystemFonts.getSystemFontConfig(map, 0, 0);
144     private FakeFontFileParser mParser;
145     private FakeFsverityUtil mFakeFsverityUtil;
146 
147     @SuppressWarnings("ResultOfMethodCallIgnored")
148     @Before
setUp()149     public void setUp() {
150         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
151         mCacheDir = new File(context.getCacheDir(), "UpdatableFontDirTest");
152         FileUtils.deleteContentsAndDir(mCacheDir);
153         mCacheDir.mkdirs();
154         mUpdatableFontFilesDir = new File(mCacheDir, "updatable_fonts");
155         mUpdatableFontFilesDir.mkdir();
156         mPreinstalledFontDirs = new ArrayList<>();
157         mPreinstalledFontDirs.add(new File(mCacheDir, "system_fonts"));
158         mPreinstalledFontDirs.add(new File(mCacheDir, "product_fonts"));
159         for (File dir : mPreinstalledFontDirs) {
160             dir.mkdir();
161         }
162         mConfigFile = new File(mCacheDir, "config.xml");
163         mParser = new FakeFontFileParser();
164         mFakeFsverityUtil = new FakeFsverityUtil();
165     }
166 
167     @After
tearDown()168     public void tearDown() {
169         FileUtils.deleteContentsAndDir(mCacheDir);
170     }
171 
172     @Test
construct()173     public void construct() throws Exception {
174         long expectedModifiedDate = CURRENT_TIME / 2;
175         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
176         config.lastModifiedMillis = expectedModifiedDate;
177         writeConfig(config, mConfigFile);
178         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
179                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
180                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
181         dirForPreparation.loadFontFileMap();
182         assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis())
183                 .isEqualTo(expectedModifiedDate);
184         dirForPreparation.update(Arrays.asList(
185                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
186                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE),
187                 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE),
188                 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE),
189                 newAddFontFamilyRequest("<family name='foobar'>"
190                         + "  <font>foo.ttf</font>"
191                         + "  <font>bar.ttf</font>"
192                         + "</family>")));
193         // Verifies that getLastModifiedTimeMillis() returns the value of currentTimeMillis.
194         assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis())
195                 .isEqualTo(CURRENT_TIME);
196         // Four font dirs are created.
197         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
198         assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis())
199                 .isNotEqualTo(expectedModifiedDate);
200 
201         UpdatableFontDir dir = new UpdatableFontDir(
202                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
203                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
204         dir.loadFontFileMap();
205         assertThat(dir.getPostScriptMap()).containsKey("foo");
206         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(3);
207         assertThat(dir.getPostScriptMap()).containsKey("bar");
208         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(4);
209         // Outdated font dir should be deleted.
210         assertThat(mUpdatableFontFilesDir.list()).hasLength(2);
211         assertNamedFamilyExists(dir.getSystemFontConfig(), "foobar");
212         assertThat(dir.getFontFamilyMap()).containsKey("foobar");
213         assertThat(dir.getFontFamilyMap().get("foobar").getFamilies().size()).isEqualTo(1);
214         FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar").getFamilies().get(0);
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_missingSignatureFile()294     public void construct_missingSignatureFile() throws Exception {
295         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
296                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
297                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
298         dirForPreparation.loadFontFileMap();
299         dirForPreparation.update(Arrays.asList(
300                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE)));
301         assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
302 
303         // Remove signature file next to the font file.
304         File fontDir = dirForPreparation.getPostScriptMap().get("foo");
305         File sigFile = new File(fontDir.getParentFile(), "font.fsv_sig");
306         assertThat(sigFile.exists()).isTrue();
307         sigFile.delete();
308 
309         UpdatableFontDir dir = new UpdatableFontDir(
310                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
311                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
312         dir.loadFontFileMap();
313         // The font file should be removed and should not be loaded.
314         assertThat(dir.getPostScriptMap()).isEmpty();
315         assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
316         assertThat(dir.getFontFamilyMap()).isEmpty();
317     }
318 
319     @Test
construct_olderThanPreinstalledFont()320     public void construct_olderThanPreinstalledFont() throws Exception {
321         Function<Map<String, File>, FontConfig> configSupplier = (map) -> {
322             FontConfig.Font fooFont = new FontConfig.Font(
323                     new File(mPreinstalledFontDirs.get(0), "foo.ttf"), null, "foo",
324                     new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
325             FontConfig.Font barFont = new FontConfig.Font(
326                     new File(mPreinstalledFontDirs.get(1), "bar.ttf"), null, "bar",
327                     new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null);
328 
329             FontConfig.FontFamily family = new FontConfig.FontFamily(
330                     Arrays.asList(fooFont, barFont), null,
331                     FontConfig.FontFamily.VARIANT_DEFAULT);
332             return new FontConfig(Collections.emptyList(),
333                     Collections.emptyList(),
334                     Collections.singletonList(new FontConfig.NamedFamilyList(
335                             Collections.singletonList(family), "sans-serif")), 0, 1);
336         };
337 
338         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
339                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
340                 mConfigFile, mCurrentTimeSupplier, configSupplier);
341         dirForPreparation.loadFontFileMap();
342         dirForPreparation.update(Arrays.asList(
343                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
344                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE),
345                 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE),
346                 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE),
347                 newAddFontFamilyRequest("<family name='foobar'>"
348                         + "  <font>foo.ttf</font>"
349                         + "  <font>bar.ttf</font>"
350                         + "</family>")));
351         // Four font dirs are created.
352         assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
353 
354         // Add preinstalled fonts.
355         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "foo.ttf"), "foo,5,foo");
356         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,1,bar");
357         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,2,bar");
358         UpdatableFontDir dir = new UpdatableFontDir(
359                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
360                 mConfigFile, mCurrentTimeSupplier, configSupplier);
361         dir.loadFontFileMap();
362         // For foo.ttf, preinstalled font (revision 5) should be used.
363         assertThat(dir.getPostScriptMap()).doesNotContainKey("foo");
364         // For bar.ttf, updated font (revision 4) should be used.
365         assertThat(dir.getPostScriptMap()).containsKey("bar");
366         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(4);
367         // Outdated font dir should be deleted.
368         // We don't delete bar.ttf in this case, because it's normal that OTA updates preinstalled
369         // fonts.
370         assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
371         // Font family depending on obsoleted font should be removed.
372         assertThat(dir.getFontFamilyMap()).isEmpty();
373     }
374 
375     @Test
construct_failedToLoadConfig()376     public void construct_failedToLoadConfig() throws Exception {
377         UpdatableFontDir dir = new UpdatableFontDir(
378                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
379                 new File("/dev/null"), mCurrentTimeSupplier, mConfigSupplier);
380         dir.loadFontFileMap();
381         assertThat(dir.getPostScriptMap()).isEmpty();
382         assertThat(dir.getFontFamilyMap()).isEmpty();
383     }
384 
385     @Test
construct_afterBatchFailure()386     public void construct_afterBatchFailure() throws Exception {
387         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
388                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
389                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
390         dirForPreparation.loadFontFileMap();
391         dirForPreparation.update(Arrays.asList(
392                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
393                 newAddFontFamilyRequest("<family name='foobar'>"
394                         + "  <font>foo.ttf</font>"
395                         + "</family>")));
396         try {
397             dirForPreparation.update(Arrays.asList(
398                     newFontUpdateRequest("foo.ttf,2,foo", GOOD_SIGNATURE),
399                     newFontUpdateRequest("bar.ttf,2,bar", "Invalid signature"),
400                     newAddFontFamilyRequest("<family name='foobar'>"
401                             + "  <font>foo.ttf</font>"
402                             + "  <font>bar.ttf</font>"
403                             + "</family>")));
404             fail("Batch update with invalid signature should fail");
405         } catch (FontManagerService.SystemFontException e) {
406             // Expected
407         }
408 
409         UpdatableFontDir dir = new UpdatableFontDir(
410                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
411                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
412         dir.loadFontFileMap();
413         // The state should be rolled back as a whole if one of the update requests fail.
414         assertThat(dir.getPostScriptMap()).containsKey("foo");
415         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
416         assertThat(dir.getFontFamilyMap()).containsKey("foobar");
417         assertThat(dir.getFontFamilyMap().get("foobar").getFamilies().size()).isEqualTo(1);
418         FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar").getFamilies().get(0);
419         assertThat(foobar.getFontList()).hasSize(1);
420         assertThat(foobar.getFontList().get(0).getFile())
421                 .isEqualTo(dir.getPostScriptMap().get("foo"));
422     }
423 
424     @Test
loadFontFileMap_twice()425     public void loadFontFileMap_twice() throws Exception {
426         UpdatableFontDir dir = new UpdatableFontDir(
427                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
428                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
429         dir.loadFontFileMap();
430         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
431                 GOOD_SIGNATURE)));
432         assertThat(dir.getPostScriptMap()).containsKey("test");
433         File fontFile = dir.getPostScriptMap().get("test");
434         dir.loadFontFileMap();
435         assertThat(dir.getPostScriptMap().get("test")).isEqualTo(fontFile);
436     }
437 
438     @Test
installFontFile()439     public void installFontFile() throws Exception {
440         UpdatableFontDir dir = new UpdatableFontDir(
441                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
442                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
443         dir.loadFontFileMap();
444 
445         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
446                 GOOD_SIGNATURE)));
447         assertThat(dir.getPostScriptMap()).containsKey("test");
448         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1);
449         File fontFile = dir.getPostScriptMap().get("test");
450         assertThat(Os.stat(fontFile.getAbsolutePath()).st_mode & 0777).isEqualTo(0644);
451         File fontDir = fontFile.getParentFile();
452         assertThat(Os.stat(fontDir.getAbsolutePath()).st_mode & 0777).isEqualTo(0711);
453     }
454 
455     @Test
installFontFile_upgrade()456     public void installFontFile_upgrade() throws Exception {
457         UpdatableFontDir dir = new UpdatableFontDir(
458                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
459                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
460         dir.loadFontFileMap();
461 
462         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
463                 GOOD_SIGNATURE)));
464         Map<String, File> mapBeforeUpgrade = dir.getPostScriptMap();
465         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
466                 GOOD_SIGNATURE)));
467         assertThat(dir.getPostScriptMap()).containsKey("test");
468         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2);
469         assertThat(mapBeforeUpgrade).containsKey("test");
470         assertWithMessage("Older fonts should not be deleted until next loadFontFileMap")
471                 .that(mParser.getRevision(mapBeforeUpgrade.get("test"))).isEqualTo(1);
472         // Check that updatedFontDirs is pruned.
473         assertWithMessage("config.updatedFontDirs should only list latest active dirs")
474                 .that(readConfig(mConfigFile).updatedFontDirs)
475                 .containsExactly(dir.getPostScriptMap().get("test").getParentFile().getName());
476     }
477 
478     @Test
installFontFile_systemFontHasPSNameDifferentFromFileName()479     public void installFontFile_systemFontHasPSNameDifferentFromFileName() throws Exception {
480 
481         // Setup the environment that the system installed font file named "foo.ttf" has PostScript
482         // name "bar".
483         File file = new File(mPreinstalledFontDirs.get(0), "foo.ttf");
484         FileUtils.stringToFile(file, "foo.ttf,1,bar");
485         UpdatableFontDir dir = new UpdatableFontDir(
486                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
487                 mConfigFile, mCurrentTimeSupplier, (map) -> {
488             FontConfig.Font font = new FontConfig.Font(
489                     file, null, "bar", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT),
490                     0, null, null);
491             FontConfig.FontFamily family = new FontConfig.FontFamily(
492                     Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT);
493             return new FontConfig(
494                     Collections.emptyList(),
495                     Collections.emptyList(),
496                     Collections.singletonList(new FontConfig.NamedFamilyList(
497                             Collections.singletonList(family), "sans-serif")), 0, 1);
498         });
499         dir.loadFontFileMap();
500 
501         dir.update(Collections.singletonList(newFontUpdateRequest("bar.ttf,2,bar",
502                 GOOD_SIGNATURE)));
503         assertThat(dir.getPostScriptMap()).containsKey("bar");
504         assertThat(dir.getPostScriptMap().size()).isEqualTo(1);
505         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2);
506         File fontFile = dir.getPostScriptMap().get("bar");
507         assertThat(Os.stat(fontFile.getAbsolutePath()).st_mode & 0777).isEqualTo(0644);
508         File fontDir = fontFile.getParentFile();
509         assertThat(Os.stat(fontDir.getAbsolutePath()).st_mode & 0777).isEqualTo(0711);
510     }
511 
512     @Test
installFontFile_sameVersion()513     public void installFontFile_sameVersion() throws Exception {
514         UpdatableFontDir dir = new UpdatableFontDir(
515                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
516                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
517         dir.loadFontFileMap();
518 
519         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
520                 GOOD_SIGNATURE)));
521         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
522                 GOOD_SIGNATURE)));
523         assertThat(dir.getPostScriptMap()).containsKey("test");
524         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1);
525     }
526 
527     @Test
installFontFile_downgrade()528     public void installFontFile_downgrade() throws Exception {
529         UpdatableFontDir dir = new UpdatableFontDir(
530                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
531                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
532         dir.loadFontFileMap();
533 
534         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
535                 GOOD_SIGNATURE)));
536         try {
537             dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
538                     GOOD_SIGNATURE)));
539             fail("Expect SystemFontException");
540         } catch (FontManagerService.SystemFontException e) {
541             assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING);
542         }
543         assertThat(dir.getPostScriptMap()).containsKey("test");
544         assertWithMessage("Font should not be downgraded to an older revision")
545                 .that(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2);
546         // Check that updatedFontDirs is not updated.
547         assertWithMessage("config.updatedFontDirs should only list latest active dirs")
548                 .that(readConfig(mConfigFile).updatedFontDirs)
549                 .containsExactly(dir.getPostScriptMap().get("test").getParentFile().getName());
550     }
551 
552     @Test
installFontFile_multiple()553     public void installFontFile_multiple() throws Exception {
554         UpdatableFontDir dir = new UpdatableFontDir(
555                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
556                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
557         dir.loadFontFileMap();
558 
559         dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
560                 GOOD_SIGNATURE)));
561         dir.update(Collections.singletonList(newFontUpdateRequest("bar.ttf,2,bar",
562                 GOOD_SIGNATURE)));
563         assertThat(dir.getPostScriptMap()).containsKey("foo");
564         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
565         assertThat(dir.getPostScriptMap()).containsKey("bar");
566         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2);
567     }
568 
569     @Test
installFontFile_batch()570     public void installFontFile_batch() throws Exception {
571         UpdatableFontDir dir = new UpdatableFontDir(
572                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
573                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
574         dir.loadFontFileMap();
575 
576         dir.update(Arrays.asList(
577                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE),
578                 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE)));
579         assertThat(dir.getPostScriptMap()).containsKey("foo");
580         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
581         assertThat(dir.getPostScriptMap()).containsKey("bar");
582         assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2);
583     }
584 
585     @Test
installFontFile_invalidSignature()586     public void installFontFile_invalidSignature() throws Exception {
587         UpdatableFontDir dir = new UpdatableFontDir(
588                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
589                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
590         dir.loadFontFileMap();
591 
592         try {
593             dir.update(
594                     Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
595                             "Invalid signature")));
596             fail("Expect SystemFontException");
597         } catch (FontManagerService.SystemFontException e) {
598             assertThat(e.getErrorCode())
599                     .isEqualTo(FontManager.RESULT_ERROR_VERIFICATION_FAILURE);
600         }
601         assertThat(dir.getPostScriptMap()).isEmpty();
602     }
603 
604     @Test
installFontFile_preinstalled_upgrade()605     public void installFontFile_preinstalled_upgrade() throws Exception {
606         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"),
607                 "test.ttf,1,test");
608         UpdatableFontDir dir = new UpdatableFontDir(
609                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
610                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
611         dir.loadFontFileMap();
612 
613         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
614                 GOOD_SIGNATURE)));
615         assertThat(dir.getPostScriptMap()).containsKey("test");
616         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2);
617     }
618 
619     @Test
installFontFile_preinstalled_sameVersion()620     public void installFontFile_preinstalled_sameVersion() throws Exception {
621         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"),
622                 "test.ttf,1,test");
623         UpdatableFontDir dir = new UpdatableFontDir(
624                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
625                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
626         dir.loadFontFileMap();
627 
628         dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
629                 GOOD_SIGNATURE)));
630         assertThat(dir.getPostScriptMap()).containsKey("test");
631         assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1);
632     }
633 
634     @Test
installFontFile_preinstalled_downgrade()635     public void installFontFile_preinstalled_downgrade() throws Exception {
636         File file = new File(mPreinstalledFontDirs.get(0), "test.ttf");
637         FileUtils.stringToFile(file, "test.ttf,2,test");
638         UpdatableFontDir dir = new UpdatableFontDir(
639                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
640                 mConfigFile, mCurrentTimeSupplier, (map) -> {
641             FontConfig.Font font = new FontConfig.Font(
642                     file, null, "test", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null,
643                     null);
644             FontConfig.FontFamily family = new FontConfig.FontFamily(
645                     Collections.singletonList(font), null, FontConfig.FontFamily.VARIANT_DEFAULT);
646             return new FontConfig(Collections.emptyList(), Collections.emptyList(),
647                     Collections.singletonList(new FontConfig.NamedFamilyList(
648                             Collections.singletonList(family), "sans-serif")), 0, 1);
649         });
650         dir.loadFontFileMap();
651 
652         try {
653             dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test",
654                     GOOD_SIGNATURE)));
655             fail("Expect SystemFontException");
656         } catch (FontManagerService.SystemFontException e) {
657             assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING);
658         }
659         assertThat(dir.getPostScriptMap()).isEmpty();
660     }
661 
662     @Test
installFontFile_failedToWriteConfigXml()663     public void installFontFile_failedToWriteConfigXml() throws Exception {
664         long expectedModifiedDate = 1234567890;
665         FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"),
666                 "test.ttf,1,test");
667 
668         File readonlyDir = new File(mCacheDir, "readonly");
669         assertThat(readonlyDir.mkdir()).isTrue();
670         File readonlyFile = new File(readonlyDir, "readonly_config.xml");
671 
672         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
673         config.lastModifiedMillis = expectedModifiedDate;
674         writeConfig(config, readonlyFile);
675 
676         assertThat(readonlyDir.setWritable(false, false)).isTrue();
677         try {
678             UpdatableFontDir dir = new UpdatableFontDir(
679                     mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
680                     readonlyFile, mCurrentTimeSupplier, mConfigSupplier);
681             dir.loadFontFileMap();
682 
683             try {
684                 dir.update(
685                         Collections.singletonList(newFontUpdateRequest("test.ttf,2,test",
686                                 GOOD_SIGNATURE)));
687             } catch (FontManagerService.SystemFontException e) {
688                 assertThat(e.getErrorCode())
689                         .isEqualTo(FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG);
690             }
691             assertThat(dir.getSystemFontConfig().getLastModifiedTimeMillis())
692                     .isEqualTo(expectedModifiedDate);
693             assertThat(dir.getPostScriptMap()).isEmpty();
694         } finally {
695             assertThat(readonlyDir.setWritable(true, true)).isTrue();
696         }
697     }
698 
699     @Test
installFontFile_failedToParsePostScript()700     public void installFontFile_failedToParsePostScript() throws Exception {
701         UpdatableFontDir dir = new UpdatableFontDir(
702                 mUpdatableFontFilesDir,
703                 new UpdatableFontDir.FontFileParser() {
704 
705                     @Override
706                     public String getPostScriptName(File file) throws IOException {
707                         return null;
708                     }
709 
710                     @Override
711                     public String buildFontFileName(File file) throws IOException {
712                         return null;
713                     }
714 
715                     @Override
716                     public long getRevision(File file) throws IOException {
717                         return 0;
718                     }
719 
720                     @Override
721                     public void tryToCreateTypeface(File file) throws IOException {
722                     }
723                 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
724         dir.loadFontFileMap();
725 
726         try {
727             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
728                     GOOD_SIGNATURE)));
729             fail("Expect SystemFontException");
730         } catch (FontManagerService.SystemFontException e) {
731             assertThat(e.getErrorCode())
732                     .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_NAME);
733         }
734         assertThat(dir.getPostScriptMap()).isEmpty();
735     }
736 
737     @Test
installFontFile_failedToParsePostScriptName_invalidFont()738     public void installFontFile_failedToParsePostScriptName_invalidFont() throws Exception {
739         UpdatableFontDir dir = new UpdatableFontDir(
740                 mUpdatableFontFilesDir,
741                 new UpdatableFontDir.FontFileParser() {
742                     @Override
743                     public String getPostScriptName(File file) throws IOException {
744                         throw new IOException();
745                     }
746 
747                     @Override
748                     public String buildFontFileName(File file) throws IOException {
749                         throw new IOException();
750                     }
751 
752                     @Override
753                     public long getRevision(File file) throws IOException {
754                         return 0;
755                     }
756 
757                     @Override
758                     public void tryToCreateTypeface(File file) throws IOException {
759                     }
760                 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
761         dir.loadFontFileMap();
762 
763         try {
764             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
765                     GOOD_SIGNATURE)));
766             fail("Expect SystemFontException");
767         } catch (FontManagerService.SystemFontException e) {
768             assertThat(e.getErrorCode())
769                     .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_FILE);
770         }
771         assertThat(dir.getPostScriptMap()).isEmpty();
772     }
773 
774     @Test
installFontFile_failedToCreateTypeface()775     public void installFontFile_failedToCreateTypeface() throws Exception {
776         UpdatableFontDir dir = new UpdatableFontDir(
777                 mUpdatableFontFilesDir,
778                 new UpdatableFontDir.FontFileParser() {
779                     @Override
780                     public String getPostScriptName(File file) throws IOException {
781                         return mParser.getPostScriptName(file);
782                     }
783 
784                     @Override
785                     public String buildFontFileName(File file) throws IOException {
786                         return mParser.buildFontFileName(file);
787                     }
788 
789                     @Override
790                     public long getRevision(File file) throws IOException {
791                         return mParser.getRevision(file);
792                     }
793 
794                     @Override
795                     public void tryToCreateTypeface(File file) throws IOException {
796                         throw new IOException();
797                     }
798                 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
799         dir.loadFontFileMap();
800 
801         try {
802             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
803                     GOOD_SIGNATURE)));
804             fail("Expect SystemFontException");
805         } catch (FontManagerService.SystemFontException e) {
806             assertThat(e.getErrorCode())
807                     .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_FILE);
808         }
809         assertThat(dir.getPostScriptMap()).isEmpty();
810     }
811 
812     @Test
installFontFile_renameToPsNameFailure()813     public void installFontFile_renameToPsNameFailure() throws Exception {
814         UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() {
815 
816             @Override
817             public boolean isFromTrustedProvider(String path, byte[] signature) {
818                 return mFakeFsverityUtil.isFromTrustedProvider(path, signature);
819             }
820 
821             @Override
822             public void setUpFsverity(String path) throws IOException {
823                 mFakeFsverityUtil.setUpFsverity(path);
824             }
825 
826             @Override
827             public boolean rename(File src, File dest) {
828                 return false;
829             }
830         };
831         UpdatableFontDir dir = new UpdatableFontDir(
832                 mUpdatableFontFilesDir, mParser, fakeFsverityUtil,
833                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
834         dir.loadFontFileMap();
835 
836         try {
837             dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
838                     GOOD_SIGNATURE)));
839             fail("Expect SystemFontException");
840         } catch (FontManagerService.SystemFontException e) {
841             assertThat(e.getErrorCode())
842                     .isEqualTo(FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE);
843         }
844         assertThat(dir.getPostScriptMap()).isEmpty();
845     }
846 
847     @Test
installFontFile_batchFailure()848     public void installFontFile_batchFailure() throws Exception {
849         UpdatableFontDir dir = new UpdatableFontDir(
850                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
851                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
852         dir.loadFontFileMap();
853 
854         dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo",
855                 GOOD_SIGNATURE)));
856         try {
857             dir.update(Arrays.asList(
858                     newFontUpdateRequest("foo.ttf,2,foo", GOOD_SIGNATURE),
859                     newFontUpdateRequest("bar.ttf,2,bar", "Invalid signature")));
860             fail("Batch update with invalid signature should fail");
861         } catch (FontManagerService.SystemFontException e) {
862             // Expected
863         }
864         // The state should be rolled back as a whole if one of the update requests fail.
865         assertThat(dir.getPostScriptMap()).containsKey("foo");
866         assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1);
867     }
868 
869     @Test
addFontFamily()870     public void addFontFamily() throws Exception {
871         UpdatableFontDir dir = new UpdatableFontDir(
872                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
873                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
874         dir.loadFontFileMap();
875 
876         dir.update(Arrays.asList(
877                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
878                 newAddFontFamilyRequest("<family name='test'>"
879                         + "  <font>test.ttf</font>"
880                         + "</family>")));
881         assertThat(dir.getPostScriptMap()).containsKey("test");
882         assertThat(dir.getFontFamilyMap()).containsKey("test");
883         assertThat(dir.getFontFamilyMap().get("test").getFamilies().size()).isEqualTo(1);
884         FontConfig.FontFamily test = dir.getFontFamilyMap().get("test").getFamilies().get(0);
885         assertThat(test.getFontList()).hasSize(1);
886         assertThat(test.getFontList().get(0).getFile())
887                 .isEqualTo(dir.getPostScriptMap().get("test"));
888     }
889 
890     @Test(expected = IllegalArgumentException.class)
addFontFamily_noName()891     public void addFontFamily_noName() throws Exception {
892         UpdatableFontDir dir = new UpdatableFontDir(
893                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
894                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
895         dir.loadFontFileMap();
896 
897         List<FontUpdateRequest> requests = Arrays.asList(
898                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
899                 newAddFontFamilyRequest("<family lang='en'>"
900                         + "  <font>test.ttf</font>"
901                         + "</family>"));
902         dir.update(requests);
903     }
904 
905     @Test
addFontFamily_fontNotAvailable()906     public void addFontFamily_fontNotAvailable() throws Exception {
907         UpdatableFontDir dir = new UpdatableFontDir(
908                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
909                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
910         dir.loadFontFileMap();
911 
912         try {
913             dir.update(Arrays.asList(newAddFontFamilyRequest("<family name='test'>"
914                     + "  <font>test.ttf</font>"
915                     + "</family>")));
916             fail("Expect SystemFontException");
917         } catch (FontManagerService.SystemFontException e) {
918             assertThat(e.getErrorCode())
919                     .isEqualTo(FontManager.RESULT_ERROR_FONT_NOT_FOUND);
920         }
921     }
922 
923     @Test
getSystemFontConfig()924     public void getSystemFontConfig() throws Exception {
925         UpdatableFontDir dir = new UpdatableFontDir(
926                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
927                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
928         dir.loadFontFileMap();
929         // We assume we have monospace.
930         assertNamedFamilyExists(dir.getSystemFontConfig(), "monospace");
931 
932         dir.update(Arrays.asList(
933                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
934                 // Updating an existing font family.
935                 newAddFontFamilyRequest("<family name='monospace'>"
936                         + "  <font>test.ttf</font>"
937                         + "</family>"),
938                 // Adding a new font family.
939                 newAddFontFamilyRequest("<family name='test'>"
940                         + "  <font>test.ttf</font>"
941                         + "</family>")));
942         FontConfig fontConfig = dir.getSystemFontConfig();
943         assertNamedFamilyExists(fontConfig, "monospace");
944         FontConfig.FontFamily monospace = getLastFamily(fontConfig, "monospace");
945         assertThat(monospace.getFontList()).hasSize(1);
946         assertThat(monospace.getFontList().get(0).getFile())
947                 .isEqualTo(dir.getPostScriptMap().get("test"));
948         assertNamedFamilyExists(fontConfig, "test");
949         assertThat(getLastFamily(fontConfig, "test").getFontList())
950                 .isEqualTo(monospace.getFontList());
951     }
952 
953     @Test
getSystemFontConfig_preserveFirstFontFamily()954     public void getSystemFontConfig_preserveFirstFontFamily() throws Exception {
955         UpdatableFontDir dir = new UpdatableFontDir(
956                 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
957                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
958         dir.loadFontFileMap();
959         assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty();
960         FontConfig.FontFamily firstFontFamily = dir.getSystemFontConfig().getFontFamilies().get(0);
961 
962         dir.update(Arrays.asList(
963                 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE),
964                 newAddFontFamilyRequest("<family name='sans-serif'>"
965                         + "  <font>test.ttf</font>"
966                         + "</family>")));
967         FontConfig fontConfig = dir.getSystemFontConfig();
968         assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty();
969         assertThat(fontConfig.getFontFamilies().get(0)).isEqualTo(firstFontFamily);
970         FontConfig.FontFamily updated = getLastFamily(fontConfig, "sans-serif");
971         assertThat(updated.getFontList()).hasSize(1);
972         assertThat(updated.getFontList().get(0).getFile())
973                 .isEqualTo(dir.getPostScriptMap().get("test"));
974         assertThat(updated).isNotEqualTo(firstFontFamily);
975     }
976 
977     @Test
deleteAllFiles()978     public void deleteAllFiles() throws Exception {
979         FakeFontFileParser parser = new FakeFontFileParser();
980         FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
981         UpdatableFontDir dirForPreparation = new UpdatableFontDir(
982                 mUpdatableFontFilesDir, parser, fakeFsverityUtil,
983                 mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
984         dirForPreparation.loadFontFileMap();
985         dirForPreparation.update(Collections.singletonList(
986                 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE)));
987         assertThat(mConfigFile.exists()).isTrue();
988         assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
989 
990         UpdatableFontDir.deleteAllFiles(mUpdatableFontFilesDir, mConfigFile);
991         assertThat(mConfigFile.exists()).isFalse();
992         assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
993     }
994 
newFontUpdateRequest(String content, String signature)995     private FontUpdateRequest newFontUpdateRequest(String content, String signature)
996             throws Exception {
997         File file = File.createTempFile("font", "ttf", mCacheDir);
998         FileUtils.stringToFile(file, content);
999         return new FontUpdateRequest(
1000                 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY),
1001                 signature.getBytes());
1002     }
1003 
newAddFontFamilyRequest(String xml)1004     private static FontUpdateRequest newAddFontFamilyRequest(String xml) throws Exception {
1005         XmlPullParser mParser = Xml.newPullParser();
1006         ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
1007         mParser.setInput(is, "UTF-8");
1008         mParser.nextTag();
1009 
1010         FontConfig.NamedFamilyList namedFamilyList = FontListParser.readNamedFamily(
1011                 mParser, "", null, true);
1012         FontConfig.FontFamily fontFamily = namedFamilyList.getFamilies().get(0);
1013         List<FontUpdateRequest.Font> fonts = new ArrayList<>();
1014         for (FontConfig.Font font : fontFamily.getFontList()) {
1015             String name = font.getFile().getName();
1016             String psName = name.substring(0, name.length() - 4);  // drop suffix
1017             FontUpdateRequest.Font updateFont = new FontUpdateRequest.Font(
1018                     psName, font.getStyle(), font.getTtcIndex(), font.getFontVariationSettings());
1019             fonts.add(updateFont);
1020         }
1021         FontUpdateRequest.Family family = new FontUpdateRequest.Family(
1022                 namedFamilyList.getName(), fonts);
1023         return new FontUpdateRequest(family);
1024     }
1025 
readConfig(File file)1026     private static PersistentSystemFontConfig.Config readConfig(File file) throws Exception {
1027         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
1028         try (InputStream is = new FileInputStream(file)) {
1029             PersistentSystemFontConfig.loadFromXml(is, config);
1030         }
1031         return config;
1032     }
1033 
writeConfig(PersistentSystemFontConfig.Config config, File file)1034     private static void writeConfig(PersistentSystemFontConfig.Config config,
1035             File file) throws IOException {
1036         try (FileOutputStream fos = new FileOutputStream(file)) {
1037             PersistentSystemFontConfig.writeToXml(fos, config);
1038         }
1039     }
1040 
1041     // Returns the last family with the given name, which will be used for creating Typeface.
getLastFamily(FontConfig fontConfig, String familyName)1042     private static FontConfig.FontFamily getLastFamily(FontConfig fontConfig, String familyName) {
1043         List<FontConfig.NamedFamilyList> namedFamilyLists = fontConfig.getNamedFamilyLists();
1044         for (int i = namedFamilyLists.size() - 1; i >= 0; i--) {
1045             if (familyName.equals(namedFamilyLists.get(i).getName())) {
1046                 return namedFamilyLists.get(i).getFamilies().get(0);
1047             }
1048         }
1049         return null;
1050     }
1051 
assertNamedFamilyExists(FontConfig fontConfig, String familyName)1052     private static void assertNamedFamilyExists(FontConfig fontConfig, String familyName) {
1053         assertThat(fontConfig.getNamedFamilyLists().stream()
1054                 .map(FontConfig.NamedFamilyList::getName)
1055                 .collect(Collectors.toSet())).contains(familyName);
1056     }
1057 }
1058