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