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