1 /*
2  * Copyright 2018 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.fonts;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.graphics.FontListParser;
22 import android.text.FontConfig;
23 import android.util.ArrayMap;
24 import android.util.Log;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 import com.android.internal.util.ArrayUtils;
28 
29 import org.xmlpull.v1.XmlPullParserException;
30 
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.IOException;
34 import java.nio.ByteBuffer;
35 import java.nio.channels.FileChannel;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Set;
44 
45 /**
46  * Provides the system font configurations.
47  */
48 public final class SystemFonts {
49     private static final String TAG = "SystemFonts";
50     private static final String DEFAULT_FAMILY = "sans-serif";
51 
SystemFonts()52     private SystemFonts() {}  // Do not instansiate.
53 
54     private static final Map<String, FontFamily[]> sSystemFallbackMap;
55     private static final FontConfig.Alias[] sAliases;
56     private static final List<Font> sAvailableFonts;
57 
58     /**
59      * Returns all available font files in the system.
60      *
61      * @return a set of system fonts
62      */
getAvailableFonts()63     public static @NonNull Set<Font> getAvailableFonts() {
64         HashSet<Font> set = new HashSet<>();
65         set.addAll(sAvailableFonts);
66         return set;
67     }
68 
69     /**
70      * Returns fallback list for the given family name.
71      *
72      * If no fallback found for the given family name, returns fallback for the default family.
73      *
74      * @param familyName family name, e.g. "serif"
75      * @hide
76      */
getSystemFallback(@ullable String familyName)77     public static @NonNull FontFamily[] getSystemFallback(@Nullable String familyName) {
78         final FontFamily[] families = sSystemFallbackMap.get(familyName);
79         return families == null ? sSystemFallbackMap.get(DEFAULT_FAMILY) : families;
80     }
81 
82     /**
83      * Returns raw system fallback map.
84      *
85      * This method is intended to be used only by Typeface static initializer.
86      * @hide
87      */
getRawSystemFallbackMap()88     public static @NonNull Map<String, FontFamily[]> getRawSystemFallbackMap() {
89         return sSystemFallbackMap;
90     }
91 
92     /**
93      * Returns a list of aliases.
94      *
95      * This method is intended to be used only by Typeface static initializer.
96      * @hide
97      */
getAliases()98     public static @NonNull FontConfig.Alias[] getAliases() {
99         return sAliases;
100     }
101 
mmap(@onNull String fullPath)102     private static @Nullable ByteBuffer mmap(@NonNull String fullPath) {
103         try (FileInputStream file = new FileInputStream(fullPath)) {
104             final FileChannel fileChannel = file.getChannel();
105             final long fontSize = fileChannel.size();
106             return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
107         } catch (IOException e) {
108             Log.e(TAG, "Error mapping font file " + fullPath);
109             return null;
110         }
111     }
112 
pushFamilyToFallback(@onNull FontConfig.Family xmlFamily, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, @NonNull Map<String, ByteBuffer> cache, @NonNull ArrayList<Font> availableFonts)113     private static void pushFamilyToFallback(@NonNull FontConfig.Family xmlFamily,
114             @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap,
115             @NonNull Map<String, ByteBuffer> cache,
116             @NonNull ArrayList<Font> availableFonts) {
117 
118         final String languageTags = xmlFamily.getLanguages();
119         final int variant = xmlFamily.getVariant();
120 
121         final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>();
122         final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>();
123 
124         // Collect default fallback and specific fallback fonts.
125         for (final FontConfig.Font font : xmlFamily.getFonts()) {
126             final String fallbackName = font.getFallbackFor();
127             if (fallbackName == null) {
128                 defaultFonts.add(font);
129             } else {
130                 ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName);
131                 if (fallback == null) {
132                     fallback = new ArrayList<>();
133                     specificFallbackFonts.put(fallbackName, fallback);
134                 }
135                 fallback.add(font);
136             }
137         }
138 
139         final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
140                 xmlFamily.getName(), defaultFonts, languageTags, variant, cache, availableFonts);
141 
142         // Insert family into fallback map.
143         for (int i = 0; i < fallbackMap.size(); i++) {
144             final ArrayList<FontConfig.Font> fallback =
145                     specificFallbackFonts.get(fallbackMap.keyAt(i));
146             if (fallback == null) {
147                 if (defaultFamily != null) {
148                     fallbackMap.valueAt(i).add(defaultFamily);
149                 }
150             } else {
151                 final FontFamily family = createFontFamily(
152                         xmlFamily.getName(), fallback, languageTags, variant, cache,
153                         availableFonts);
154                 if (family != null) {
155                     fallbackMap.valueAt(i).add(family);
156                 } else if (defaultFamily != null) {
157                     fallbackMap.valueAt(i).add(defaultFamily);
158                 } else {
159                     // There is no valid for for default fallback. Ignore.
160                 }
161             }
162         }
163     }
164 
createFontFamily(@onNull String familyName, @NonNull List<FontConfig.Font> fonts, @NonNull String languageTags, @FontConfig.Family.Variant int variant, @NonNull Map<String, ByteBuffer> cache, @NonNull ArrayList<Font> availableFonts)165     private static @Nullable FontFamily createFontFamily(@NonNull String familyName,
166             @NonNull List<FontConfig.Font> fonts,
167             @NonNull String languageTags,
168             @FontConfig.Family.Variant int variant,
169             @NonNull Map<String, ByteBuffer> cache,
170             @NonNull ArrayList<Font> availableFonts) {
171         if (fonts.size() == 0) {
172             return null;
173         }
174 
175         FontFamily.Builder b = null;
176         for (int i = 0; i < fonts.size(); i++) {
177             final FontConfig.Font fontConfig = fonts.get(i);
178             final String fullPath = fontConfig.getFontName();
179             ByteBuffer buffer = cache.get(fullPath);
180             if (buffer == null) {
181                 if (cache.containsKey(fullPath)) {
182                     continue;  // Already failed to mmap. Skip it.
183                 }
184                 buffer = mmap(fullPath);
185                 cache.put(fullPath, buffer);
186                 if (buffer == null) {
187                     continue;
188                 }
189             }
190 
191             final Font font;
192             try {
193                 font = new Font.Builder(buffer, new File(fullPath), languageTags)
194                         .setWeight(fontConfig.getWeight())
195                         .setSlant(fontConfig.isItalic() ? FontStyle.FONT_SLANT_ITALIC
196                                 : FontStyle.FONT_SLANT_UPRIGHT)
197                         .setTtcIndex(fontConfig.getTtcIndex())
198                         .setFontVariationSettings(fontConfig.getAxes())
199                         .build();
200             } catch (IOException e) {
201                 throw new RuntimeException(e);  // Never reaches here
202             }
203 
204             availableFonts.add(font);
205             if (b == null) {
206                 b = new FontFamily.Builder(font);
207             } else {
208                 b.addFont(font);
209             }
210         }
211         return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */);
212     }
213 
appendNamedFamily(@onNull FontConfig.Family xmlFamily, @NonNull HashMap<String, ByteBuffer> bufferCache, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap, @NonNull ArrayList<Font> availableFonts)214     private static void appendNamedFamily(@NonNull FontConfig.Family xmlFamily,
215             @NonNull HashMap<String, ByteBuffer> bufferCache,
216             @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap,
217             @NonNull ArrayList<Font> availableFonts) {
218         final String familyName = xmlFamily.getName();
219         final FontFamily family = createFontFamily(
220                 familyName, Arrays.asList(xmlFamily.getFonts()),
221                 xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, availableFonts);
222         if (family == null) {
223             return;
224         }
225         final ArrayList<FontFamily> fallback = new ArrayList<>();
226         fallback.add(family);
227         fallbackListMap.put(familyName, fallback);
228     }
229 
230     /**
231      * Build the system fallback from xml file.
232      *
233      * @param xmlPath A full path string to the fonts.xml file.
234      * @param fontDir A full path string to the system font directory. This must end with
235      *                slash('/').
236      * @param fallbackMap An output system fallback map. Caller must pass empty map.
237      * @return a list of aliases
238      * @hide
239      */
240     @VisibleForTesting
buildSystemFallback(@onNull String xmlPath, @NonNull String fontDir, @NonNull FontCustomizationParser.Result oemCustomization, @NonNull ArrayMap<String, FontFamily[]> fallbackMap, @NonNull ArrayList<Font> availableFonts)241     public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
242             @NonNull String fontDir,
243             @NonNull FontCustomizationParser.Result oemCustomization,
244             @NonNull ArrayMap<String, FontFamily[]> fallbackMap,
245             @NonNull ArrayList<Font> availableFonts) {
246         try {
247             final FileInputStream fontsIn = new FileInputStream(xmlPath);
248             final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir);
249 
250             final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
251             final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
252 
253             final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>();
254             // First traverse families which have a 'name' attribute to create fallback map.
255             for (final FontConfig.Family xmlFamily : xmlFamilies) {
256                 final String familyName = xmlFamily.getName();
257                 if (familyName == null) {
258                     continue;
259                 }
260                 appendNamedFamily(xmlFamily, bufferCache, fallbackListMap, availableFonts);
261             }
262 
263             for (int i = 0; i < oemCustomization.mAdditionalNamedFamilies.size(); ++i) {
264                 appendNamedFamily(oemCustomization.mAdditionalNamedFamilies.get(i),
265                         bufferCache, fallbackListMap, availableFonts);
266             }
267 
268             // Then, add fallback fonts to the each fallback map.
269             for (int i = 0; i < xmlFamilies.length; i++) {
270                 final FontConfig.Family xmlFamily = xmlFamilies[i];
271                 // The first family (usually the sans-serif family) is always placed immediately
272                 // after the primary family in the fallback.
273                 if (i == 0 || xmlFamily.getName() == null) {
274                     pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, availableFonts);
275                 }
276             }
277 
278             // Build the font map and fallback map.
279             for (int i = 0; i < fallbackListMap.size(); i++) {
280                 final String fallbackName = fallbackListMap.keyAt(i);
281                 final List<FontFamily> familyList = fallbackListMap.valueAt(i);
282                 final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]);
283 
284                 fallbackMap.put(fallbackName, families);
285             }
286 
287             final ArrayList<FontConfig.Alias> list = new ArrayList<>();
288             list.addAll(Arrays.asList(fontConfig.getAliases()));
289             list.addAll(oemCustomization.mAdditionalAliases);
290             return list.toArray(new FontConfig.Alias[list.size()]);
291         } catch (IOException | XmlPullParserException e) {
292             Log.e(TAG, "Failed initialize system fallbacks.", e);
293             return ArrayUtils.emptyArray(FontConfig.Alias.class);
294         }
295     }
296 
readFontCustomization( @onNull String customizeXml, @NonNull String customFontsDir)297     private static FontCustomizationParser.Result readFontCustomization(
298             @NonNull String customizeXml, @NonNull String customFontsDir) {
299         try (FileInputStream f = new FileInputStream(customizeXml)) {
300             return FontCustomizationParser.parse(f, customFontsDir);
301         } catch (IOException e) {
302             return new FontCustomizationParser.Result();
303         } catch (XmlPullParserException e) {
304             Log.e(TAG, "Failed to parse font customization XML", e);
305             return new FontCustomizationParser.Result();
306         }
307     }
308 
309     static {
310         final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>();
311         final ArrayList<Font> availableFonts = new ArrayList<>();
312         final FontCustomizationParser.Result oemCustomization =
313                 readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
314         sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
315                 oemCustomization, systemFallbackMap, availableFonts);
316         sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap);
317         sAvailableFonts = Collections.unmodifiableList(availableFonts);
318     }
319 }
320