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