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