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 android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.graphics.fonts.FontCustomizationParser;
23 import android.graphics.fonts.FontStyle;
24 import android.graphics.fonts.FontVariationAxis;
25 import android.os.Build;
26 import android.os.LocaleList;
27 import android.text.FontConfig;
28 import android.util.ArraySet;
29 import android.util.Xml;
30 
31 import org.xmlpull.v1.XmlPullParser;
32 import org.xmlpull.v1.XmlPullParserException;
33 
34 import java.io.File;
35 import java.io.FileInputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.regex.Pattern;
43 
44 /**
45  * Parser for font config files.
46  * @hide
47  */
48 public class FontListParser {
49 
50     // XML constants for FontFamily.
51     private static final String ATTR_NAME = "name";
52     private static final String ATTR_LANG = "lang";
53     private static final String ATTR_VARIANT = "variant";
54     private static final String TAG_FONT = "font";
55     private static final String VARIANT_COMPACT = "compact";
56     private static final String VARIANT_ELEGANT = "elegant";
57 
58     // XML constants for Font.
59     public static final String ATTR_INDEX = "index";
60     public static final String ATTR_WEIGHT = "weight";
61     public static final String ATTR_POSTSCRIPT_NAME = "postScriptName";
62     public static final String ATTR_STYLE = "style";
63     public static final String ATTR_FALLBACK_FOR = "fallbackFor";
64     public static final String STYLE_ITALIC = "italic";
65     public static final String STYLE_NORMAL = "normal";
66     public static final String TAG_AXIS = "axis";
67 
68     // XML constants for FontVariationAxis.
69     public static final String ATTR_TAG = "tag";
70     public static final String ATTR_STYLEVALUE = "stylevalue";
71 
72     /* Parse fallback list (no names) */
73     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
parse(InputStream in)74     public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
75         XmlPullParser parser = Xml.newPullParser();
76         parser.setInput(in, null);
77         parser.nextTag();
78         return readFamilies(parser, "/system/fonts/", new FontCustomizationParser.Result(), null,
79                 0, 0, true);
80     }
81 
82     /**
83      * Parses system font config XMLs
84      *
85      * @param fontsXmlPath location of fonts.xml
86      * @param systemFontDir location of system font directory
87      * @param oemCustomizationXmlPath location of oem_customization.xml
88      * @param productFontDir location of oem customized font directory
89      * @param updatableFontMap map of updated font files.
90      * @return font configuration
91      * @throws IOException
92      * @throws XmlPullParserException
93      */
parse( @onNull String fontsXmlPath, @NonNull String systemFontDir, @Nullable String oemCustomizationXmlPath, @Nullable String productFontDir, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )94     public static FontConfig parse(
95             @NonNull String fontsXmlPath,
96             @NonNull String systemFontDir,
97             @Nullable String oemCustomizationXmlPath,
98             @Nullable String productFontDir,
99             @Nullable Map<String, File> updatableFontMap,
100             long lastModifiedDate,
101             int configVersion
102     ) throws IOException, XmlPullParserException {
103         FontCustomizationParser.Result oemCustomization;
104         if (oemCustomizationXmlPath != null) {
105             try (InputStream is = new FileInputStream(oemCustomizationXmlPath)) {
106                 oemCustomization = FontCustomizationParser.parse(is, productFontDir,
107                         updatableFontMap);
108             } catch (IOException e) {
109                 // OEM customization may not exists. Ignoring
110                 oemCustomization = new FontCustomizationParser.Result();
111             }
112         } else {
113             oemCustomization = new FontCustomizationParser.Result();
114         }
115 
116         try (InputStream is = new FileInputStream(fontsXmlPath)) {
117             XmlPullParser parser = Xml.newPullParser();
118             parser.setInput(is, null);
119             parser.nextTag();
120             return readFamilies(parser, systemFontDir, oemCustomization, updatableFontMap,
121                     lastModifiedDate, configVersion, false /* filter out the non-exising files */);
122         }
123     }
124 
125     /**
126      * Parses the familyset tag in font.xml
127      * @param parser a XML pull parser
128      * @param fontDir A system font directory, e.g. "/system/fonts"
129      * @param customization A OEM font customization
130      * @param updatableFontMap A map of updated font files
131      * @param lastModifiedDate A date that the system font is updated.
132      * @param configVersion A version of system font config.
133      * @param allowNonExistingFile true if allowing non-existing font files during parsing fonts.xml
134      * @return result of fonts.xml
135      *
136      * @throws XmlPullParserException
137      * @throws IOException
138      *
139      * @hide
140      */
readFamilies( @onNull XmlPullParser parser, @NonNull String fontDir, @NonNull FontCustomizationParser.Result customization, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion, boolean allowNonExistingFile)141     public static FontConfig readFamilies(
142             @NonNull XmlPullParser parser,
143             @NonNull String fontDir,
144             @NonNull FontCustomizationParser.Result customization,
145             @Nullable Map<String, File> updatableFontMap,
146             long lastModifiedDate,
147             int configVersion,
148             boolean allowNonExistingFile)
149             throws XmlPullParserException, IOException {
150         List<FontConfig.FontFamily> families = new ArrayList<>();
151         List<FontConfig.Alias> aliases = new ArrayList<>(customization.getAdditionalAliases());
152 
153         Map<String, FontConfig.FontFamily> oemNamedFamilies =
154                 customization.getAdditionalNamedFamilies();
155 
156         parser.require(XmlPullParser.START_TAG, null, "familyset");
157         while (keepReading(parser)) {
158             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
159             String tag = parser.getName();
160             if (tag.equals("family")) {
161                 FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap,
162                         allowNonExistingFile);
163                 if (family == null) {
164                     continue;
165                 }
166                 String name = family.getName();
167                 if (name == null || !oemNamedFamilies.containsKey(name)) {
168                     // The OEM customization overrides system named family. Skip if OEM
169                     // customization XML defines the same named family.
170                     families.add(family);
171                 }
172             } else if (tag.equals("alias")) {
173                 aliases.add(readAlias(parser));
174             } else {
175                 skip(parser);
176             }
177         }
178 
179         families.addAll(oemNamedFamilies.values());
180 
181         // Filters aliases that point to non-existing families.
182         Set<String> namedFamilies = new ArraySet<>();
183         for (int i = 0; i < families.size(); ++i) {
184             String name = families.get(i).getName();
185             if (name != null) {
186                 namedFamilies.add(name);
187             }
188         }
189         List<FontConfig.Alias> filtered = new ArrayList<>();
190         for (int i = 0; i < aliases.size(); ++i) {
191             FontConfig.Alias alias = aliases.get(i);
192             if (namedFamilies.contains(alias.getOriginal())) {
193                 filtered.add(alias);
194             }
195         }
196 
197         return new FontConfig(families, filtered, lastModifiedDate, configVersion);
198     }
199 
keepReading(XmlPullParser parser)200     private static boolean keepReading(XmlPullParser parser)
201             throws XmlPullParserException, IOException {
202         int next = parser.next();
203         return next != XmlPullParser.END_TAG && next != XmlPullParser.END_DOCUMENT;
204     }
205 
206     /**
207      * Read family tag in fonts.xml or oem_customization.xml
208      *
209      * @param parser An XML parser.
210      * @param fontDir a font directory name.
211      * @param updatableFontMap a updated font file map.
212      * @param allowNonExistingFile true to allow font file that doesn't exists
213      * @return a FontFamily instance. null if no font files are available in this FontFamily.
214      */
readFamily(XmlPullParser parser, String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)215     public static @Nullable FontConfig.FontFamily readFamily(XmlPullParser parser, String fontDir,
216             @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)
217             throws XmlPullParserException, IOException {
218         final String name = parser.getAttributeValue(null, "name");
219         final String lang = parser.getAttributeValue("", "lang");
220         final String variant = parser.getAttributeValue(null, "variant");
221         final List<FontConfig.Font> fonts = new ArrayList<>();
222         while (keepReading(parser)) {
223             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
224             final String tag = parser.getName();
225             if (tag.equals(TAG_FONT)) {
226                 FontConfig.Font font = readFont(parser, fontDir, updatableFontMap,
227                         allowNonExistingFile);
228                 if (font != null) {
229                     fonts.add(font);
230                 }
231             } else {
232                 skip(parser);
233             }
234         }
235         int intVariant = FontConfig.FontFamily.VARIANT_DEFAULT;
236         if (variant != null) {
237             if (variant.equals(VARIANT_COMPACT)) {
238                 intVariant = FontConfig.FontFamily.VARIANT_COMPACT;
239             } else if (variant.equals(VARIANT_ELEGANT)) {
240                 intVariant = FontConfig.FontFamily.VARIANT_ELEGANT;
241             }
242         }
243         if (fonts.isEmpty()) {
244             return null;
245         }
246         return new FontConfig.FontFamily(fonts, name, LocaleList.forLanguageTags(lang), intVariant);
247     }
248 
249     /** Matches leading and trailing XML whitespace. */
250     private static final Pattern FILENAME_WHITESPACE_PATTERN =
251             Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
252 
readFont( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)253     private static @Nullable FontConfig.Font readFont(
254             @NonNull XmlPullParser parser,
255             @NonNull String fontDir,
256             @Nullable Map<String, File> updatableFontMap,
257             boolean allowNonExistingFile)
258             throws XmlPullParserException, IOException {
259 
260         String indexStr = parser.getAttributeValue(null, ATTR_INDEX);
261         int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
262         List<FontVariationAxis> axes = new ArrayList<>();
263         String weightStr = parser.getAttributeValue(null, ATTR_WEIGHT);
264         int weight = weightStr == null ? FontStyle.FONT_WEIGHT_NORMAL : Integer.parseInt(weightStr);
265         boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE));
266         String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR);
267         String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME);
268         StringBuilder filename = new StringBuilder();
269         while (keepReading(parser)) {
270             if (parser.getEventType() == XmlPullParser.TEXT) {
271                 filename.append(parser.getText());
272             }
273             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
274             String tag = parser.getName();
275             if (tag.equals(TAG_AXIS)) {
276                 axes.add(readAxis(parser));
277             } else {
278                 skip(parser);
279             }
280         }
281         String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
282 
283         if (postScriptName == null) {
284             // If post script name was not provided, assume the file name is same to PostScript
285             // name.
286             postScriptName = sanitizedName.substring(0, sanitizedName.length() - 4);
287         }
288 
289         String updatedName = findUpdatedFontFile(postScriptName, updatableFontMap);
290         String filePath;
291         String originalPath;
292         if (updatedName != null) {
293             filePath = updatedName;
294             originalPath = fontDir + sanitizedName;
295         } else {
296             filePath = fontDir + sanitizedName;
297             originalPath = null;
298         }
299 
300         String varSettings;
301         if (axes.isEmpty()) {
302             varSettings = "";
303         } else {
304             varSettings = FontVariationAxis.toFontVariationSettings(
305                     axes.toArray(new FontVariationAxis[0]));
306         }
307 
308         File file = new File(filePath);
309 
310         if (!(allowNonExistingFile || file.isFile())) {
311             return null;
312         }
313 
314         return new FontConfig.Font(file,
315                 originalPath == null ? null : new File(originalPath),
316                 postScriptName,
317                 new FontStyle(
318                         weight,
319                         isItalic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT
320                 ),
321                 index,
322                 varSettings,
323                 fallbackFor);
324     }
325 
findUpdatedFontFile(String psName, @Nullable Map<String, File> updatableFontMap)326     private static String findUpdatedFontFile(String psName,
327             @Nullable Map<String, File> updatableFontMap) {
328         if (updatableFontMap != null) {
329             File updatedFile = updatableFontMap.get(psName);
330             if (updatedFile != null) {
331                 return updatedFile.getAbsolutePath();
332             }
333         }
334         return null;
335     }
336 
readAxis(XmlPullParser parser)337     private static FontVariationAxis readAxis(XmlPullParser parser)
338             throws XmlPullParserException, IOException {
339         String tagStr = parser.getAttributeValue(null, ATTR_TAG);
340         String styleValueStr = parser.getAttributeValue(null, ATTR_STYLEVALUE);
341         skip(parser);  // axis tag is empty, ignore any contents and consume end tag
342         return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr));
343     }
344 
345     /**
346      * Reads alias elements
347      */
readAlias(XmlPullParser parser)348     public static FontConfig.Alias readAlias(XmlPullParser parser)
349             throws XmlPullParserException, IOException {
350         String name = parser.getAttributeValue(null, "name");
351         String toName = parser.getAttributeValue(null, "to");
352         String weightStr = parser.getAttributeValue(null, "weight");
353         int weight;
354         if (weightStr == null) {
355             weight = FontStyle.FONT_WEIGHT_NORMAL;
356         } else {
357             weight = Integer.parseInt(weightStr);
358         }
359         skip(parser);  // alias tag is empty, ignore any contents and consume end tag
360         return new FontConfig.Alias(name, toName, weight);
361     }
362 
363     /**
364      * Skip until next element
365      */
skip(XmlPullParser parser)366     public static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
367         int depth = 1;
368         while (depth > 0) {
369             switch (parser.next()) {
370                 case XmlPullParser.START_TAG:
371                     depth++;
372                     break;
373                 case XmlPullParser.END_TAG:
374                     depth--;
375                     break;
376                 case XmlPullParser.END_DOCUMENT:
377                     return;
378             }
379         }
380     }
381 }
382