• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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 android.graphics;
18 
19 import static android.text.FontConfig.NamedFamilyList;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.graphics.fonts.FontCustomizationParser;
25 import android.graphics.fonts.FontStyle;
26 import android.graphics.fonts.FontVariationAxis;
27 import android.os.Build;
28 import android.os.LocaleList;
29 import android.text.FontConfig;
30 import android.util.ArraySet;
31 import android.util.Xml;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import java.io.File;
37 import java.io.FileInputStream;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 import java.util.regex.Pattern;
46 
47 /**
48  * Parser for font config files.
49  * @hide
50  */
51 @android.ravenwood.annotation.RavenwoodKeepWholeClass
52 public class FontListParser {
53     private static final String TAG = "FontListParser";
54 
55     // XML constants for FontFamily.
56     private static final String ATTR_NAME = "name";
57     private static final String ATTR_LANG = "lang";
58     private static final String ATTR_VARIANT = "variant";
59     private static final String TAG_FONT = "font";
60     private static final String VARIANT_COMPACT = "compact";
61     private static final String VARIANT_ELEGANT = "elegant";
62 
63     // XML constants for Font.
64     public static final String ATTR_SUPPORTED_AXES = "supportedAxes";
65     public static final String ATTR_INDEX = "index";
66     public static final String ATTR_WEIGHT = "weight";
67     public static final String ATTR_POSTSCRIPT_NAME = "postScriptName";
68     public static final String ATTR_STYLE = "style";
69     public static final String ATTR_FALLBACK_FOR = "fallbackFor";
70     public static final String STYLE_ITALIC = "italic";
71     public static final String STYLE_NORMAL = "normal";
72     public static final String TAG_AXIS = "axis";
73 
74     // XML constants for FontVariationAxis.
75     public static final String ATTR_TAG = "tag";
76     public static final String ATTR_STYLEVALUE = "stylevalue";
77 
78     // The tag string for variable font type resolution.
79     private static final String TAG_WGHT = "wght";
80     private static final String TAG_ITAL = "ital";
81 
82     /* Parse fallback list (no names) */
83     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
parse(InputStream in)84     public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
85         XmlPullParser parser = Xml.newPullParser();
86         parser.setInput(in, null);
87         parser.nextTag();
88         return readFamilies(parser, "/system/fonts/", new FontCustomizationParser.Result(), null,
89                 0, 0, true);
90     }
91 
92     /**
93      * Parses system font config XMLs
94      *
95      * @param fontsXmlPath location of fonts.xml
96      * @param systemFontDir location of system font directory
97      * @param oemCustomizationXmlPath location of oem_customization.xml
98      * @param productFontDir location of oem customized font directory
99      * @param updatableFontMap map of updated font files.
100      * @return font configuration
101      * @throws IOException
102      * @throws XmlPullParserException
103      */
parse( @onNull String fontsXmlPath, @NonNull String systemFontDir, @Nullable String oemCustomizationXmlPath, @Nullable String productFontDir, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )104     public static FontConfig parse(
105             @NonNull String fontsXmlPath,
106             @NonNull String systemFontDir,
107             @Nullable String oemCustomizationXmlPath,
108             @Nullable String productFontDir,
109             @Nullable Map<String, File> updatableFontMap,
110             long lastModifiedDate,
111             int configVersion
112     ) throws IOException, XmlPullParserException {
113         FontCustomizationParser.Result oemCustomization;
114         if (oemCustomizationXmlPath != null) {
115             try (InputStream is = new FileInputStream(oemCustomizationXmlPath)) {
116                 oemCustomization = FontCustomizationParser.parse(is, productFontDir,
117                         updatableFontMap);
118             } catch (IOException e) {
119                 // OEM customization may not exists. Ignoring
120                 oemCustomization = new FontCustomizationParser.Result();
121             }
122         } else {
123             oemCustomization = new FontCustomizationParser.Result();
124         }
125 
126         try (InputStream is = new FileInputStream(fontsXmlPath)) {
127             XmlPullParser parser = Xml.newPullParser();
128             parser.setInput(is, null);
129             parser.nextTag();
130             return readFamilies(parser, systemFontDir, oemCustomization, updatableFontMap,
131                     lastModifiedDate, configVersion, false /* filter out the non-existing files */);
132         }
133     }
134 
135     /**
136      * Parses the familyset tag in font.xml
137      * @param parser a XML pull parser
138      * @param fontDir A system font directory, e.g. "/system/fonts"
139      * @param customization A OEM font customization
140      * @param updatableFontMap A map of updated font files
141      * @param lastModifiedDate A date that the system font is updated.
142      * @param configVersion A version of system font config.
143      * @param allowNonExistingFile true if allowing non-existing font files during parsing fonts.xml
144      * @return result of fonts.xml
145      *
146      * @throws XmlPullParserException
147      * @throws IOException
148      *
149      * @hide
150      */
readFamilies( @onNull XmlPullParser parser, @NonNull String fontDir, @NonNull FontCustomizationParser.Result customization, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion, boolean allowNonExistingFile)151     public static FontConfig readFamilies(
152             @NonNull XmlPullParser parser,
153             @NonNull String fontDir,
154             @NonNull FontCustomizationParser.Result customization,
155             @Nullable Map<String, File> updatableFontMap,
156             long lastModifiedDate,
157             int configVersion,
158             boolean allowNonExistingFile)
159             throws XmlPullParserException, IOException {
160         List<FontConfig.FontFamily> families = new ArrayList<>();
161         List<FontConfig.NamedFamilyList> resultNamedFamilies = new ArrayList<>();
162         List<FontConfig.Alias> aliases = new ArrayList<>(customization.getAdditionalAliases());
163 
164         Map<String, NamedFamilyList> oemNamedFamilies =
165                 customization.getAdditionalNamedFamilies();
166 
167         boolean firstFamily = true;
168         parser.require(XmlPullParser.START_TAG, null, "familyset");
169         while (keepReading(parser)) {
170             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
171             String tag = parser.getName();
172             if (tag.equals("family")) {
173                 final String name = parser.getAttributeValue(null, "name");
174                 if (name == null) {
175                     FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap,
176                             allowNonExistingFile);
177                     if (family == null) {
178                         continue;
179                     }
180                     families.add(family);
181 
182                 } else {
183                     FontConfig.NamedFamilyList namedFamilyList = readNamedFamily(
184                             parser, fontDir, updatableFontMap, allowNonExistingFile);
185                     if (namedFamilyList == null) {
186                         continue;
187                     }
188                     if (!oemNamedFamilies.containsKey(name)) {
189                         // The OEM customization overrides system named family. Skip if OEM
190                         // customization XML defines the same named family.
191                         resultNamedFamilies.add(namedFamilyList);
192                     }
193                     if (firstFamily) {
194                         // The first font family is used as a fallback family as well.
195                         families.addAll(namedFamilyList.getFamilies());
196                     }
197                 }
198                 firstFamily = false;
199             } else if (tag.equals("family-list")) {
200                 FontConfig.NamedFamilyList namedFamilyList = readNamedFamilyList(
201                         parser, fontDir, updatableFontMap, allowNonExistingFile);
202                 if (namedFamilyList == null) {
203                     continue;
204                 }
205                 if (!oemNamedFamilies.containsKey(namedFamilyList.getName())) {
206                     // The OEM customization overrides system named family. Skip if OEM
207                     // customization XML defines the same named family.
208                     resultNamedFamilies.add(namedFamilyList);
209                 }
210                 if (firstFamily) {
211                     // The first font family is used as a fallback family as well.
212                     families.addAll(namedFamilyList.getFamilies());
213                 }
214                 firstFamily = false;
215             } else if (tag.equals("alias")) {
216                 aliases.add(readAlias(parser));
217             } else {
218                 skip(parser);
219             }
220         }
221 
222         resultNamedFamilies.addAll(oemNamedFamilies.values());
223 
224         // Filters aliases that point to non-existing families.
225         Set<String> namedFamilies = new ArraySet<>();
226         for (int i = 0; i < resultNamedFamilies.size(); ++i) {
227             String name = resultNamedFamilies.get(i).getName();
228             if (name != null) {
229                 namedFamilies.add(name);
230             }
231         }
232         List<FontConfig.Alias> filtered = new ArrayList<>();
233         for (int i = 0; i < aliases.size(); ++i) {
234             FontConfig.Alias alias = aliases.get(i);
235             if (namedFamilies.contains(alias.getOriginal())) {
236                 filtered.add(alias);
237             }
238         }
239 
240         return new FontConfig(families, filtered, resultNamedFamilies,
241                 customization.getLocaleFamilyCustomizations(),
242                 lastModifiedDate,
243                 configVersion);
244     }
245 
keepReading(XmlPullParser parser)246     private static boolean keepReading(XmlPullParser parser)
247             throws XmlPullParserException, IOException {
248         int next = parser.next();
249         return next != XmlPullParser.END_TAG && next != XmlPullParser.END_DOCUMENT;
250     }
251 
252     /**
253      * Read family tag in fonts.xml or oem_customization.xml
254      *
255      * @param parser An XML parser.
256      * @param fontDir a font directory name.
257      * @param updatableFontMap a updated font file map.
258      * @param allowNonExistingFile true to allow font file that doesn't exist.
259      * @return a FontFamily instance. null if no font files are available in this FontFamily.
260      */
readFamily(XmlPullParser parser, String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)261     public static @Nullable FontConfig.FontFamily readFamily(XmlPullParser parser, String fontDir,
262             @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)
263             throws XmlPullParserException, IOException {
264         final String lang = parser.getAttributeValue("", "lang");
265         final String variant = parser.getAttributeValue(null, "variant");
266         final String ignore = parser.getAttributeValue(null, "ignore");
267         final List<FontConfig.Font> fonts = new ArrayList<>();
268         while (keepReading(parser)) {
269             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
270             final String tag = parser.getName();
271             if (tag.equals(TAG_FONT)) {
272                 FontConfig.Font font = readFont(parser, fontDir, updatableFontMap,
273                         allowNonExistingFile);
274                 if (font != null) {
275                     fonts.add(font);
276                 }
277             } else {
278                 skip(parser);
279             }
280         }
281         int intVariant = FontConfig.FontFamily.VARIANT_DEFAULT;
282         if (variant != null) {
283             if (variant.equals(VARIANT_COMPACT)) {
284                 intVariant = FontConfig.FontFamily.VARIANT_COMPACT;
285             } else if (variant.equals(VARIANT_ELEGANT)) {
286                 intVariant = FontConfig.FontFamily.VARIANT_ELEGANT;
287             }
288         }
289 
290         boolean skip = (ignore != null && (ignore.equals("true") || ignore.equals("1")));
291         if (skip || fonts.isEmpty()) {
292             return null;
293         }
294         return new FontConfig.FontFamily(fonts, LocaleList.forLanguageTags(lang), intVariant);
295     }
296 
throwIfAttributeExists(String attrName, XmlPullParser parser)297     private static void throwIfAttributeExists(String attrName, XmlPullParser parser) {
298         if (parser.getAttributeValue(null, attrName) != null) {
299             throw new IllegalArgumentException(attrName + " cannot be used in FontFamily inside "
300                     + " family or family-list with name attribute.");
301         }
302     }
303 
304     /**
305      * Read a font family with name attribute as a single element family-list element.
306      */
readNamedFamily( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)307     public static @Nullable FontConfig.NamedFamilyList readNamedFamily(
308             @NonNull XmlPullParser parser, @NonNull String fontDir,
309             @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)
310             throws XmlPullParserException, IOException {
311         final String name = parser.getAttributeValue(null, "name");
312         throwIfAttributeExists("lang", parser);
313         throwIfAttributeExists("variant", parser);
314         throwIfAttributeExists("ignore", parser);
315 
316         final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap,
317                 allowNonExistingFile);
318         if (family == null) {
319             return null;
320         }
321         return new NamedFamilyList(Collections.singletonList(family), name);
322     }
323 
324     /**
325      * Read a family-list element
326      */
readNamedFamilyList( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)327     public static @Nullable FontConfig.NamedFamilyList readNamedFamilyList(
328             @NonNull XmlPullParser parser, @NonNull String fontDir,
329             @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)
330             throws XmlPullParserException, IOException {
331         final String name = parser.getAttributeValue(null, "name");
332         final List<FontConfig.FontFamily> familyList = new ArrayList<>();
333         while (keepReading(parser)) {
334             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
335             final String tag = parser.getName();
336             if (tag.equals("family")) {
337                 throwIfAttributeExists("name", parser);
338                 throwIfAttributeExists("lang", parser);
339                 throwIfAttributeExists("variant", parser);
340                 throwIfAttributeExists("ignore", parser);
341 
342                 final FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap,
343                         allowNonExistingFile);
344                 if (family != null) {
345                     familyList.add(family);
346                 }
347             } else {
348                 skip(parser);
349             }
350         }
351 
352         if (familyList.isEmpty()) {
353             return null;
354         }
355         return new FontConfig.NamedFamilyList(familyList, name);
356     }
357 
358     /** Matches leading and trailing XML whitespace. */
359     private static final Pattern FILENAME_WHITESPACE_PATTERN =
360             Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
361 
readFont( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)362     private static @Nullable FontConfig.Font readFont(
363             @NonNull XmlPullParser parser,
364             @NonNull String fontDir,
365             @Nullable Map<String, File> updatableFontMap,
366             boolean allowNonExistingFile)
367             throws XmlPullParserException, IOException {
368 
369         String indexStr = parser.getAttributeValue(null, ATTR_INDEX);
370         int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
371         List<FontVariationAxis> axes = new ArrayList<>();
372         String weightStr = parser.getAttributeValue(null, ATTR_WEIGHT);
373         int weight = weightStr == null ? FontStyle.FONT_WEIGHT_NORMAL : Integer.parseInt(weightStr);
374         boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE));
375         String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR);
376         String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME);
377         final String supportedAxes = parser.getAttributeValue(null, ATTR_SUPPORTED_AXES);
378         StringBuilder filename = new StringBuilder();
379         while (keepReading(parser)) {
380             if (parser.getEventType() == XmlPullParser.TEXT) {
381                 filename.append(parser.getText());
382             }
383             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
384             String tag = parser.getName();
385             if (tag.equals(TAG_AXIS)) {
386                 axes.add(readAxis(parser));
387             } else {
388                 skip(parser);
389             }
390         }
391         String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
392 
393         int varTypeAxes = 0;
394         if (supportedAxes != null) {
395             for (String tag : supportedAxes.split(",")) {
396                 String strippedTag = tag.strip();
397                 if (strippedTag.equals(TAG_WGHT)) {
398                     varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_WGHT;
399                 } else if (strippedTag.equals(TAG_ITAL)) {
400                     varTypeAxes |= FontConfig.Font.VAR_TYPE_AXES_ITAL;
401                 }
402             }
403         }
404 
405         if (postScriptName == null) {
406             // If post script name was not provided, assume the file name is same to PostScript
407             // name.
408             postScriptName = sanitizedName.substring(0, sanitizedName.length() - 4);
409         }
410 
411         String updatedName = findUpdatedFontFile(postScriptName, updatableFontMap);
412         String filePath;
413         String originalPath;
414         if (updatedName != null) {
415             filePath = updatedName;
416             originalPath = fontDir + sanitizedName;
417         } else {
418             filePath = fontDir + sanitizedName;
419             originalPath = null;
420         }
421 
422         String varSettings;
423         if (axes.isEmpty()) {
424             varSettings = "";
425         } else {
426             varSettings = FontVariationAxis.toFontVariationSettings(
427                     axes.toArray(new FontVariationAxis[0]));
428         }
429 
430         File file = new File(filePath);
431 
432         if (!(allowNonExistingFile || file.isFile())) {
433             return null;
434         }
435 
436         return new FontConfig.Font(file,
437                 originalPath == null ? null : new File(originalPath),
438                 postScriptName,
439                 new FontStyle(
440                         weight,
441                         isItalic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT
442                 ),
443                 index,
444                 varSettings,
445                 fallbackFor,
446                 varTypeAxes);
447     }
448 
findUpdatedFontFile(String psName, @Nullable Map<String, File> updatableFontMap)449     private static String findUpdatedFontFile(String psName,
450             @Nullable Map<String, File> updatableFontMap) {
451         if (updatableFontMap != null) {
452             File updatedFile = updatableFontMap.get(psName);
453             if (updatedFile != null) {
454                 return updatedFile.getAbsolutePath();
455             }
456         }
457         return null;
458     }
459 
readAxis(XmlPullParser parser)460     private static FontVariationAxis readAxis(XmlPullParser parser)
461             throws XmlPullParserException, IOException {
462         String tagStr = parser.getAttributeValue(null, ATTR_TAG);
463         String styleValueStr = parser.getAttributeValue(null, ATTR_STYLEVALUE);
464         skip(parser);  // axis tag is empty, ignore any contents and consume end tag
465         return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr));
466     }
467 
468     /**
469      * Reads alias elements
470      */
readAlias(XmlPullParser parser)471     public static FontConfig.Alias readAlias(XmlPullParser parser)
472             throws XmlPullParserException, IOException {
473         String name = parser.getAttributeValue(null, "name");
474         String toName = parser.getAttributeValue(null, "to");
475         String weightStr = parser.getAttributeValue(null, "weight");
476         int weight;
477         if (weightStr == null) {
478             weight = FontStyle.FONT_WEIGHT_NORMAL;
479         } else {
480             weight = Integer.parseInt(weightStr);
481         }
482         skip(parser);  // alias tag is empty, ignore any contents and consume end tag
483         return new FontConfig.Alias(name, toName, weight);
484     }
485 
486     /**
487      * Skip until next element
488      */
skip(XmlPullParser parser)489     public static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
490         int depth = 1;
491         while (depth > 0) {
492             switch (parser.next()) {
493                 case XmlPullParser.START_TAG:
494                     depth++;
495                     break;
496                 case XmlPullParser.END_TAG:
497                     depth--;
498                     break;
499                 case XmlPullParser.END_DOCUMENT:
500                     return;
501             }
502         }
503     }
504 }
505