Rewrite system fallback construction with new FontFamily
Bug: 111133573
Bug: 112196940
Test: atest CtsWidgetTestCases:EditTextTest
CtsWidgetTestCases:TextViewFadingEdgeTest
FrameworksCoreTests:TextViewFallbackLineSpacingTest
FrameworksCoreTests:TextViewTest FrameworksCoreTests:TypefaceTest
CtsGraphicsTestCases:TypefaceTest CtsWidgetTestCases:TextViewTest
CtsTextTestCases FrameworksCoreTests:android.text
CtsWidgetTestCases:TextViewPrecomputedTextTest
CtsGraphicsTestCases:android.graphics.fonts
Change-Id: I69e21940eaec4427f62abe59ae003b4f60855f22
diff --git a/core/jni/android/graphics/fonts/FontFamily.cpp b/core/jni/android/graphics/fonts/FontFamily.cpp
index 4597386..767e068 100644
--- a/core/jni/android/graphics/fonts/FontFamily.cpp
+++ b/core/jni/android/graphics/fonts/FontFamily.cpp
@@ -17,11 +17,13 @@
#define LOG_TAG "Minikin"
#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedUtfChars.h>
#include <core_jni_helpers.h>
#include "FontUtils.h"
#include <minikin/FontFamily.h>
+#include <minikin/LocaleList.h>
#include <memory>
@@ -54,10 +56,18 @@
}
// Regular JNI
-static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr) {
+static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr,
+ jstring langTags, jint variant) {
std::unique_ptr<NativeFamilyBuilder> builder(toBuilder(builderPtr));
- std::shared_ptr<minikin::FontFamily> family =
- std::make_shared<minikin::FontFamily>(std::move(builder->fonts));
+ uint32_t localeId;
+ if (langTags == nullptr) {
+ localeId = minikin::registerLocaleList("");
+ } else {
+ ScopedUtfChars str(env, langTags);
+ localeId = minikin::registerLocaleList(str.c_str());
+ }
+ std::shared_ptr<minikin::FontFamily> family = std::make_shared<minikin::FontFamily>(
+ localeId, static_cast<minikin::FamilyVariant>(variant), std::move(builder->fonts));
if (family->getCoverage().length() == 0) {
// No coverage means minikin rejected given font for some reasons.
jniThrowException(env, "java/lang/IllegalArgumentException",
@@ -77,7 +87,7 @@
static const JNINativeMethod gFontFamilyBuilderMethods[] = {
{ "nInitBuilder", "()J", (void*) FontFamily_Builder_initBuilder },
{ "nAddFont", "(JJ)V", (void*) FontFamily_Builder_addFont },
- { "nBuild", "(J)J", (void*) FontFamily_Builder_build },
+ { "nBuild", "(JLjava/lang/String;I)J", (void*) FontFamily_Builder_build },
{ "nGetReleaseNativeFamily", "()J", (void*) FontFamily_Builder_GetReleaseFunc },
};
diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
index eaabdc8..dd34f1f 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
@@ -23,9 +23,12 @@
import android.content.Context;
import android.content.res.AssetManager;
+import android.graphics.fonts.FontFamily;
+import android.graphics.fonts.SystemFonts;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.text.FontConfig;
import android.util.ArrayMap;
import org.junit.After;
@@ -112,7 +115,9 @@
} catch (IOException e) {
throw new RuntimeException(e);
}
- Typeface.buildSystemFallback(TEST_FONTS_XML, TEST_FONT_DIR, fontMap, fallbackMap);
+ final FontConfig.Alias[] aliases = SystemFonts.buildSystemFallback(TEST_FONTS_XML,
+ TEST_FONT_DIR, fallbackMap);
+ Typeface.initSystemDefaultTypefaces(fontMap, fallbackMap, aliases);
}
@Test
@@ -120,10 +125,14 @@
final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();
- Typeface.buildSystemFallback(SYSTEM_FONTS_XML, SYSTEM_FONT_DIR, fontMap, fallbackMap);
+ final FontConfig.Alias[] aliases = SystemFonts.buildSystemFallback(SYSTEM_FONTS_XML,
+ SYSTEM_FONT_DIR, fallbackMap);
- assertFalse(fontMap.isEmpty());
+ assertNotNull(aliases);
assertFalse(fallbackMap.isEmpty());
+
+ Typeface.initSystemDefaultTypefaces(fontMap, fallbackMap, aliases);
+ assertFalse(fontMap.isEmpty());
}
@Test
diff --git a/core/tests/coretests/src/android/text/FontFallbackSetup.java b/core/tests/coretests/src/android/text/FontFallbackSetup.java
index ced74ee..6c34043 100644
--- a/core/tests/coretests/src/android/text/FontFallbackSetup.java
+++ b/core/tests/coretests/src/android/text/FontFallbackSetup.java
@@ -19,8 +19,9 @@
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.AssetManager;
-import android.graphics.FontFamily;
import android.graphics.Typeface;
+import android.graphics.fonts.FontFamily;
+import android.graphics.fonts.SystemFonts;
import android.support.test.InstrumentationRegistry;
import android.util.ArrayMap;
@@ -73,7 +74,9 @@
}
final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();
- Typeface.buildSystemFallback(testFontsXml, mTestFontsDir, mFontMap, fallbackMap);
+ final FontConfig.Alias[] aliases = SystemFonts.buildSystemFallback(testFontsXml,
+ mTestFontsDir, fallbackMap);
+ Typeface.initSystemDefaultTypefaces(mFontMap, fallbackMap, aliases);
}
@NonNull
diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java
index 522d7a5..7fa7484 100644
--- a/graphics/java/android/graphics/Typeface.java
+++ b/graphics/java/android/graphics/Typeface.java
@@ -28,13 +28,13 @@
import android.annotation.UnsupportedAppUsage;
import android.content.res.AssetManager;
import android.graphics.fonts.FontVariationAxis;
+import android.graphics.fonts.SystemFonts;
import android.net.Uri;
import android.provider.FontRequest;
import android.provider.FontsContract;
import android.text.FontConfig;
import android.util.ArrayMap;
import android.util.Base64;
-import android.util.Log;
import android.util.LongSparseArray;
import android.util.LruCache;
import android.util.SparseArray;
@@ -47,12 +47,9 @@
import libcore.util.NativeAllocationRegistry;
-import org.xmlpull.v1.XmlPullParserException;
-
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
@@ -121,10 +118,14 @@
private static final Object sDynamicCacheLock = new Object();
static Typeface sDefaultTypeface;
+
+ // Following two fields are not used but left for hiddenapi private list
@UnsupportedAppUsage
static final Map<String, Typeface> sSystemFontMap;
+
+ // We cannot support sSystemFallbackMap since we will migrate to public FontFamily API.
@UnsupportedAppUsage
- static final Map<String, FontFamily[]> sSystemFallbackMap;
+ static final Map<String, FontFamily[]> sSystemFallbackMap = Collections.emptyMap();
/**
* @hide
@@ -566,11 +567,7 @@
return null;
}
- Typeface base = sSystemFontMap.get(mFallbackFamilyName);
- if (base == null) {
- base = sDefaultTypeface;
- }
-
+ final Typeface base = getSystemDefaultTypeface(mFallbackFamilyName);
if (mWeight == RESOLVE_BY_FONT_TABLE && mItalic == RESOLVE_BY_FONT_TABLE) {
return base;
}
@@ -687,7 +684,7 @@
* @return The best matching typeface.
*/
public static Typeface create(String familyName, @Style int style) {
- return create(sSystemFontMap.get(familyName), style);
+ return create(getSystemDefaultTypeface(familyName), style);
}
/**
@@ -897,7 +894,9 @@
* Create a new typeface from an array of font families.
*
* @param families array of font families
+ * @deprecated
*/
+ @Deprecated
@UnsupportedAppUsage
private static Typeface createFromFamilies(FontFamily[] families) {
long[] ptrArray = new long[families.length];
@@ -909,6 +908,21 @@
}
/**
+ * Create a new typeface from an array of android.graphics.fonts.FontFamily.
+ *
+ * @param families array of font families
+ */
+ private static Typeface createFromFamilies(
+ @Nullable android.graphics.fonts.FontFamily[] families) {
+ final long[] ptrArray = new long[families.length];
+ for (int i = 0; i < families.length; ++i) {
+ ptrArray[i] = families[i].getNativePtr();
+ }
+ return new Typeface(nativeCreateFromArray(ptrArray,
+ RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+ }
+
+ /**
* This method is used by supportlib-v27.
* TODO: Remove private API use in supportlib: http://b/72665240
*/
@@ -934,16 +948,13 @@
@UnsupportedAppUsage
private static Typeface createFromFamiliesWithDefault(FontFamily[] families,
String fallbackName, int weight, int italic) {
- FontFamily[] fallback = sSystemFallbackMap.get(fallbackName);
- if (fallback == null) {
- fallback = sSystemFallbackMap.get(DEFAULT_FAMILY);
- }
+ android.graphics.fonts.FontFamily[] fallback = SystemFonts.getSystemFallback(fallbackName);
long[] ptrArray = new long[families.length + fallback.length];
for (int i = 0; i < families.length; i++) {
ptrArray[i] = families[i].mNativePtr;
}
for (int i = 0; i < fallback.length; i++) {
- ptrArray[i + families.length] = fallback[i].mNativePtr;
+ ptrArray[i + families.length] = fallback[i].getNativePtr();
}
return new Typeface(nativeCreateFromArray(ptrArray, weight, italic));
}
@@ -961,191 +972,41 @@
mWeight = nativeGetWeight(ni);
}
- private static @Nullable ByteBuffer mmap(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 Typeface getSystemDefaultTypeface(@NonNull String familyName) {
+ Typeface tf = sSystemFontMap.get(familyName);
+ return tf == null ? Typeface.DEFAULT : tf;
}
- private static @Nullable FontFamily createFontFamily(
- String familyName, List<FontConfig.Font> fonts, String[] languageTags, int variant,
- Map<String, ByteBuffer> cache, String fontDir) {
- final FontFamily family = new FontFamily(languageTags, variant);
- for (int i = 0; i < fonts.size(); i++) {
- final FontConfig.Font font = fonts.get(i);
- final String fullPath = fontDir + font.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;
- }
- }
- if (!family.addFontFromBuffer(buffer, font.getTtcIndex(), font.getAxes(),
- font.getWeight(), font.isItalic() ? STYLE_ITALIC : STYLE_NORMAL)) {
- Log.e(TAG, "Error creating font " + fullPath + "#" + font.getTtcIndex());
- }
- }
- if (!family.freeze()) {
- Log.e(TAG, "Unable to load Family: " + familyName + " : "
- + Arrays.toString(languageTags));
- return null;
- }
- return family;
- }
-
- private static void pushFamilyToFallback(FontConfig.Family xmlFamily,
- ArrayMap<String, ArrayList<FontFamily>> fallbackMap,
- Map<String, ByteBuffer> cache,
- String fontDir) {
-
- 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, fontDir);
-
- // 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, fontDir);
- 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.
- }
- }
- }
- }
-
- /**
- * 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 fontMap An output system font map. Caller must pass empty map.
- * @param fallbackMap An output system fallback map. Caller must pass empty map.
- * @hide
- */
+ /** @hide */
@VisibleForTesting
+ public static void initSystemDefaultTypefaces(Map<String, Typeface> systemFontMap,
+ Map<String, android.graphics.fonts.FontFamily[]> fallbacks,
+ FontConfig.Alias[] aliases) {
+ for (Map.Entry<String, android.graphics.fonts.FontFamily[]> entry : fallbacks.entrySet()) {
+ systemFontMap.put(entry.getKey(), createFromFamilies(entry.getValue()));
+ }
+
+ for (FontConfig.Alias alias : aliases) {
+ final Typeface base = systemFontMap.get(alias.getToName());
+ final int weight = alias.getWeight();
+ final Typeface newFace = weight == 400 ? base :
+ new Typeface(nativeCreateWeightAlias(base.native_instance, weight));
+ systemFontMap.put(alias.getName(), newFace);
+ }
+ }
+
+ // Following methods are left for layoutlib
+ // TODO: Remove once layoutlib stop calling buildSystemFallback
+ /** @hide */
public static void buildSystemFallback(String xmlPath, String fontDir,
ArrayMap<String, Typeface> fontMap, ArrayMap<String, FontFamily[]> fallbackMap) {
- try {
- final FileInputStream fontsIn = new FileInputStream(xmlPath);
- final FontConfig fontConfig = FontListParser.parse(fontsIn);
-
- 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;
- }
- final FontFamily family = createFontFamily(
- xmlFamily.getName(), Arrays.asList(xmlFamily.getFonts()),
- xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, fontDir);
- if (family == null) {
- continue;
- }
- final ArrayList<FontFamily> fallback = new ArrayList<>();
- fallback.add(family);
- fallbackListMap.put(familyName, fallback);
- }
-
- // 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, fontDir);
- }
- }
-
- // 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 long[] ptrArray = new long[families.length];
- for (int j = 0; j < families.length; j++) {
- ptrArray[j] = families[j].mNativePtr;
- }
- fontMap.put(fallbackName, new Typeface(nativeCreateFromArray(
- ptrArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)));
- }
-
- // Insert alias to font maps.
- for (final FontConfig.Alias alias : fontConfig.getAliases()) {
- Typeface base = fontMap.get(alias.getToName());
- Typeface newFace = base;
- int weight = alias.getWeight();
- if (weight != 400) {
- newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));
- }
- fontMap.put(alias.getName(), newFace);
- }
- } catch (RuntimeException e) {
- Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);
- // TODO: normal in non-Minikin case, remove or make error when Minikin-only
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Error opening " + xmlPath, e);
- } catch (IOException e) {
- Log.e(TAG, "Error reading " + xmlPath, e);
- } catch (XmlPullParserException e) {
- Log.e(TAG, "XML parse exception for " + xmlPath, e);
- }
}
static {
- final ArrayMap<String, Typeface> systemFontMap = new ArrayMap<>();
- final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>();
- buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", systemFontMap,
- systemFallbackMap);
+ final HashMap<String, Typeface> systemFontMap = new HashMap<>();
+ initSystemDefaultTypefaces(systemFontMap, SystemFonts.getRawSystemFallbackMap(),
+ SystemFonts.getAliases());
sSystemFontMap = Collections.unmodifiableMap(systemFontMap);
- sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap);
setDefault(sSystemFontMap.get(DEFAULT_FAMILY));
diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java
index 74b58ea..dc213ea 100644
--- a/graphics/java/android/graphics/fonts/FontFamily.java
+++ b/graphics/java/android/graphics/fonts/FontFamily.java
@@ -18,6 +18,9 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.FontConfig;
+import android.text.TextUtils;
import com.android.internal.util.Preconditions;
@@ -107,11 +110,25 @@
* @return a font family
*/
public @NonNull FontFamily build() {
+ return build(null, FontConfig.Family.VARIANT_DEFAULT);
+ }
+
+ /** @hide */
+ public @NonNull FontFamily build(@Nullable String[] langTags, int variant) {
final long builderPtr = nInitBuilder();
for (int i = 0; i < mFonts.size(); ++i) {
nAddFont(builderPtr, mFonts.get(i).getNativePtr());
}
- final long ptr = nBuild(builderPtr);
+ final String langString;
+ if (langTags == null || langTags.length == 0) {
+ langString = null;
+ } else if (langTags.length == 1) {
+ langString = langTags[0];
+ } else {
+ langString = TextUtils.join(",", langTags);
+ }
+
+ final long ptr = nBuild(builderPtr, langString, variant);
final FontFamily family = new FontFamily(mFonts, ptr);
sFamilyRegistory.registerNativeAllocation(family, ptr);
return family;
@@ -124,7 +141,7 @@
private static native long nInitBuilder();
@CriticalNative
private static native void nAddFont(long builderPtr, long fontPtr);
- private static native long nBuild(long builderPtr);
+ private static native long nBuild(long builderPtr, String langTags, int variant);
@CriticalNative
private static native long nGetReleaseNativeFamily();
}
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
new file mode 100644
index 0000000..bd49130
--- /dev/null
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -0,0 +1,260 @@
+/*
+ * 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 org.xmlpull.v1.XmlPullParserException;
+
+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.List;
+import java.util.Map;
+
+/**
+ * Provides the system font configurations.
+ * @hide
+ */
+public class SystemFonts {
+ private static final String TAG = "SystemFonts";
+ private static final String DEFAULT_FAMILY = "sans-serif";
+
+ private SystemFonts() {} // Do not instansiate.
+
+ static final Map<String, FontFamily[]> sSystemFallbackMap;
+ static final FontConfig.Alias[] sAliases;
+
+ /**
+ * 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"
+ */
+ 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.
+ */
+ 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.
+ */
+ 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 String fontDir) {
+
+ 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, fontDir);
+
+ // 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, fontDir);
+ 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 String fontDir) {
+ 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 = fontDir + 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 = new Font.Builder(buffer)
+ .setWeight(fontConfig.getWeight())
+ .setItalic(fontConfig.isItalic())
+ .setTtcIndex(fontConfig.getTtcIndex())
+ .setFontVariationSettings(fontConfig.getAxes())
+ .build();
+
+ if (b == null) {
+ b = new FontFamily.Builder(font);
+ } else {
+ b.addFont(font);
+ }
+ }
+ return b == null ? null : b.build(languageTags, variant);
+ }
+
+ /**
+ * 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 ArrayMap<String, FontFamily[]> fallbackMap) {
+ try {
+ final FileInputStream fontsIn = new FileInputStream(xmlPath);
+ final FontConfig fontConfig = FontListParser.parse(fontsIn);
+
+ 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;
+ }
+ final FontFamily family = createFontFamily(
+ xmlFamily.getName(), Arrays.asList(xmlFamily.getFonts()),
+ xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, fontDir);
+ if (family == null) {
+ continue;
+ }
+ final ArrayList<FontFamily> fallback = new ArrayList<>();
+ fallback.add(family);
+ fallbackListMap.put(familyName, fallback);
+ }
+
+ // 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, fontDir);
+ }
+ }
+
+ // 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);
+ }
+
+ return fontConfig.getAliases();
+ } catch (IOException | XmlPullParserException e) {
+ Log.e(TAG, "Failed initialize system fallbacks.", e);
+ return null;
+ }
+ }
+
+ static {
+ final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>();
+ sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
+ systemFallbackMap);
+ sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap);
+ }
+
+}