blob: 4a9cf14d04a5cae8ca9060cd8fac09b3b0bd69dd [file] [log] [blame]
/*
* Copyright 2018 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.fonts;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.FontListParser;
import android.text.FontConfig;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Provides the system font configurations.
*/
public final class SystemFonts {
private static final String TAG = "SystemFonts";
private static final String DEFAULT_FAMILY = "sans-serif";
private SystemFonts() {} // Do not instansiate.
private static final Map<String, FontFamily[]> sSystemFallbackMap;
private static final FontConfig.Alias[] sAliases;
private static final List<Font> sAvailableFonts;
/**
* Returns all available font files in the system.
*
* @return a set of system fonts
*/
public static @NonNull Set<Font> getAvailableFonts() {
HashSet<Font> set = new HashSet<>();
set.addAll(sAvailableFonts);
return set;
}
/**
* Returns fallback list for the given family name.
*
* If no fallback found for the given family name, returns fallback for the default family.
*
* @param familyName family name, e.g. "serif"
* @hide
*/
public static @NonNull FontFamily[] getSystemFallback(@Nullable String familyName) {
final FontFamily[] families = sSystemFallbackMap.get(familyName);
return families == null ? sSystemFallbackMap.get(DEFAULT_FAMILY) : families;
}
/**
* Returns raw system fallback map.
*
* This method is intended to be used only by Typeface static initializer.
* @hide
*/
public static @NonNull Map<String, FontFamily[]> getRawSystemFallbackMap() {
return sSystemFallbackMap;
}
/**
* Returns a list of aliases.
*
* This method is intended to be used only by Typeface static initializer.
* @hide
*/
public static @NonNull FontConfig.Alias[] getAliases() {
return sAliases;
}
private static @Nullable ByteBuffer mmap(@NonNull String fullPath) {
try (FileInputStream file = new FileInputStream(fullPath)) {
final FileChannel fileChannel = file.getChannel();
final long fontSize = fileChannel.size();
return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
} catch (IOException e) {
Log.e(TAG, "Error mapping font file " + fullPath);
return null;
}
}
private static void pushFamilyToFallback(@NonNull FontConfig.Family xmlFamily,
@NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap,
@NonNull Map<String, ByteBuffer> cache,
@NonNull ArrayList<Font> availableFonts) {
final String languageTags = xmlFamily.getLanguages();
final int variant = xmlFamily.getVariant();
final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>();
final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>();
// Collect default fallback and specific fallback fonts.
for (final FontConfig.Font font : xmlFamily.getFonts()) {
final String fallbackName = font.getFallbackFor();
if (fallbackName == null) {
defaultFonts.add(font);
} else {
ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName);
if (fallback == null) {
fallback = new ArrayList<>();
specificFallbackFonts.put(fallbackName, fallback);
}
fallback.add(font);
}
}
final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
xmlFamily.getName(), defaultFonts, languageTags, variant, cache, availableFonts);
// Insert family into fallback map.
for (int i = 0; i < fallbackMap.size(); i++) {
final ArrayList<FontConfig.Font> fallback =
specificFallbackFonts.get(fallbackMap.keyAt(i));
if (fallback == null) {
if (defaultFamily != null) {
fallbackMap.valueAt(i).add(defaultFamily);
}
} else {
final FontFamily family = createFontFamily(
xmlFamily.getName(), fallback, languageTags, variant, cache,
availableFonts);
if (family != null) {
fallbackMap.valueAt(i).add(family);
} else if (defaultFamily != null) {
fallbackMap.valueAt(i).add(defaultFamily);
} else {
// There is no valid for for default fallback. Ignore.
}
}
}
}
private static @Nullable FontFamily createFontFamily(@NonNull String familyName,
@NonNull List<FontConfig.Font> fonts,
@NonNull String languageTags,
@FontConfig.Family.Variant int variant,
@NonNull Map<String, ByteBuffer> cache,
@NonNull ArrayList<Font> availableFonts) {
if (fonts.size() == 0) {
return null;
}
FontFamily.Builder b = null;
for (int i = 0; i < fonts.size(); i++) {
final FontConfig.Font fontConfig = fonts.get(i);
final String fullPath = fontConfig.getFontName();
ByteBuffer buffer = cache.get(fullPath);
if (buffer == null) {
if (cache.containsKey(fullPath)) {
continue; // Already failed to mmap. Skip it.
}
buffer = mmap(fullPath);
cache.put(fullPath, buffer);
if (buffer == null) {
continue;
}
}
final Font font;
try {
font = new Font.Builder(buffer, new File(fullPath), languageTags)
.setWeight(fontConfig.getWeight())
.setSlant(fontConfig.isItalic() ? FontStyle.FONT_SLANT_ITALIC
: FontStyle.FONT_SLANT_UPRIGHT)
.setTtcIndex(fontConfig.getTtcIndex())
.setFontVariationSettings(fontConfig.getAxes())
.build();
} catch (IOException e) {
throw new RuntimeException(e); // Never reaches here
}
availableFonts.add(font);
if (b == null) {
b = new FontFamily.Builder(font);
} else {
b.addFont(font);
}
}
return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */);
}
private static void appendNamedFamily(@NonNull FontConfig.Family xmlFamily,
@NonNull HashMap<String, ByteBuffer> bufferCache,
@NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap,
@NonNull ArrayList<Font> availableFonts) {
final String familyName = xmlFamily.getName();
final FontFamily family = createFontFamily(
familyName, Arrays.asList(xmlFamily.getFonts()),
xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, availableFonts);
if (family == null) {
return;
}
final ArrayList<FontFamily> fallback = new ArrayList<>();
fallback.add(family);
fallbackListMap.put(familyName, fallback);
}
/**
* Build the system fallback from xml file.
*
* @param xmlPath A full path string to the fonts.xml file.
* @param fontDir A full path string to the system font directory. This must end with
* slash('/').
* @param fallbackMap An output system fallback map. Caller must pass empty map.
* @return a list of aliases
* @hide
*/
@VisibleForTesting
public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
@NonNull String fontDir,
@NonNull FontCustomizationParser.Result oemCustomization,
@NonNull ArrayMap<String, FontFamily[]> fallbackMap,
@NonNull ArrayList<Font> availableFonts) {
try {
final FileInputStream fontsIn = new FileInputStream(xmlPath);
final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir);
final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>();
// First traverse families which have a 'name' attribute to create fallback map.
for (final FontConfig.Family xmlFamily : xmlFamilies) {
final String familyName = xmlFamily.getName();
if (familyName == null) {
continue;
}
appendNamedFamily(xmlFamily, bufferCache, fallbackListMap, availableFonts);
}
for (int i = 0; i < oemCustomization.mAdditionalNamedFamilies.size(); ++i) {
appendNamedFamily(oemCustomization.mAdditionalNamedFamilies.get(i),
bufferCache, fallbackListMap, availableFonts);
}
// Then, add fallback fonts to the each fallback map.
for (int i = 0; i < xmlFamilies.length; i++) {
final FontConfig.Family xmlFamily = xmlFamilies[i];
// The first family (usually the sans-serif family) is always placed immediately
// after the primary family in the fallback.
if (i == 0 || xmlFamily.getName() == null) {
pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, availableFonts);
}
}
// Build the font map and fallback map.
for (int i = 0; i < fallbackListMap.size(); i++) {
final String fallbackName = fallbackListMap.keyAt(i);
final List<FontFamily> familyList = fallbackListMap.valueAt(i);
final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]);
fallbackMap.put(fallbackName, families);
}
final ArrayList<FontConfig.Alias> list = new ArrayList<>();
list.addAll(Arrays.asList(fontConfig.getAliases()));
list.addAll(oemCustomization.mAdditionalAliases);
return list.toArray(new FontConfig.Alias[list.size()]);
} catch (IOException | XmlPullParserException e) {
Log.e(TAG, "Failed initialize system fallbacks.", e);
return ArrayUtils.emptyArray(FontConfig.Alias.class);
}
}
private static FontCustomizationParser.Result readFontCustomization(
@NonNull String customizeXml, @NonNull String customFontsDir) {
try (FileInputStream f = new FileInputStream(customizeXml)) {
return FontCustomizationParser.parse(f, customFontsDir);
} catch (IOException e) {
return new FontCustomizationParser.Result();
} catch (XmlPullParserException e) {
Log.e(TAG, "Failed to parse font customization XML", e);
return new FontCustomizationParser.Result();
}
}
static {
final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>();
final ArrayList<Font> availableFonts = new ArrayList<>();
final FontCustomizationParser.Result oemCustomization =
readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
oemCustomization, systemFallbackMap, availableFonts);
sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap);
sAvailableFonts = Collections.unmodifiableList(availableFonts);
}
}