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