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             return null;
109         }
110     }
111 
pushFamilyToFallback(@onNull FontConfig.Family xmlFamily, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, @NonNull Map<String, ByteBuffer> cache, @NonNull ArrayList<Font> availableFonts)112     private static void pushFamilyToFallback(@NonNull FontConfig.Family xmlFamily,
113             @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap,
114             @NonNull Map<String, ByteBuffer> cache,
115             @NonNull ArrayList<Font> availableFonts) {
116 
117         final String languageTags = xmlFamily.getLanguages();
118         final int variant = xmlFamily.getVariant();
119 
120         final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>();
121         final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>();
122 
123         // Collect default fallback and specific fallback fonts.
124         for (final FontConfig.Font font : xmlFamily.getFonts()) {
125             final String fallbackName = font.getFallbackFor();
126             if (fallbackName == null) {
127                 defaultFonts.add(font);
128             } else {
129                 ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName);
130                 if (fallback == null) {
131                     fallback = new ArrayList<>();
132                     specificFallbackFonts.put(fallbackName, fallback);
133                 }
134                 fallback.add(font);
135             }
136         }
137 
138         final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
139                 xmlFamily.getName(), defaultFonts, languageTags, variant, cache, availableFonts);
140 
141         // Insert family into fallback map.
142         for (int i = 0; i < fallbackMap.size(); i++) {
143             final ArrayList<FontConfig.Font> fallback =
144                     specificFallbackFonts.get(fallbackMap.keyAt(i));
145             if (fallback == null) {
146                 if (defaultFamily != null) {
147                     fallbackMap.valueAt(i).add(defaultFamily);
148                 }
149             } else {
150                 final FontFamily family = createFontFamily(
151                         xmlFamily.getName(), fallback, languageTags, variant, cache,
152                         availableFonts);
153                 if (family != null) {
154                     fallbackMap.valueAt(i).add(family);
155                 } else if (defaultFamily != null) {
156                     fallbackMap.valueAt(i).add(defaultFamily);
157                 } else {
158                     // There is no valid for for default fallback. Ignore.
159                 }
160             }
161         }
162     }
163 
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)164     private static @Nullable FontFamily createFontFamily(@NonNull String familyName,
165             @NonNull List<FontConfig.Font> fonts,
166             @NonNull String languageTags,
167             @FontConfig.Family.Variant int variant,
168             @NonNull Map<String, ByteBuffer> cache,
169             @NonNull ArrayList<Font> availableFonts) {
170         if (fonts.size() == 0) {
171             return null;
172         }
173 
174         FontFamily.Builder b = null;
175         for (int i = 0; i < fonts.size(); i++) {
176             final FontConfig.Font fontConfig = fonts.get(i);
177             final String fullPath = fontConfig.getFontName();
178             ByteBuffer buffer = cache.get(fullPath);
179             if (buffer == null) {
180                 if (cache.containsKey(fullPath)) {
181                     continue;  // Already failed to mmap. Skip it.
182                 }
183                 buffer = mmap(fullPath);
184                 cache.put(fullPath, buffer);
185                 if (buffer == null) {
186                     continue;
187                 }
188             }
189 
190             final Font font;
191             try {
192                 font = new Font.Builder(buffer, new File(fullPath), languageTags)
193                         .setWeight(fontConfig.getWeight())
194                         .setSlant(fontConfig.isItalic() ? FontStyle.FONT_SLANT_ITALIC
195                                 : FontStyle.FONT_SLANT_UPRIGHT)
196                         .setTtcIndex(fontConfig.getTtcIndex())
197                         .setFontVariationSettings(fontConfig.getAxes())
198                         .build();
199             } catch (IOException e) {
200                 throw new RuntimeException(e);  // Never reaches here
201             }
202 
203             availableFonts.add(font);
204             if (b == null) {
205                 b = new FontFamily.Builder(font);
206             } else {
207                 b.addFont(font);
208             }
209         }
210         return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */);
211     }
212 
appendNamedFamily(@onNull FontConfig.Family xmlFamily, @NonNull HashMap<String, ByteBuffer> bufferCache, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap, @NonNull ArrayList<Font> availableFonts)213     private static void appendNamedFamily(@NonNull FontConfig.Family xmlFamily,
214             @NonNull HashMap<String, ByteBuffer> bufferCache,
215             @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap,
216             @NonNull ArrayList<Font> availableFonts) {
217         final String familyName = xmlFamily.getName();
218         final FontFamily family = createFontFamily(
219                 familyName, Arrays.asList(xmlFamily.getFonts()),
220                 xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, availableFonts);
221         if (family == null) {
222             return;
223         }
224         final ArrayList<FontFamily> fallback = new ArrayList<>();
225         fallback.add(family);
226         fallbackListMap.put(familyName, fallback);
227     }
228 
229     /**
230      * Build the system fallback from xml file.
231      *
232      * @param xmlPath A full path string to the fonts.xml file.
233      * @param fontDir A full path string to the system font directory. This must end with
234      *                slash('/').
235      * @param fallbackMap An output system fallback map. Caller must pass empty map.
236      * @return a list of aliases
237      * @hide
238      */
239     @VisibleForTesting
buildSystemFallback(@onNull String xmlPath, @NonNull String fontDir, @NonNull FontCustomizationParser.Result oemCustomization, @NonNull ArrayMap<String, FontFamily[]> fallbackMap, @NonNull ArrayList<Font> availableFonts)240     public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
241             @NonNull String fontDir,
242             @NonNull FontCustomizationParser.Result oemCustomization,
243             @NonNull ArrayMap<String, FontFamily[]> fallbackMap,
244             @NonNull ArrayList<Font> availableFonts) {
245         try {
246             final FileInputStream fontsIn = new FileInputStream(xmlPath);
247             final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir);
248 
249             final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
250             final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
251 
252             final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>();
253             // First traverse families which have a 'name' attribute to create fallback map.
254             for (final FontConfig.Family xmlFamily : xmlFamilies) {
255                 final String familyName = xmlFamily.getName();
256                 if (familyName == null) {
257                     continue;
258                 }
259                 appendNamedFamily(xmlFamily, bufferCache, fallbackListMap, availableFonts);
260             }
261 
262             for (int i = 0; i < oemCustomization.mAdditionalNamedFamilies.size(); ++i) {
263                 appendNamedFamily(oemCustomization.mAdditionalNamedFamilies.get(i),
264                         bufferCache, fallbackListMap, availableFonts);
265             }
266 
267             // Then, add fallback fonts to the each fallback map.
268             for (int i = 0; i < xmlFamilies.length; i++) {
269                 final FontConfig.Family xmlFamily = xmlFamilies[i];
270                 // The first family (usually the sans-serif family) is always placed immediately
271                 // after the primary family in the fallback.
272                 if (i == 0 || xmlFamily.getName() == null) {
273                     pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, availableFonts);
274                 }
275             }
276 
277             // Build the font map and fallback map.
278             for (int i = 0; i < fallbackListMap.size(); i++) {
279                 final String fallbackName = fallbackListMap.keyAt(i);
280                 final List<FontFamily> familyList = fallbackListMap.valueAt(i);
281                 final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]);
282 
283                 fallbackMap.put(fallbackName, families);
284             }
285 
286             final ArrayList<FontConfig.Alias> list = new ArrayList<>();
287             list.addAll(Arrays.asList(fontConfig.getAliases()));
288             list.addAll(oemCustomization.mAdditionalAliases);
289             return list.toArray(new FontConfig.Alias[list.size()]);
290         } catch (IOException | XmlPullParserException e) {
291             Log.e(TAG, "Failed initialize system fallbacks.", e);
292             return ArrayUtils.emptyArray(FontConfig.Alias.class);
293         }
294     }
295 
readFontCustomization( @onNull String customizeXml, @NonNull String customFontsDir)296     private static FontCustomizationParser.Result readFontCustomization(
297             @NonNull String customizeXml, @NonNull String customFontsDir) {
298         try (FileInputStream f = new FileInputStream(customizeXml)) {
299             return FontCustomizationParser.parse(f, customFontsDir);
300         } catch (IOException e) {
301             return new FontCustomizationParser.Result();
302         } catch (XmlPullParserException e) {
303             Log.e(TAG, "Failed to parse font customization XML", e);
304             return new FontCustomizationParser.Result();
305         }
306     }
307 
308     static {
309         final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>();
310         final ArrayList<Font> availableFonts = new ArrayList<>();
311         final FontCustomizationParser.Result oemCustomization =
312                 readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
313         sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
314                 oemCustomization, systemFallbackMap, availableFonts);
315         sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap);
316         sAvailableFonts = Collections.unmodifiableList(availableFonts);
317     }
318 }
319