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.android.server.graphics.fonts.FontManagerService.SystemFontException;
20 
21 import android.annotation.NonNull;
22 import android.graphics.fonts.FontManager;
23 import android.graphics.fonts.FontUpdateRequest;
24 import android.graphics.fonts.SystemFonts;
25 import android.os.FileUtils;
26 import android.os.LocaleList;
27 import android.system.ErrnoException;
28 import android.system.Os;
29 import android.text.FontConfig;
30 import android.util.ArrayMap;
31 import android.util.AtomicFile;
32 import android.util.Base64;
33 import android.util.Slog;
34 
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.File;
38 import java.io.FileDescriptor;
39 import java.io.FileInputStream;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.nio.file.Files;
43 import java.nio.file.Paths;
44 import java.security.SecureRandom;
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Objects;
51 import java.util.function.Function;
52 import java.util.function.Supplier;
53 
54 /**
55  * Manages set of updatable font files.
56  *
57  * <p>This class is not thread safe.
58  */
59 final class UpdatableFontDir {
60 
61     private static final String TAG = "UpdatableFontDir";
62     private static final String RANDOM_DIR_PREFIX = "~~";
63 
64     private static final String FONT_SIGNATURE_FILE = "font.fsv_sig";
65 
66     /** Interface to mock font file access in tests. */
67     interface FontFileParser {
getPostScriptName(File file)68         String getPostScriptName(File file) throws IOException;
69 
buildFontFileName(File file)70         String buildFontFileName(File file) throws IOException;
71 
getRevision(File file)72         long getRevision(File file) throws IOException;
73 
tryToCreateTypeface(File file)74         void tryToCreateTypeface(File file) throws Throwable;
75     }
76 
77     /** Interface to mock fs-verity in tests. */
78     interface FsverityUtil {
isFromTrustedProvider(String path, byte[] pkcs7Signature)79         boolean isFromTrustedProvider(String path, byte[] pkcs7Signature);
80 
setUpFsverity(String path)81         void setUpFsverity(String path) throws IOException;
82 
rename(File src, File dest)83         boolean rename(File src, File dest);
84     }
85 
86     /** Data class to hold font file path and revision. */
87     private static final class FontFileInfo {
88         private final File mFile;
89         private final String mPsName;
90         private final long mRevision;
91 
FontFileInfo(File file, String psName, long revision)92         FontFileInfo(File file, String psName, long revision) {
93             mFile = file;
94             mPsName = psName;
95             mRevision = revision;
96         }
97 
getFile()98         public File getFile() {
99             return mFile;
100         }
101 
getPostScriptName()102         public String getPostScriptName() {
103             return mPsName;
104         }
105 
106         /** Returns the unique randomized font dir containing this font file. */
getRandomizedFontDir()107         public File getRandomizedFontDir() {
108             return mFile.getParentFile();
109         }
110 
getRevision()111         public long getRevision() {
112             return mRevision;
113         }
114 
115         @Override
toString()116         public String toString() {
117             return "FontFileInfo{mFile=" + mFile
118                     + ", psName=" + mPsName
119                     + ", mRevision=" + mRevision + '}';
120         }
121     }
122 
123     /**
124      * Root directory for storing updated font files. Each font file is stored in a unique
125      * randomized dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}.
126      */
127     private final File mFilesDir;
128     private final FontFileParser mParser;
129     private final FsverityUtil mFsverityUtil;
130     private final AtomicFile mConfigFile;
131     private final Supplier<Long> mCurrentTimeSupplier;
132     private final Function<Map<String, File>, FontConfig> mConfigSupplier;
133 
134     private long mLastModifiedMillis;
135     private int mConfigVersion;
136 
137     /**
138      * A mutable map containing mapping from font file name (e.g. "NotoColorEmoji.ttf") to {@link
139      * FontFileInfo}. All files in this map are validated, and have higher revision numbers than
140      * corresponding font files returned by {@link #mConfigSupplier}.
141      */
142     private final ArrayMap<String, FontFileInfo> mFontFileInfoMap = new ArrayMap<>();
143 
UpdatableFontDir(File filesDir, FontFileParser parser, FsverityUtil fsverityUtil, File configFile)144     UpdatableFontDir(File filesDir, FontFileParser parser, FsverityUtil fsverityUtil,
145             File configFile) {
146         this(filesDir, parser, fsverityUtil, configFile,
147                 System::currentTimeMillis,
148                 (map) -> SystemFonts.getSystemFontConfig(map, 0, 0)
149         );
150     }
151 
152     // For unit testing
UpdatableFontDir(File filesDir, FontFileParser parser, FsverityUtil fsverityUtil, File configFile, Supplier<Long> currentTimeSupplier, Function<Map<String, File>, FontConfig> configSupplier)153     UpdatableFontDir(File filesDir, FontFileParser parser, FsverityUtil fsverityUtil,
154             File configFile, Supplier<Long> currentTimeSupplier,
155             Function<Map<String, File>, FontConfig> configSupplier) {
156         mFilesDir = filesDir;
157         mParser = parser;
158         mFsverityUtil = fsverityUtil;
159         mConfigFile = new AtomicFile(configFile);
160         mCurrentTimeSupplier = currentTimeSupplier;
161         mConfigSupplier = configSupplier;
162     }
163 
164     /**
165      * Loads fonts from file system, validate them, and delete obsolete font files.
166      * Note that this method may be called by multiple times in integration tests via {@link
167      * FontManagerService#restart()}.
168      */
loadFontFileMap()169     /* package */ void loadFontFileMap() {
170         mFontFileInfoMap.clear();
171         mLastModifiedMillis = 0;
172         mConfigVersion = 1;
173         boolean success = false;
174         try {
175             PersistentSystemFontConfig.Config config = readPersistentConfig();
176             mLastModifiedMillis = config.lastModifiedMillis;
177 
178             File[] dirs = mFilesDir.listFiles();
179             if (dirs == null) {
180                 // mFilesDir should be created by init script.
181                 Slog.e(TAG, "Could not read: " + mFilesDir);
182                 return;
183             }
184             FontConfig fontConfig = null;
185             for (File dir : dirs) {
186                 if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) {
187                     Slog.e(TAG, "Unexpected dir found: " + dir);
188                     return;
189                 }
190                 if (!config.updatedFontDirs.contains(dir.getName())) {
191                     Slog.i(TAG, "Deleting obsolete dir: " + dir);
192                     FileUtils.deleteContentsAndDir(dir);
193                     continue;
194                 }
195 
196                 File signatureFile = new File(dir, FONT_SIGNATURE_FILE);
197                 if (!signatureFile.exists()) {
198                     Slog.i(TAG, "The signature file is missing.");
199                     FileUtils.deleteContentsAndDir(dir);
200                     continue;
201                 }
202                 byte[] signature;
203                 try {
204                     signature = Files.readAllBytes(Paths.get(signatureFile.getAbsolutePath()));
205                 } catch (IOException e) {
206                     Slog.e(TAG, "Failed to read signature file.");
207                     return;
208                 }
209 
210                 File[] files = dir.listFiles();
211                 if (files == null || files.length != 2) {
212                     Slog.e(TAG, "Unexpected files in dir: " + dir);
213                     return;
214                 }
215 
216                 File fontFile;
217                 if (files[0].equals(signatureFile)) {
218                     fontFile = files[1];
219                 } else {
220                     fontFile = files[0];
221                 }
222 
223                 FontFileInfo fontFileInfo = validateFontFile(fontFile, signature);
224                 if (fontConfig == null) {
225                     fontConfig = getSystemFontConfig();
226                 }
227                 addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, true /* deleteOldFile */);
228             }
229             success = true;
230         } catch (Throwable t) {
231             // If something happened during loading system fonts, clear all contents in finally
232             // block. Here, just dumping errors.
233             Slog.e(TAG, "Failed to load font mappings.", t);
234         } finally {
235             // Delete all files just in case if we find a problematic file.
236             if (!success) {
237                 mFontFileInfoMap.clear();
238                 mLastModifiedMillis = 0;
239                 FileUtils.deleteContents(mFilesDir);
240             }
241         }
242     }
243 
244     /**
245      * Applies multiple {@link FontUpdateRequest}s in transaction.
246      * If one of the request fails, the fonts and config are rolled back to the previous state
247      * before this method is called.
248      */
update(List<FontUpdateRequest> requests)249     public void update(List<FontUpdateRequest> requests) throws SystemFontException {
250         for (FontUpdateRequest request : requests) {
251             switch (request.getType()) {
252                 case FontUpdateRequest.TYPE_UPDATE_FONT_FILE:
253                     Objects.requireNonNull(request.getFd());
254                     Objects.requireNonNull(request.getSignature());
255                     break;
256                 case FontUpdateRequest.TYPE_UPDATE_FONT_FAMILY:
257                     Objects.requireNonNull(request.getFontFamily());
258                     Objects.requireNonNull(request.getFontFamily().getName());
259                     break;
260             }
261         }
262         // Backup the mapping for rollback.
263         ArrayMap<String, FontFileInfo> backupMap = new ArrayMap<>(mFontFileInfoMap);
264         PersistentSystemFontConfig.Config curConfig = readPersistentConfig();
265         Map<String, FontUpdateRequest.Family> familyMap = new HashMap<>();
266         for (int i = 0; i < curConfig.fontFamilies.size(); ++i) {
267             FontUpdateRequest.Family family = curConfig.fontFamilies.get(i);
268             familyMap.put(family.getName(), family);
269         }
270 
271         long backupLastModifiedDate = mLastModifiedMillis;
272         boolean success = false;
273         try {
274             for (FontUpdateRequest request : requests) {
275                 switch (request.getType()) {
276                     case FontUpdateRequest.TYPE_UPDATE_FONT_FILE:
277                         installFontFile(
278                                 request.getFd().getFileDescriptor(), request.getSignature());
279                         break;
280                     case FontUpdateRequest.TYPE_UPDATE_FONT_FAMILY:
281                         FontUpdateRequest.Family family = request.getFontFamily();
282                         familyMap.put(family.getName(), family);
283                         break;
284                 }
285             }
286 
287             // Before processing font family update, check all family points the available fonts.
288             for (FontUpdateRequest.Family family : familyMap.values()) {
289                 if (resolveFontFilesForNamedFamily(family) == null) {
290                     throw new SystemFontException(
291                             FontManager.RESULT_ERROR_FONT_NOT_FOUND,
292                             "Required fonts are not available");
293                 }
294             }
295 
296             // Write config file.
297             mLastModifiedMillis = mCurrentTimeSupplier.get();
298 
299             PersistentSystemFontConfig.Config newConfig = new PersistentSystemFontConfig.Config();
300             newConfig.lastModifiedMillis = mLastModifiedMillis;
301             for (FontFileInfo info : mFontFileInfoMap.values()) {
302                 newConfig.updatedFontDirs.add(info.getRandomizedFontDir().getName());
303             }
304             newConfig.fontFamilies.addAll(familyMap.values());
305             writePersistentConfig(newConfig);
306             mConfigVersion++;
307             success = true;
308         } finally {
309             if (!success) {
310                 mFontFileInfoMap.clear();
311                 mFontFileInfoMap.putAll(backupMap);
312                 mLastModifiedMillis = backupLastModifiedDate;
313             }
314         }
315     }
316 
317     /**
318      * Installs a new font file, or updates an existing font file.
319      *
320      * <p>The new font will be immediately available for new Zygote-forked processes through
321      * {@link #getPostScriptMap()}. Old font files will be kept until next system server reboot,
322      * because existing Zygote-forked processes have paths to old font files.
323      *
324      * @param fd             A file descriptor to the font file.
325      * @param pkcs7Signature A PKCS#7 detached signature to enable fs-verity for the font file.
326      * @throws SystemFontException if error occurs.
327      */
installFontFile(FileDescriptor fd, byte[] pkcs7Signature)328     private void installFontFile(FileDescriptor fd, byte[] pkcs7Signature)
329             throws SystemFontException {
330         File newDir = getRandomDir(mFilesDir);
331         if (!newDir.mkdir()) {
332             throw new SystemFontException(
333                     FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
334                     "Failed to create font directory.");
335         }
336         try {
337             // Make newDir executable so that apps can access font file inside newDir.
338             Os.chmod(newDir.getAbsolutePath(), 0711);
339         } catch (ErrnoException e) {
340             throw new SystemFontException(
341                     FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
342                     "Failed to change mode to 711", e);
343         }
344         boolean success = false;
345         try {
346             File tempNewFontFile = new File(newDir, "font.ttf");
347             try (FileOutputStream out = new FileOutputStream(tempNewFontFile)) {
348                 FileUtils.copy(fd, out.getFD());
349             } catch (IOException e) {
350                 throw new SystemFontException(
351                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
352                         "Failed to write font file to storage.", e);
353             }
354             try {
355                 // Do not parse font file before setting up fs-verity.
356                 // setUpFsverity throws IOException if failed.
357                 mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath());
358             } catch (IOException e) {
359                 throw new SystemFontException(
360                         FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
361                         "Failed to setup fs-verity.", e);
362             }
363             String fontFileName;
364             try {
365                 fontFileName = mParser.buildFontFileName(tempNewFontFile);
366             } catch (IOException e) {
367                 throw new SystemFontException(
368                         FontManager.RESULT_ERROR_INVALID_FONT_FILE,
369                         "Failed to read PostScript name from font file", e);
370             }
371             if (fontFileName == null) {
372                 throw new SystemFontException(
373                         FontManager.RESULT_ERROR_INVALID_FONT_NAME,
374                         "Failed to read PostScript name from font file");
375             }
376             File newFontFile = new File(newDir, fontFileName);
377             if (!mFsverityUtil.rename(tempNewFontFile, newFontFile)) {
378                 throw new SystemFontException(
379                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
380                         "Failed to move verified font file.");
381             }
382             try {
383                 // Make the font file readable by apps.
384                 Os.chmod(newFontFile.getAbsolutePath(), 0644);
385             } catch (ErrnoException e) {
386                 throw new SystemFontException(
387                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
388                         "Failed to change font file mode to 644", e);
389             }
390             File signatureFile = new File(newDir, FONT_SIGNATURE_FILE);
391             try (FileOutputStream out = new FileOutputStream(signatureFile)) {
392                 out.write(pkcs7Signature);
393             } catch (IOException e) {
394                 // TODO: Do we need new error code for signature write failure?
395                 throw new SystemFontException(
396                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
397                         "Failed to write font signature file to storage.", e);
398             }
399             try {
400                 Os.chmod(signatureFile.getAbsolutePath(), 0600);
401             } catch (ErrnoException e) {
402                 throw new SystemFontException(
403                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
404                         "Failed to change the signature file mode to 600", e);
405             }
406             FontFileInfo fontFileInfo = validateFontFile(newFontFile, pkcs7Signature);
407 
408             // Try to create Typeface and treat as failure something goes wrong.
409             try {
410                 mParser.tryToCreateTypeface(fontFileInfo.getFile());
411             } catch (Throwable t) {
412                 throw new SystemFontException(
413                         FontManager.RESULT_ERROR_INVALID_FONT_FILE,
414                         "Failed to create Typeface from file", t);
415             }
416 
417             FontConfig fontConfig = getSystemFontConfig();
418             if (!addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, false)) {
419                 throw new SystemFontException(
420                         FontManager.RESULT_ERROR_DOWNGRADING,
421                         "Downgrading font file is forbidden.");
422             }
423             success = true;
424         } finally {
425             if (!success) {
426                 FileUtils.deleteContentsAndDir(newDir);
427             }
428         }
429     }
430 
431     /**
432      * Given {@code parent}, returns {@code parent/~~[randomStr]}.
433      * Makes sure that {@code parent/~~[randomStr]} directory doesn't exist.
434      * Notice that this method doesn't actually create any directory.
435      */
getRandomDir(File parent)436     private static File getRandomDir(File parent) {
437         SecureRandom random = new SecureRandom();
438         byte[] bytes = new byte[16];
439         File dir;
440         do {
441             random.nextBytes(bytes);
442             String dirName = RANDOM_DIR_PREFIX
443                     + Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
444             dir = new File(parent, dirName);
445         } while (dir.exists());
446         return dir;
447     }
448 
lookupFontFileInfo(String psName)449     private FontFileInfo lookupFontFileInfo(String psName) {
450         return mFontFileInfoMap.get(psName);
451     }
452 
putFontFileInfo(FontFileInfo info)453     private void putFontFileInfo(FontFileInfo info) {
454         mFontFileInfoMap.put(info.getPostScriptName(), info);
455     }
456 
457     /**
458      * Add the given {@link FontFileInfo} to {@link #mFontFileInfoMap} if its font revision is
459      * equal to or higher than the revision of currently used font file (either in
460      * {@link #mFontFileInfoMap} or {@code fontConfig}).
461      */
addFileToMapIfSameOrNewer(FontFileInfo fontFileInfo, FontConfig fontConfig, boolean deleteOldFile)462     private boolean addFileToMapIfSameOrNewer(FontFileInfo fontFileInfo, FontConfig fontConfig,
463             boolean deleteOldFile) {
464         FontFileInfo existingInfo = lookupFontFileInfo(fontFileInfo.getPostScriptName());
465         final boolean shouldAddToMap;
466         if (existingInfo == null) {
467             // We got a new updatable font. We need to check if it's newer than preinstalled fonts.
468             // Note that getPreinstalledFontRevision() returns -1 if there is no preinstalled font
469             // with 'name'.
470             long preInstalledRev = getPreinstalledFontRevision(fontFileInfo, fontConfig);
471             shouldAddToMap = preInstalledRev <= fontFileInfo.getRevision();
472         } else {
473             shouldAddToMap = existingInfo.getRevision() <= fontFileInfo.getRevision();
474         }
475         if (shouldAddToMap) {
476             if (deleteOldFile && existingInfo != null) {
477                 FileUtils.deleteContentsAndDir(existingInfo.getRandomizedFontDir());
478             }
479             putFontFileInfo(fontFileInfo);
480         } else {
481             if (deleteOldFile) {
482                 FileUtils.deleteContentsAndDir(fontFileInfo.getRandomizedFontDir());
483             }
484         }
485         return shouldAddToMap;
486     }
487 
getPreinstalledFontRevision(FontFileInfo info, FontConfig fontConfig)488     private long getPreinstalledFontRevision(FontFileInfo info, FontConfig fontConfig) {
489         String psName = info.getPostScriptName();
490         FontConfig.Font targetFont = null;
491         for (int i = 0; i < fontConfig.getFontFamilies().size(); i++) {
492             FontConfig.FontFamily family = fontConfig.getFontFamilies().get(i);
493             for (int j = 0; j < family.getFontList().size(); ++j) {
494                 FontConfig.Font font = family.getFontList().get(j);
495                 if (font.getPostScriptName().equals(psName)) {
496                     targetFont = font;
497                     break;
498                 }
499             }
500         }
501         for (int i = 0; i < fontConfig.getNamedFamilyLists().size(); ++i) {
502             FontConfig.NamedFamilyList namedFamilyList = fontConfig.getNamedFamilyLists().get(i);
503             for (int j = 0; j < namedFamilyList.getFamilies().size(); ++j) {
504                 FontConfig.FontFamily family = namedFamilyList.getFamilies().get(j);
505                 for (int k = 0; k < family.getFontList().size(); ++k) {
506                     FontConfig.Font font = family.getFontList().get(k);
507                     if (font.getPostScriptName().equals(psName)) {
508                         targetFont = font;
509                         break;
510                     }
511                 }
512             }
513         }
514         if (targetFont == null) {
515             return -1;
516         }
517 
518         File preinstalledFontFile = targetFont.getOriginalFile() != null
519                 ? targetFont.getOriginalFile() : targetFont.getFile();
520         if (!preinstalledFontFile.exists()) {
521             return -1;
522         }
523         long revision = getFontRevision(preinstalledFontFile);
524         if (revision == -1) {
525             Slog.w(TAG, "Invalid preinstalled font file");
526         }
527         return revision;
528     }
529 
530     /**
531      * Checks the fs-verity protection status of the given font file, validates the file name, and
532      * returns a {@link FontFileInfo} on success. This method does not check if the font revision
533      * is higher than the currently used font.
534      */
535     @NonNull
validateFontFile(File file, byte[] pkcs7Signature)536     private FontFileInfo validateFontFile(File file, byte[] pkcs7Signature)
537             throws SystemFontException {
538         if (!mFsverityUtil.isFromTrustedProvider(file.getAbsolutePath(), pkcs7Signature)) {
539             throw new SystemFontException(
540                     FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
541                     "Font validation failed. Fs-verity is not enabled: " + file);
542         }
543         final String psName;
544         try {
545             psName = mParser.getPostScriptName(file);
546         } catch (IOException e) {
547             throw new SystemFontException(
548                     FontManager.RESULT_ERROR_INVALID_FONT_NAME,
549                     "Font validation failed. Could not read PostScript name name: " + file);
550         }
551         long revision = getFontRevision(file);
552         if (revision == -1) {
553             throw new SystemFontException(
554                     FontManager.RESULT_ERROR_INVALID_FONT_FILE,
555                     "Font validation failed. Could not read font revision: " + file);
556         }
557         return new FontFileInfo(file, psName, revision);
558     }
559     /** Returns the non-negative font revision of the given font file, or -1. */
getFontRevision(File file)560     private long getFontRevision(File file) {
561         try {
562             return mParser.getRevision(file);
563         } catch (IOException e) {
564             Slog.e(TAG, "Failed to read font file", e);
565             return -1;
566         }
567     }
568 
resolveFontFilesForNamedFamily( FontUpdateRequest.Family fontFamily)569     private FontConfig.NamedFamilyList resolveFontFilesForNamedFamily(
570             FontUpdateRequest.Family fontFamily) {
571         List<FontUpdateRequest.Font> fontList = fontFamily.getFonts();
572         List<FontConfig.Font> resolvedFonts = new ArrayList<>(fontList.size());
573         for (int i = 0; i < fontList.size(); i++) {
574             FontUpdateRequest.Font font = fontList.get(i);
575             FontFileInfo info = mFontFileInfoMap.get(font.getPostScriptName());
576             if (info == null) {
577                 Slog.e(TAG, "Failed to lookup font file that has " + font.getPostScriptName());
578                 return null;
579             }
580             resolvedFonts.add(new FontConfig.Font(info.mFile, null, info.getPostScriptName(),
581                     font.getFontStyle(), font.getIndex(), font.getFontVariationSettings(), null));
582         }
583         FontConfig.FontFamily family = new FontConfig.FontFamily(resolvedFonts,
584                 LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT);
585         return new FontConfig.NamedFamilyList(Collections.singletonList(family),
586                 fontFamily.getName());
587     }
588 
getPostScriptMap()589     Map<String, File> getPostScriptMap() {
590         Map<String, File> map = new ArrayMap<>();
591         for (int i = 0; i < mFontFileInfoMap.size(); ++i) {
592             FontFileInfo info = mFontFileInfoMap.valueAt(i);
593             map.put(info.getPostScriptName(), info.getFile());
594         }
595         return map;
596     }
597 
getSystemFontConfig()598     /* package */ FontConfig getSystemFontConfig() {
599         FontConfig config = mConfigSupplier.apply(getPostScriptMap());
600         PersistentSystemFontConfig.Config persistentConfig = readPersistentConfig();
601         List<FontUpdateRequest.Family> families = persistentConfig.fontFamilies;
602 
603         List<FontConfig.NamedFamilyList> mergedFamilies =
604                 new ArrayList<>(config.getNamedFamilyLists().size() + families.size());
605         // We should keep the first font family (config.getFontFamilies().get(0)) because it's used
606         // as a fallback font. See SystemFonts.java.
607         mergedFamilies.addAll(config.getNamedFamilyLists());
608         // When building Typeface, a latter font family definition will override the previous font
609         // family definition with the same name. An exception is config.getFontFamilies.get(0),
610         // which will be used as a fallback font without being overridden.
611         for (int i = 0; i < families.size(); ++i) {
612             FontConfig.NamedFamilyList family = resolveFontFilesForNamedFamily(families.get(i));
613             if (family != null) {
614                 mergedFamilies.add(family);
615             }
616         }
617 
618         return new FontConfig(
619                 config.getFontFamilies(), config.getAliases(), mergedFamilies, mLastModifiedMillis,
620                 mConfigVersion);
621     }
622 
readPersistentConfig()623     private PersistentSystemFontConfig.Config readPersistentConfig() {
624         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
625         try (FileInputStream fis = mConfigFile.openRead()) {
626             PersistentSystemFontConfig.loadFromXml(fis, config);
627         } catch (IOException | XmlPullParserException e) {
628             // The font config file is missing on the first boot. Just do nothing.
629         }
630         return config;
631     }
632 
writePersistentConfig(PersistentSystemFontConfig.Config config)633     private void writePersistentConfig(PersistentSystemFontConfig.Config config)
634             throws SystemFontException {
635         FileOutputStream fos = null;
636         try {
637             fos = mConfigFile.startWrite();
638             PersistentSystemFontConfig.writeToXml(fos, config);
639             mConfigFile.finishWrite(fos);
640         } catch (IOException e) {
641             if (fos != null) {
642                 mConfigFile.failWrite(fos);
643             }
644             throw new SystemFontException(
645                     FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG,
646                     "Failed to write config XML.", e);
647         }
648     }
649 
getConfigVersion()650     /* package */ int getConfigVersion() {
651         return mConfigVersion;
652     }
653 
getFontFamilyMap()654     public Map<String, FontConfig.NamedFamilyList> getFontFamilyMap() {
655         PersistentSystemFontConfig.Config curConfig = readPersistentConfig();
656         Map<String, FontConfig.NamedFamilyList> familyMap = new HashMap<>();
657         for (int i = 0; i < curConfig.fontFamilies.size(); ++i) {
658             FontUpdateRequest.Family family = curConfig.fontFamilies.get(i);
659             FontConfig.NamedFamilyList resolvedFamily = resolveFontFilesForNamedFamily(family);
660             if (resolvedFamily != null) {
661                 familyMap.put(family.getName(), resolvedFamily);
662             }
663         }
664         return familyMap;
665     }
666 
deleteAllFiles(File filesDir, File configFile)667     /* package */ static void deleteAllFiles(File filesDir, File configFile) {
668         // As this method is called in safe mode, try to delete all files even though an exception
669         // is thrown.
670         try {
671             new AtomicFile(configFile).delete();
672         } catch (Throwable t) {
673             Slog.w(TAG, "Failed to delete " + configFile);
674         }
675         try {
676             FileUtils.deleteContents(filesDir);
677         } catch (Throwable t) {
678             Slog.w(TAG, "Failed to delete " + filesDir);
679         }
680     }
681 }
682