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