/* * 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 static android.text.FontConfig.Customization.LocaleFallback.OPERATION_APPEND; import static android.text.FontConfig.Customization.LocaleFallback.OPERATION_PREPEND; import static android.text.FontConfig.Customization.LocaleFallback.OPERATION_REPLACE; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.FontListParser; import android.graphics.Typeface; import android.os.LocaleList; import android.text.FontConfig; import android.util.ArrayMap; import android.util.Log; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; 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.Collections; import java.util.List; import java.util.Locale; 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 FONTS_XML = "/system/etc/font_fallback.xml"; private static final String LEGACY_FONTS_XML = "/system/etc/fonts.xml"; /** @hide */ public static final String SYSTEM_FONT_DIR = "/system/fonts/"; private static final String OEM_XML = "/product/etc/fonts_customization.xml"; /** @hide */ public static final String OEM_FONT_DIR = "/product/fonts/"; private SystemFonts() {} // Do not instansiate. private static final Object LOCK = new Object(); private static @GuardedBy("sLock") Set sAvailableFonts; /** * Returns all available font files in the system. * * @return a set of system fonts */ public static @NonNull Set getAvailableFonts() { synchronized (LOCK) { if (sAvailableFonts == null) { sAvailableFonts = Font.getAvailableFonts(); } return sAvailableFonts; } } /** * @hide */ public static void resetAvailableFonts() { synchronized (LOCK) { sAvailableFonts = null; } } 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) { return null; } } /** @hide */ @VisibleForTesting public static @FontFamily.Builder.VariableFontFamilyType int resolveVarFamilyType( @NonNull FontConfig.FontFamily xmlFamily, @Nullable String familyName) { int wghtCount = 0; int italCount = 0; int targetFonts = 0; boolean hasItalicFont = false; List fonts = xmlFamily.getFontList(); for (int i = 0; i < fonts.size(); ++i) { FontConfig.Font font = fonts.get(i); if (familyName == null) { // for default family if (font.getFontFamilyName() != null) { continue; // this font is not for the default family. } } else { // for the specific family if (!familyName.equals(font.getFontFamilyName())) { continue; // this font is not for given family. } } final int varTypeAxes = font.getVarTypeAxes(); if (varTypeAxes == 0) { // If we see static font, we can immediately return as VAR_TYPE_NONE. return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE; } if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_WGHT) != 0) { wghtCount++; } if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_ITAL) != 0) { italCount++; } if (font.getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC) { hasItalicFont = true; } targetFonts++; } if (italCount == 0) { // No ital font. if (targetFonts == 1 && wghtCount == 1) { // If there is only single font that has wght, use it for regular style and // use synthetic bolding for italic. return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY; } else if (targetFonts == 2 && wghtCount == 2 && hasItalicFont) { // If there are two fonts and italic font is available, use them for regular and // italic separately. (It is impossible to have two italic fonts. It will end up // with Typeface creation failure.) return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT; } } else if (italCount == 1) { // If ital font is included, a single font should support both wght and ital. if (wghtCount == 1 && targetFonts == 1) { return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL; } } // Otherwise, unsupported. return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE; } private static void pushFamilyToFallback(@NonNull FontConfig.FontFamily xmlFamily, @NonNull ArrayMap fallbackMap, @NonNull Map cache) { final String languageTags = xmlFamily.getLocaleList().toLanguageTags(); final int variant = xmlFamily.getVariant(); final ArrayList defaultFonts = new ArrayList<>(); final ArrayMap> specificFallbackFonts = new ArrayMap<>(); // Collect default fallback and specific fallback fonts. for (final FontConfig.Font font : xmlFamily.getFonts()) { final String fallbackName = font.getFontFamilyName(); if (fallbackName == null) { defaultFonts.add(font); } else { ArrayList fallback = specificFallbackFonts.get(fallbackName); if (fallback == null) { fallback = new ArrayList<>(); specificFallbackFonts.put(fallbackName, fallback); } fallback.add(font); } } final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( defaultFonts, languageTags, variant, resolveVarFamilyType(xmlFamily, null), false, cache); // Insert family into fallback map. for (int i = 0; i < fallbackMap.size(); i++) { final String name = fallbackMap.keyAt(i); final NativeFamilyListSet familyListSet = fallbackMap.valueAt(i); int identityHash = System.identityHashCode(xmlFamily); if (familyListSet.seenXmlFamilies.get(identityHash, -1) != -1) { continue; } else { familyListSet.seenXmlFamilies.append(identityHash, 1); } final ArrayList fallback = specificFallbackFonts.get(name); if (fallback == null) { if (defaultFamily != null) { familyListSet.familyList.add(defaultFamily); } } else { final FontFamily family = createFontFamily(fallback, languageTags, variant, resolveVarFamilyType(xmlFamily, name), false, cache); if (family != null) { familyListSet.familyList.add(family); } else if (defaultFamily != null) { familyListSet.familyList.add(defaultFamily); } else { // There is no valid for default fallback. Ignore. } } } } private static @Nullable FontFamily createFontFamily( @NonNull List fonts, @NonNull String languageTags, @FontConfig.FontFamily.Variant int variant, int varFamilyType, boolean isDefaultFallback, @NonNull Map cache) { 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.getFile().getAbsolutePath(); 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.getStyle().getWeight()) .setSlant(fontConfig.getStyle().getSlant()) .setTtcIndex(fontConfig.getTtcIndex()) .setFontVariationSettings(fontConfig.getFontVariationSettings()) .build(); } catch (IOException e) { throw new RuntimeException(e); // Never reaches here } if (b == null) { b = new FontFamily.Builder(font); } else { b.addFont(font); } } return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */, isDefaultFallback, varFamilyType); } private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList namedFamilyList, @NonNull ArrayMap bufferCache, @NonNull ArrayMap fallbackListMap) { final String familyName = namedFamilyList.getName(); final NativeFamilyListSet familyListSet = new NativeFamilyListSet(); final List xmlFamilies = namedFamilyList.getFamilies(); for (int i = 0; i < xmlFamilies.size(); ++i) { FontConfig.FontFamily xmlFamily = xmlFamilies.get(i); final FontFamily family = createFontFamily( xmlFamily.getFontList(), xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(), resolveVarFamilyType(xmlFamily, null /* all fonts under named family should be treated as default */), true, // named family is always default bufferCache); if (family == null) { return; } familyListSet.familyList.add(family); familyListSet.seenXmlFamilies.append(System.identityHashCode(xmlFamily), 1); } fallbackListMap.put(familyName, familyListSet); } /** * Get the updated FontConfig. * * @param updatableFontMap a font mapping of updated font files. * @hide */ public static @NonNull FontConfig getSystemFontConfig( @Nullable Map updatableFontMap, long lastModifiedDate, int configVersion ) { final String fontsXml; if (com.android.text.flags.Flags.newFontsFallbackXml()) { fontsXml = FONTS_XML; } else { fontsXml = LEGACY_FONTS_XML; } return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, updatableFontMap, lastModifiedDate, configVersion); } /** * Get the updated FontConfig. * * @param updatableFontMap a font mapping of updated font files. * @hide */ public static @NonNull FontConfig getSystemFontConfigForTesting( @NonNull String fontsXml, @Nullable Map updatableFontMap, long lastModifiedDate, int configVersion ) { return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, updatableFontMap, lastModifiedDate, configVersion); } /** * Get the system preinstalled FontConfig. * @hide */ public static @NonNull FontConfig getSystemPreinstalledFontConfig() { final String fontsXml; if (com.android.text.flags.Flags.newFontsFallbackXml()) { fontsXml = FONTS_XML; } else { fontsXml = LEGACY_FONTS_XML; } return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null, 0, 0); } /** * @hide */ public static @NonNull FontConfig getSystemPreinstalledFontConfigFromLegacyXml() { return getSystemFontConfigInternal(LEGACY_FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null, 0, 0); } /* package */ static @NonNull FontConfig getSystemFontConfigInternal( @NonNull String fontsXml, @NonNull String systemFontDir, @Nullable String oemXml, @Nullable String productFontDir, @Nullable Map updatableFontMap, long lastModifiedDate, int configVersion ) { try { Log.i(TAG, "Loading font config from " + fontsXml); return FontListParser.parse(fontsXml, systemFontDir, oemXml, productFontDir, updatableFontMap, lastModifiedDate, configVersion); } catch (IOException e) { Log.e(TAG, "Failed to open/read system font configurations.", e); return new FontConfig(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), 0, 0); } catch (XmlPullParserException e) { Log.e(TAG, "Failed to parse the system font configuration.", e); return new FontConfig(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), 0, 0); } } /** * Build the system fallback from FontConfig. * @hide */ @VisibleForTesting public static Map buildSystemFallback(FontConfig fontConfig) { return buildSystemFallback(fontConfig, new ArrayMap<>()); } private static final class NativeFamilyListSet { public List familyList = new ArrayList<>(); public SparseIntArray seenXmlFamilies = new SparseIntArray(); } /** @hide */ @VisibleForTesting public static Map buildSystemFallback(FontConfig fontConfig, ArrayMap outBufferCache) { final ArrayMap fallbackListMap = new ArrayMap<>(); final List localeFallbacks = fontConfig.getLocaleFallbackCustomizations(); final List namedFamilies = fontConfig.getNamedFamilyLists(); for (int i = 0; i < namedFamilies.size(); ++i) { FontConfig.NamedFamilyList namedFamilyList = namedFamilies.get(i); appendNamedFamilyList(namedFamilyList, outBufferCache, fallbackListMap); } // Then, add fallback fonts to the fallback map. final List customizations = new ArrayList<>(); final List xmlFamilies = fontConfig.getFontFamilies(); final SparseIntArray seenCustomization = new SparseIntArray(); for (int i = 0; i < xmlFamilies.size(); i++) { final FontConfig.FontFamily xmlFamily = xmlFamilies.get(i); customizations.clear(); for (int j = 0; j < localeFallbacks.size(); ++j) { if (seenCustomization.get(j, -1) != -1) { continue; // The customization is already applied. } FontConfig.Customization.LocaleFallback localeFallback = localeFallbacks.get(j); if (scriptMatch(xmlFamily.getLocaleList(), localeFallback.getScript())) { customizations.add(localeFallback); seenCustomization.put(j, 1); } } if (customizations.isEmpty()) { pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache); } else { for (int j = 0; j < customizations.size(); ++j) { FontConfig.Customization.LocaleFallback localeFallback = customizations.get(j); if (localeFallback.getOperation() == OPERATION_PREPEND) { pushFamilyToFallback(localeFallback.getFamily(), fallbackListMap, outBufferCache); } } boolean isReplaced = false; for (int j = 0; j < customizations.size(); ++j) { FontConfig.Customization.LocaleFallback localeFallback = customizations.get(j); if (localeFallback.getOperation() == OPERATION_REPLACE) { pushFamilyToFallback(localeFallback.getFamily(), fallbackListMap, outBufferCache); isReplaced = true; } } if (!isReplaced) { // If nothing is replaced, push the original one. pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache); } for (int j = 0; j < customizations.size(); ++j) { FontConfig.Customization.LocaleFallback localeFallback = customizations.get(j); if (localeFallback.getOperation() == OPERATION_APPEND) { pushFamilyToFallback(localeFallback.getFamily(), fallbackListMap, outBufferCache); } } } } // Build the font map and fallback map. final Map fallbackMap = new ArrayMap<>(); for (int i = 0; i < fallbackListMap.size(); i++) { final String fallbackName = fallbackListMap.keyAt(i); final List familyList = fallbackListMap.valueAt(i).familyList; fallbackMap.put(fallbackName, familyList.toArray(new FontFamily[0])); } return fallbackMap; } /** * Build the system Typeface mappings from FontConfig and FallbackMap. * @hide */ @VisibleForTesting public static Map buildSystemTypefaces( FontConfig fontConfig, Map fallbackMap) { final ArrayMap result = new ArrayMap<>(); Typeface.initSystemDefaultTypefaces(fallbackMap, fontConfig.getAliases(), result); return result; } private static boolean scriptMatch(LocaleList localeList, String targetScript) { if (localeList == null || localeList.isEmpty()) { return false; } for (int i = 0; i < localeList.size(); ++i) { Locale locale = localeList.get(i); if (locale == null) { continue; } String baseScript = FontConfig.resolveScript(locale); if (baseScript.equals(targetScript)) { return true; } // Subtag match if (targetScript.equals("Bopo") && baseScript.equals("Hanb")) { // Hanb is Han with Bopomofo. return true; } else if (targetScript.equals("Hani")) { if (baseScript.equals("Hanb") || baseScript.equals("Hans") || baseScript.equals("Hant") || baseScript.equals("Kore") || baseScript.equals("Jpan")) { // Han id suppoted by Taiwanese, Traditional Chinese, Simplified Chinese, Korean // and Japanese. return true; } } else if (targetScript.equals("Hira") || targetScript.equals("Hrkt") || targetScript.equals("Kana")) { if (baseScript.equals("Jpan") || baseScript.equals("Hrkt")) { // Hiragana, Hiragana-Katakana, Katakana is supported by Japanese and // Hiragana-Katakana script. return true; } } } return false; } }