• 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.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                     return;
200                 }
201                 byte[] signature;
202                 try {
203                     signature = Files.readAllBytes(Paths.get(signatureFile.getAbsolutePath()));
204                 } catch (IOException e) {
205                     Slog.e(TAG, "Failed to read signature file.");
206                     return;
207                 }
208 
209                 File[] files = dir.listFiles();
210                 if (files == null || files.length != 2) {
211                     Slog.e(TAG, "Unexpected files in dir: " + dir);
212                     return;
213                 }
214 
215                 File fontFile;
216                 if (files[0].equals(signatureFile)) {
217                     fontFile = files[1];
218                 } else {
219                     fontFile = files[0];
220                 }
221 
222                 FontFileInfo fontFileInfo = validateFontFile(fontFile, signature);
223                 if (fontConfig == null) {
224                     // Use preinstalled font config for checking revision number.
225                     fontConfig = mConfigSupplier.apply(Collections.emptyMap());
226                 }
227                 addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, true /* deleteOldFile */);
228             }
229 
230             // Treat as error if post script name of font family was not installed.
231             for (int i = 0; i < config.fontFamilies.size(); ++i) {
232                 FontUpdateRequest.Family family = config.fontFamilies.get(i);
233                 for (int j = 0; j < family.getFonts().size(); ++j) {
234                     FontUpdateRequest.Font font = family.getFonts().get(j);
235                     if (mFontFileInfoMap.containsKey(font.getPostScriptName())) {
236                         continue;
237                     }
238 
239                     if (fontConfig == null) {
240                         fontConfig = mConfigSupplier.apply(Collections.emptyMap());
241                     }
242 
243                     if (getFontByPostScriptName(font.getPostScriptName(), fontConfig) != null) {
244                         continue;
245                     }
246 
247                     Slog.e(TAG, "Unknown font that has PostScript name "
248                             + font.getPostScriptName() + " is requested in FontFamily "
249                             + family.getName());
250                     return;
251                 }
252             }
253 
254             success = true;
255         } catch (Throwable t) {
256             // If something happened during loading system fonts, clear all contents in finally
257             // block. Here, just dumping errors.
258             Slog.e(TAG, "Failed to load font mappings.", t);
259         } finally {
260             // Delete all files just in case if we find a problematic file.
261             if (!success) {
262                 mFontFileInfoMap.clear();
263                 mLastModifiedMillis = 0;
264                 FileUtils.deleteContents(mFilesDir);
265                 mConfigFile.delete();
266             }
267         }
268     }
269 
270     /**
271      * Applies multiple {@link FontUpdateRequest}s in transaction.
272      * If one of the request fails, the fonts and config are rolled back to the previous state
273      * before this method is called.
274      */
update(List<FontUpdateRequest> requests)275     public void update(List<FontUpdateRequest> requests) throws SystemFontException {
276         for (FontUpdateRequest request : requests) {
277             switch (request.getType()) {
278                 case FontUpdateRequest.TYPE_UPDATE_FONT_FILE:
279                     Objects.requireNonNull(request.getFd());
280                     Objects.requireNonNull(request.getSignature());
281                     break;
282                 case FontUpdateRequest.TYPE_UPDATE_FONT_FAMILY:
283                     Objects.requireNonNull(request.getFontFamily());
284                     Objects.requireNonNull(request.getFontFamily().getName());
285                     break;
286             }
287         }
288         // Backup the mapping for rollback.
289         ArrayMap<String, FontFileInfo> backupMap = new ArrayMap<>(mFontFileInfoMap);
290         PersistentSystemFontConfig.Config curConfig = readPersistentConfig();
291         Map<String, FontUpdateRequest.Family> familyMap = new HashMap<>();
292         for (int i = 0; i < curConfig.fontFamilies.size(); ++i) {
293             FontUpdateRequest.Family family = curConfig.fontFamilies.get(i);
294             familyMap.put(family.getName(), family);
295         }
296 
297         long backupLastModifiedDate = mLastModifiedMillis;
298         boolean success = false;
299         try {
300             for (FontUpdateRequest request : requests) {
301                 switch (request.getType()) {
302                     case FontUpdateRequest.TYPE_UPDATE_FONT_FILE:
303                         installFontFile(
304                                 request.getFd().getFileDescriptor(), request.getSignature());
305                         break;
306                     case FontUpdateRequest.TYPE_UPDATE_FONT_FAMILY:
307                         FontUpdateRequest.Family family = request.getFontFamily();
308                         familyMap.put(family.getName(), family);
309                         break;
310                 }
311             }
312 
313             // Before processing font family update, check all family points the available fonts.
314             for (FontUpdateRequest.Family family : familyMap.values()) {
315                 if (resolveFontFilesForNamedFamily(family) == null) {
316                     throw new SystemFontException(
317                             FontManager.RESULT_ERROR_FONT_NOT_FOUND,
318                             "Required fonts are not available");
319                 }
320             }
321 
322             // Write config file.
323             mLastModifiedMillis = mCurrentTimeSupplier.get();
324 
325             PersistentSystemFontConfig.Config newConfig = new PersistentSystemFontConfig.Config();
326             newConfig.lastModifiedMillis = mLastModifiedMillis;
327             for (FontFileInfo info : mFontFileInfoMap.values()) {
328                 newConfig.updatedFontDirs.add(info.getRandomizedFontDir().getName());
329             }
330             newConfig.fontFamilies.addAll(familyMap.values());
331             writePersistentConfig(newConfig);
332             mConfigVersion++;
333             success = true;
334         } finally {
335             if (!success) {
336                 mFontFileInfoMap.clear();
337                 mFontFileInfoMap.putAll(backupMap);
338                 mLastModifiedMillis = backupLastModifiedDate;
339             }
340         }
341     }
342 
343     /**
344      * Installs a new font file, or updates an existing font file.
345      *
346      * <p>The new font will be immediately available for new Zygote-forked processes through
347      * {@link #getPostScriptMap()}. Old font files will be kept until next system server reboot,
348      * because existing Zygote-forked processes have paths to old font files.
349      *
350      * @param fd             A file descriptor to the font file.
351      * @param pkcs7Signature A PKCS#7 detached signature to enable fs-verity for the font file.
352      * @throws SystemFontException if error occurs.
353      */
installFontFile(FileDescriptor fd, byte[] pkcs7Signature)354     private void installFontFile(FileDescriptor fd, byte[] pkcs7Signature)
355             throws SystemFontException {
356         File newDir = getRandomDir(mFilesDir);
357         if (!newDir.mkdir()) {
358             throw new SystemFontException(
359                     FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
360                     "Failed to create font directory.");
361         }
362         try {
363             // Make newDir executable so that apps can access font file inside newDir.
364             Os.chmod(newDir.getAbsolutePath(), 0711);
365         } catch (ErrnoException e) {
366             throw new SystemFontException(
367                     FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
368                     "Failed to change mode to 711", e);
369         }
370         boolean success = false;
371         try {
372             File tempNewFontFile = new File(newDir, "font.ttf");
373             try (FileOutputStream out = new FileOutputStream(tempNewFontFile)) {
374                 FileUtils.copy(fd, out.getFD());
375             } catch (IOException e) {
376                 throw new SystemFontException(
377                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
378                         "Failed to write font file to storage.", e);
379             }
380             try {
381                 // Do not parse font file before setting up fs-verity.
382                 // setUpFsverity throws IOException if failed.
383                 mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath());
384             } catch (IOException e) {
385                 throw new SystemFontException(
386                         FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
387                         "Failed to setup fs-verity.", e);
388             }
389             String fontFileName;
390             try {
391                 fontFileName = mParser.buildFontFileName(tempNewFontFile);
392             } catch (IOException e) {
393                 throw new SystemFontException(
394                         FontManager.RESULT_ERROR_INVALID_FONT_FILE,
395                         "Failed to read PostScript name from font file", e);
396             }
397             if (fontFileName == null) {
398                 throw new SystemFontException(
399                         FontManager.RESULT_ERROR_INVALID_FONT_NAME,
400                         "Failed to read PostScript name from font file");
401             }
402             File newFontFile = new File(newDir, fontFileName);
403             if (!mFsverityUtil.rename(tempNewFontFile, newFontFile)) {
404                 throw new SystemFontException(
405                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
406                         "Failed to move verified font file.");
407             }
408             try {
409                 // Make the font file readable by apps.
410                 Os.chmod(newFontFile.getAbsolutePath(), 0644);
411             } catch (ErrnoException e) {
412                 throw new SystemFontException(
413                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
414                         "Failed to change font file mode to 644", e);
415             }
416             File signatureFile = new File(newDir, FONT_SIGNATURE_FILE);
417             try (FileOutputStream out = new FileOutputStream(signatureFile)) {
418                 out.write(pkcs7Signature);
419             } catch (IOException e) {
420                 // TODO: Do we need new error code for signature write failure?
421                 throw new SystemFontException(
422                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
423                         "Failed to write font signature file to storage.", e);
424             }
425             try {
426                 Os.chmod(signatureFile.getAbsolutePath(), 0600);
427             } catch (ErrnoException e) {
428                 throw new SystemFontException(
429                         FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
430                         "Failed to change the signature file mode to 600", e);
431             }
432             FontFileInfo fontFileInfo = validateFontFile(newFontFile, pkcs7Signature);
433 
434             // Try to create Typeface and treat as failure something goes wrong.
435             try {
436                 mParser.tryToCreateTypeface(fontFileInfo.getFile());
437             } catch (Throwable t) {
438                 throw new SystemFontException(
439                         FontManager.RESULT_ERROR_INVALID_FONT_FILE,
440                         "Failed to create Typeface from file", t);
441             }
442 
443             FontConfig fontConfig = getSystemFontConfig();
444             if (!addFileToMapIfSameOrNewer(fontFileInfo, fontConfig, false)) {
445                 throw new SystemFontException(
446                         FontManager.RESULT_ERROR_DOWNGRADING,
447                         "Downgrading font file is forbidden.");
448             }
449             success = true;
450         } finally {
451             if (!success) {
452                 FileUtils.deleteContentsAndDir(newDir);
453             }
454         }
455     }
456 
457     /**
458      * Given {@code parent}, returns {@code parent/~~[randomStr]}.
459      * Makes sure that {@code parent/~~[randomStr]} directory doesn't exist.
460      * Notice that this method doesn't actually create any directory.
461      */
getRandomDir(File parent)462     private static File getRandomDir(File parent) {
463         SecureRandom random = new SecureRandom();
464         byte[] bytes = new byte[16];
465         File dir;
466         do {
467             random.nextBytes(bytes);
468             String dirName = RANDOM_DIR_PREFIX
469                     + Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
470             dir = new File(parent, dirName);
471         } while (dir.exists());
472         return dir;
473     }
474 
lookupFontFileInfo(String psName)475     private FontFileInfo lookupFontFileInfo(String psName) {
476         return mFontFileInfoMap.get(psName);
477     }
478 
putFontFileInfo(FontFileInfo info)479     private void putFontFileInfo(FontFileInfo info) {
480         mFontFileInfoMap.put(info.getPostScriptName(), info);
481     }
482 
483     /**
484      * Add the given {@link FontFileInfo} to {@link #mFontFileInfoMap} if its font revision is
485      * equal to or higher than the revision of currently used font file (either in
486      * {@link #mFontFileInfoMap} or {@code fontConfig}).
487      */
addFileToMapIfSameOrNewer(FontFileInfo fontFileInfo, FontConfig fontConfig, boolean deleteOldFile)488     private boolean addFileToMapIfSameOrNewer(FontFileInfo fontFileInfo, FontConfig fontConfig,
489             boolean deleteOldFile) {
490         FontFileInfo existingInfo = lookupFontFileInfo(fontFileInfo.getPostScriptName());
491         final boolean shouldAddToMap;
492         if (existingInfo == null) {
493             // We got a new updatable font. We need to check if it's newer than preinstalled fonts.
494             // Note that getPreinstalledFontRevision() returns -1 if there is no preinstalled font
495             // with 'name'.
496             long preInstalledRev = getPreinstalledFontRevision(fontFileInfo, fontConfig);
497             shouldAddToMap = preInstalledRev <= fontFileInfo.getRevision();
498         } else {
499             shouldAddToMap = existingInfo.getRevision() <= fontFileInfo.getRevision();
500         }
501         if (shouldAddToMap) {
502             if (deleteOldFile && existingInfo != null) {
503                 FileUtils.deleteContentsAndDir(existingInfo.getRandomizedFontDir());
504             }
505             putFontFileInfo(fontFileInfo);
506         } else {
507             if (deleteOldFile) {
508                 FileUtils.deleteContentsAndDir(fontFileInfo.getRandomizedFontDir());
509             }
510         }
511         return shouldAddToMap;
512     }
513 
getFontByPostScriptName(String psName, FontConfig fontConfig)514     private FontConfig.Font getFontByPostScriptName(String psName, FontConfig fontConfig) {
515         FontConfig.Font targetFont = null;
516         for (int i = 0; i < fontConfig.getFontFamilies().size(); i++) {
517             FontConfig.FontFamily family = fontConfig.getFontFamilies().get(i);
518             for (int j = 0; j < family.getFontList().size(); ++j) {
519                 FontConfig.Font font = family.getFontList().get(j);
520                 if (font.getPostScriptName().equals(psName)) {
521                     targetFont = font;
522                     break;
523                 }
524             }
525         }
526         for (int i = 0; i < fontConfig.getNamedFamilyLists().size(); ++i) {
527             FontConfig.NamedFamilyList namedFamilyList = fontConfig.getNamedFamilyLists().get(i);
528             for (int j = 0; j < namedFamilyList.getFamilies().size(); ++j) {
529                 FontConfig.FontFamily family = namedFamilyList.getFamilies().get(j);
530                 for (int k = 0; k < family.getFontList().size(); ++k) {
531                     FontConfig.Font font = family.getFontList().get(k);
532                     if (font.getPostScriptName().equals(psName)) {
533                         targetFont = font;
534                         break;
535                     }
536                 }
537             }
538         }
539         return targetFont;
540     }
541 
getPreinstalledFontRevision(FontFileInfo info, FontConfig fontConfig)542     private long getPreinstalledFontRevision(FontFileInfo info, FontConfig fontConfig) {
543         String psName = info.getPostScriptName();
544         FontConfig.Font targetFont = getFontByPostScriptName(psName, fontConfig);
545 
546         if (targetFont == null) {
547             return -1;
548         }
549 
550         File preinstalledFontFile = targetFont.getOriginalFile() != null
551                 ? targetFont.getOriginalFile() : targetFont.getFile();
552         if (!preinstalledFontFile.exists()) {
553             return -1;
554         }
555         long revision = getFontRevision(preinstalledFontFile);
556         if (revision == -1) {
557             Slog.w(TAG, "Invalid preinstalled font file");
558         }
559         return revision;
560     }
561 
562     /**
563      * Checks the fs-verity protection status of the given font file, validates the file name, and
564      * returns a {@link FontFileInfo} on success. This method does not check if the font revision
565      * is higher than the currently used font.
566      */
567     @NonNull
validateFontFile(File file, byte[] pkcs7Signature)568     private FontFileInfo validateFontFile(File file, byte[] pkcs7Signature)
569             throws SystemFontException {
570         if (!mFsverityUtil.isFromTrustedProvider(file.getAbsolutePath(), pkcs7Signature)) {
571             throw new SystemFontException(
572                     FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
573                     "Font validation failed. Fs-verity is not enabled: " + file);
574         }
575         final String psName;
576         try {
577             psName = mParser.getPostScriptName(file);
578         } catch (IOException e) {
579             throw new SystemFontException(
580                     FontManager.RESULT_ERROR_INVALID_FONT_NAME,
581                     "Font validation failed. Could not read PostScript name name: " + file);
582         }
583         long revision = getFontRevision(file);
584         if (revision == -1) {
585             throw new SystemFontException(
586                     FontManager.RESULT_ERROR_INVALID_FONT_FILE,
587                     "Font validation failed. Could not read font revision: " + file);
588         }
589         return new FontFileInfo(file, psName, revision);
590     }
591     /** Returns the non-negative font revision of the given font file, or -1. */
getFontRevision(File file)592     private long getFontRevision(File file) {
593         try {
594             return mParser.getRevision(file);
595         } catch (IOException e) {
596             Slog.e(TAG, "Failed to read font file", e);
597             return -1;
598         }
599     }
600 
resolveFontFilesForNamedFamily( FontUpdateRequest.Family fontFamily)601     private FontConfig.NamedFamilyList resolveFontFilesForNamedFamily(
602             FontUpdateRequest.Family fontFamily) {
603         List<FontUpdateRequest.Font> fontList = fontFamily.getFonts();
604         List<FontConfig.Font> resolvedFonts = new ArrayList<>(fontList.size());
605         for (int i = 0; i < fontList.size(); i++) {
606             FontUpdateRequest.Font font = fontList.get(i);
607             FontFileInfo info = mFontFileInfoMap.get(font.getPostScriptName());
608             if (info == null) {
609                 Slog.e(TAG, "Failed to lookup font file that has " + font.getPostScriptName());
610                 return null;
611             }
612             resolvedFonts.add(new FontConfig.Font(info.mFile, null, info.getPostScriptName(),
613                     font.getFontStyle(), font.getIndex(), font.getFontVariationSettings(),
614                     null /* family name */, FontConfig.Font.VAR_TYPE_AXES_NONE));
615         }
616         FontConfig.FontFamily family = new FontConfig.FontFamily(resolvedFonts,
617                 LocaleList.getEmptyLocaleList(), FontConfig.FontFamily.VARIANT_DEFAULT);
618         return new FontConfig.NamedFamilyList(Collections.singletonList(family),
619                 fontFamily.getName());
620     }
621 
getPostScriptMap()622     Map<String, File> getPostScriptMap() {
623         Map<String, File> map = new ArrayMap<>();
624         for (int i = 0; i < mFontFileInfoMap.size(); ++i) {
625             FontFileInfo info = mFontFileInfoMap.valueAt(i);
626             map.put(info.getPostScriptName(), info.getFile());
627         }
628         return map;
629     }
630 
getSystemFontConfig()631     /* package */ FontConfig getSystemFontConfig() {
632         FontConfig config = mConfigSupplier.apply(getPostScriptMap());
633         PersistentSystemFontConfig.Config persistentConfig = readPersistentConfig();
634         List<FontUpdateRequest.Family> families = persistentConfig.fontFamilies;
635 
636         List<FontConfig.NamedFamilyList> mergedFamilies =
637                 new ArrayList<>(config.getNamedFamilyLists().size() + families.size());
638         // We should keep the first font family (config.getFontFamilies().get(0)) because it's used
639         // as a fallback font. See SystemFonts.java.
640         mergedFamilies.addAll(config.getNamedFamilyLists());
641         // When building Typeface, a latter font family definition will override the previous font
642         // family definition with the same name. An exception is config.getFontFamilies.get(0),
643         // which will be used as a fallback font without being overridden.
644         for (int i = 0; i < families.size(); ++i) {
645             FontConfig.NamedFamilyList family = resolveFontFilesForNamedFamily(families.get(i));
646             if (family != null) {
647                 mergedFamilies.add(family);
648             }
649         }
650 
651         return new FontConfig(
652                 config.getFontFamilies(), config.getAliases(), mergedFamilies,
653                 config.getLocaleFallbackCustomizations(), mLastModifiedMillis, mConfigVersion);
654     }
655 
readPersistentConfig()656     private PersistentSystemFontConfig.Config readPersistentConfig() {
657         PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config();
658         try (FileInputStream fis = mConfigFile.openRead()) {
659             PersistentSystemFontConfig.loadFromXml(fis, config);
660         } catch (IOException | XmlPullParserException e) {
661             // The font config file is missing on the first boot. Just do nothing.
662         }
663         return config;
664     }
665 
writePersistentConfig(PersistentSystemFontConfig.Config config)666     private void writePersistentConfig(PersistentSystemFontConfig.Config config)
667             throws SystemFontException {
668         FileOutputStream fos = null;
669         try {
670             fos = mConfigFile.startWrite();
671             PersistentSystemFontConfig.writeToXml(fos, config);
672             mConfigFile.finishWrite(fos);
673         } catch (IOException e) {
674             if (fos != null) {
675                 mConfigFile.failWrite(fos);
676             }
677             throw new SystemFontException(
678                     FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG,
679                     "Failed to write config XML.", e);
680         }
681     }
682 
getConfigVersion()683     /* package */ int getConfigVersion() {
684         return mConfigVersion;
685     }
686 
getFontFamilyMap()687     public Map<String, FontConfig.NamedFamilyList> getFontFamilyMap() {
688         PersistentSystemFontConfig.Config curConfig = readPersistentConfig();
689         Map<String, FontConfig.NamedFamilyList> familyMap = new HashMap<>();
690         for (int i = 0; i < curConfig.fontFamilies.size(); ++i) {
691             FontUpdateRequest.Family family = curConfig.fontFamilies.get(i);
692             FontConfig.NamedFamilyList resolvedFamily = resolveFontFilesForNamedFamily(family);
693             if (resolvedFamily != null) {
694                 familyMap.put(family.getName(), resolvedFamily);
695             }
696         }
697         return familyMap;
698     }
699 
deleteAllFiles(File filesDir, File configFile)700     /* package */ static void deleteAllFiles(File filesDir, File configFile) {
701         // As this method is called in safe mode, try to delete all files even though an exception
702         // is thrown.
703         try {
704             new AtomicFile(configFile).delete();
705         } catch (Throwable t) {
706             Slog.w(TAG, "Failed to delete " + configFile);
707         }
708         try {
709             FileUtils.deleteContents(filesDir);
710         } catch (Throwable t) {
711             Slog.w(TAG, "Failed to delete " + filesDir);
712         }
713     }
714 }
715