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