• 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 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 String ignore = parser.getAttributeValue(null, "ignore");
222         final List<FontConfig.Font> fonts = new ArrayList<>();
223         while (keepReading(parser)) {
224             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
225             final String tag = parser.getName();
226             if (tag.equals(TAG_FONT)) {
227                 FontConfig.Font font = readFont(parser, fontDir, updatableFontMap,
228                         allowNonExistingFile);
229                 if (font != null) {
230                     fonts.add(font);
231                 }
232             } else {
233                 skip(parser);
234             }
235         }
236         int intVariant = FontConfig.FontFamily.VARIANT_DEFAULT;
237         if (variant != null) {
238             if (variant.equals(VARIANT_COMPACT)) {
239                 intVariant = FontConfig.FontFamily.VARIANT_COMPACT;
240             } else if (variant.equals(VARIANT_ELEGANT)) {
241                 intVariant = FontConfig.FontFamily.VARIANT_ELEGANT;
242             }
243         }
244 
245         boolean skip = (ignore != null && (ignore.equals("true") || ignore.equals("1")));
246         if (skip || fonts.isEmpty()) {
247             return null;
248         }
249         return new FontConfig.FontFamily(fonts, name, LocaleList.forLanguageTags(lang), intVariant);
250     }
251 
252     /** Matches leading and trailing XML whitespace. */
253     private static final Pattern FILENAME_WHITESPACE_PATTERN =
254             Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
255 
readFont( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)256     private static @Nullable FontConfig.Font readFont(
257             @NonNull XmlPullParser parser,
258             @NonNull String fontDir,
259             @Nullable Map<String, File> updatableFontMap,
260             boolean allowNonExistingFile)
261             throws XmlPullParserException, IOException {
262 
263         String indexStr = parser.getAttributeValue(null, ATTR_INDEX);
264         int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
265         List<FontVariationAxis> axes = new ArrayList<>();
266         String weightStr = parser.getAttributeValue(null, ATTR_WEIGHT);
267         int weight = weightStr == null ? FontStyle.FONT_WEIGHT_NORMAL : Integer.parseInt(weightStr);
268         boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE));
269         String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR);
270         String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME);
271         StringBuilder filename = new StringBuilder();
272         while (keepReading(parser)) {
273             if (parser.getEventType() == XmlPullParser.TEXT) {
274                 filename.append(parser.getText());
275             }
276             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
277             String tag = parser.getName();
278             if (tag.equals(TAG_AXIS)) {
279                 axes.add(readAxis(parser));
280             } else {
281                 skip(parser);
282             }
283         }
284         String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
285 
286         if (postScriptName == null) {
287             // If post script name was not provided, assume the file name is same to PostScript
288             // name.
289             postScriptName = sanitizedName.substring(0, sanitizedName.length() - 4);
290         }
291 
292         String updatedName = findUpdatedFontFile(postScriptName, updatableFontMap);
293         String filePath;
294         String originalPath;
295         if (updatedName != null) {
296             filePath = updatedName;
297             originalPath = fontDir + sanitizedName;
298         } else {
299             filePath = fontDir + sanitizedName;
300             originalPath = null;
301         }
302 
303         String varSettings;
304         if (axes.isEmpty()) {
305             varSettings = "";
306         } else {
307             varSettings = FontVariationAxis.toFontVariationSettings(
308                     axes.toArray(new FontVariationAxis[0]));
309         }
310 
311         File file = new File(filePath);
312 
313         if (!(allowNonExistingFile || file.isFile())) {
314             return null;
315         }
316 
317         return new FontConfig.Font(file,
318                 originalPath == null ? null : new File(originalPath),
319                 postScriptName,
320                 new FontStyle(
321                         weight,
322                         isItalic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT
323                 ),
324                 index,
325                 varSettings,
326                 fallbackFor);
327     }
328 
findUpdatedFontFile(String psName, @Nullable Map<String, File> updatableFontMap)329     private static String findUpdatedFontFile(String psName,
330             @Nullable Map<String, File> updatableFontMap) {
331         if (updatableFontMap != null) {
332             File updatedFile = updatableFontMap.get(psName);
333             if (updatedFile != null) {
334                 return updatedFile.getAbsolutePath();
335             }
336         }
337         return null;
338     }
339 
readAxis(XmlPullParser parser)340     private static FontVariationAxis readAxis(XmlPullParser parser)
341             throws XmlPullParserException, IOException {
342         String tagStr = parser.getAttributeValue(null, ATTR_TAG);
343         String styleValueStr = parser.getAttributeValue(null, ATTR_STYLEVALUE);
344         skip(parser);  // axis tag is empty, ignore any contents and consume end tag
345         return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr));
346     }
347 
348     /**
349      * Reads alias elements
350      */
readAlias(XmlPullParser parser)351     public static FontConfig.Alias readAlias(XmlPullParser parser)
352             throws XmlPullParserException, IOException {
353         String name = parser.getAttributeValue(null, "name");
354         String toName = parser.getAttributeValue(null, "to");
355         String weightStr = parser.getAttributeValue(null, "weight");
356         int weight;
357         if (weightStr == null) {
358             weight = FontStyle.FONT_WEIGHT_NORMAL;
359         } else {
360             weight = Integer.parseInt(weightStr);
361         }
362         skip(parser);  // alias tag is empty, ignore any contents and consume end tag
363         return new FontConfig.Alias(name, toName, weight);
364     }
365 
366     /**
367      * Skip until next element
368      */
skip(XmlPullParser parser)369     public static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
370         int depth = 1;
371         while (depth > 0) {
372             switch (parser.next()) {
373                 case XmlPullParser.START_TAG:
374                     depth++;
375                     break;
376                 case XmlPullParser.END_TAG:
377                     depth--;
378                     break;
379                 case XmlPullParser.END_DOCUMENT:
380                     return;
381             }
382         }
383     }
384 }
385