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