blob: 93a336e7a4084216b94251263a654f535a0416ea [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.fonts.FontCustomizationParser;
import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontVariationAxis;
import android.os.Build;
import android.os.LocaleList;
import android.text.FontConfig;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Parser for font config files.
* @hide
*/
public class FontListParser {
// XML constants for FontFamily.
private static final String ATTR_NAME = "name";
private static final String ATTR_LANG = "lang";
private static final String ATTR_VARIANT = "variant";
private static final String TAG_FONT = "font";
private static final String VARIANT_COMPACT = "compact";
private static final String VARIANT_ELEGANT = "elegant";
// XML constants for Font.
public static final String ATTR_INDEX = "index";
public static final String ATTR_WEIGHT = "weight";
public static final String ATTR_POSTSCRIPT_NAME = "postScriptName";
public static final String ATTR_STYLE = "style";
public static final String ATTR_FALLBACK_FOR = "fallbackFor";
public static final String STYLE_ITALIC = "italic";
public static final String STYLE_NORMAL = "normal";
public static final String TAG_AXIS = "axis";
// XML constants for FontVariationAxis.
public static final String ATTR_TAG = "tag";
public static final String ATTR_STYLEVALUE = "stylevalue";
/* Parse fallback list (no names) */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(in, null);
parser.nextTag();
return readFamilies(parser, "/system/fonts/", new FontCustomizationParser.Result(), null,
0, 0, true);
}
/**
* Parses system font config XMLs
*
* @param fontsXmlPath location of fonts.xml
* @param systemFontDir location of system font directory
* @param oemCustomizationXmlPath location of oem_customization.xml
* @param productFontDir location of oem customized font directory
* @param updatableFontMap map of updated font files.
* @return font configuration
* @throws IOException
* @throws XmlPullParserException
*/
public static FontConfig parse(
@NonNull String fontsXmlPath,
@NonNull String systemFontDir,
@Nullable String oemCustomizationXmlPath,
@Nullable String productFontDir,
@Nullable Map<String, File> updatableFontMap,
long lastModifiedDate,
int configVersion
) throws IOException, XmlPullParserException {
FontCustomizationParser.Result oemCustomization;
if (oemCustomizationXmlPath != null) {
try (InputStream is = new FileInputStream(oemCustomizationXmlPath)) {
oemCustomization = FontCustomizationParser.parse(is, productFontDir,
updatableFontMap);
} catch (IOException e) {
// OEM customization may not exists. Ignoring
oemCustomization = new FontCustomizationParser.Result();
}
} else {
oemCustomization = new FontCustomizationParser.Result();
}
try (InputStream is = new FileInputStream(fontsXmlPath)) {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(is, null);
parser.nextTag();
return readFamilies(parser, systemFontDir, oemCustomization, updatableFontMap,
lastModifiedDate, configVersion, false /* filter out the non-exising files */);
}
}
private static FontConfig readFamilies(
@NonNull XmlPullParser parser,
@NonNull String fontDir,
@NonNull FontCustomizationParser.Result customization,
@Nullable Map<String, File> updatableFontMap,
long lastModifiedDate,
int configVersion,
boolean allowNonExistingFile)
throws XmlPullParserException, IOException {
List<FontConfig.FontFamily> families = new ArrayList<>();
List<FontConfig.Alias> aliases = new ArrayList<>(customization.getAdditionalAliases());
Map<String, FontConfig.FontFamily> oemNamedFamilies =
customization.getAdditionalNamedFamilies();
parser.require(XmlPullParser.START_TAG, null, "familyset");
while (keepReading(parser)) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
String tag = parser.getName();
if (tag.equals("family")) {
FontConfig.FontFamily family = readFamily(parser, fontDir, updatableFontMap,
allowNonExistingFile);
if (family == null) {
continue;
}
String name = family.getName();
if (name == null || !oemNamedFamilies.containsKey(name)) {
// The OEM customization overrides system named family. Skip if OEM
// customization XML defines the same named family.
families.add(family);
}
} else if (tag.equals("alias")) {
aliases.add(readAlias(parser));
} else {
skip(parser);
}
}
families.addAll(oemNamedFamilies.values());
return new FontConfig(families, aliases, lastModifiedDate, configVersion);
}
private static boolean keepReading(XmlPullParser parser)
throws XmlPullParserException, IOException {
int next = parser.next();
return next != XmlPullParser.END_TAG && next != XmlPullParser.END_DOCUMENT;
}
/**
* Read family tag in fonts.xml or oem_customization.xml
*
* @param parser An XML parser.
* @param fontDir a font directory name.
* @param updatableFontMap a updated font file map.
* @param allowNonExistingFile true to allow font file that doesn't exists
* @return a FontFamily instance. null if no font files are available in this FontFamily.
*/
public static @Nullable FontConfig.FontFamily readFamily(XmlPullParser parser, String fontDir,
@Nullable Map<String, File> updatableFontMap, boolean allowNonExistingFile)
throws XmlPullParserException, IOException {
final String name = parser.getAttributeValue(null, "name");
final String lang = parser.getAttributeValue("", "lang");
final String variant = parser.getAttributeValue(null, "variant");
final List<FontConfig.Font> fonts = new ArrayList<>();
while (keepReading(parser)) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
final String tag = parser.getName();
if (tag.equals(TAG_FONT)) {
FontConfig.Font font = readFont(parser, fontDir, updatableFontMap,
allowNonExistingFile);
if (font != null) {
fonts.add(font);
}
} else {
skip(parser);
}
}
int intVariant = FontConfig.FontFamily.VARIANT_DEFAULT;
if (variant != null) {
if (variant.equals(VARIANT_COMPACT)) {
intVariant = FontConfig.FontFamily.VARIANT_COMPACT;
} else if (variant.equals(VARIANT_ELEGANT)) {
intVariant = FontConfig.FontFamily.VARIANT_ELEGANT;
}
}
if (fonts.isEmpty()) {
return null;
}
return new FontConfig.FontFamily(fonts, name, LocaleList.forLanguageTags(lang), intVariant);
}
/** Matches leading and trailing XML whitespace. */
private static final Pattern FILENAME_WHITESPACE_PATTERN =
Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
private static @Nullable FontConfig.Font readFont(
@NonNull XmlPullParser parser,
@NonNull String fontDir,
@Nullable Map<String, File> updatableFontMap,
boolean allowNonExistingFile)
throws XmlPullParserException, IOException {
String indexStr = parser.getAttributeValue(null, ATTR_INDEX);
int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
List<FontVariationAxis> axes = new ArrayList<>();
String weightStr = parser.getAttributeValue(null, ATTR_WEIGHT);
int weight = weightStr == null ? FontStyle.FONT_WEIGHT_NORMAL : Integer.parseInt(weightStr);
boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE));
String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR);
String postScriptName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME);
StringBuilder filename = new StringBuilder();
while (keepReading(parser)) {
if (parser.getEventType() == XmlPullParser.TEXT) {
filename.append(parser.getText());
}
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
String tag = parser.getName();
if (tag.equals(TAG_AXIS)) {
axes.add(readAxis(parser));
} else {
skip(parser);
}
}
String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
if (postScriptName == null) {
// If post script name was not provided, assume the file name is same to PostScript
// name.
postScriptName = sanitizedName.substring(0, sanitizedName.length() - 4);
}
String updatedName = findUpdatedFontFile(postScriptName, updatableFontMap);
String filePath;
String originalPath;
if (updatedName != null) {
filePath = updatedName;
originalPath = fontDir + sanitizedName;
} else {
filePath = fontDir + sanitizedName;
originalPath = null;
}
String varSettings;
if (axes.isEmpty()) {
varSettings = "";
} else {
varSettings = FontVariationAxis.toFontVariationSettings(
axes.toArray(new FontVariationAxis[0]));
}
File file = new File(filePath);
if (!(allowNonExistingFile || file.isFile())) {
return null;
}
return new FontConfig.Font(file,
originalPath == null ? null : new File(originalPath),
postScriptName,
new FontStyle(
weight,
isItalic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT
),
index,
varSettings,
fallbackFor);
}
private static String findUpdatedFontFile(String psName,
@Nullable Map<String, File> updatableFontMap) {
if (updatableFontMap != null) {
File updatedFile = updatableFontMap.get(psName);
if (updatedFile != null) {
return updatedFile.getAbsolutePath();
}
}
return null;
}
private static FontVariationAxis readAxis(XmlPullParser parser)
throws XmlPullParserException, IOException {
String tagStr = parser.getAttributeValue(null, ATTR_TAG);
String styleValueStr = parser.getAttributeValue(null, ATTR_STYLEVALUE);
skip(parser); // axis tag is empty, ignore any contents and consume end tag
return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr));
}
/**
* Reads alias elements
*/
public static FontConfig.Alias readAlias(XmlPullParser parser)
throws XmlPullParserException, IOException {
String name = parser.getAttributeValue(null, "name");
String toName = parser.getAttributeValue(null, "to");
String weightStr = parser.getAttributeValue(null, "weight");
int weight;
if (weightStr == null) {
weight = FontStyle.FONT_WEIGHT_NORMAL;
} else {
weight = Integer.parseInt(weightStr);
}
skip(parser); // alias tag is empty, ignore any contents and consume end tag
return new FontConfig.Alias(name, toName, weight);
}
/**
* Skip until next element
*/
public static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
int depth = 1;
while (depth > 0) {
switch (parser.next()) {
case XmlPullParser.START_TAG:
depth++;
break;
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.END_DOCUMENT:
return;
}
}
}
}