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 static android.text.FontConfig.Customization.LocaleFallback.OPERATION_APPEND;
20 import static android.text.FontConfig.Customization.LocaleFallback.OPERATION_PREPEND;
21 import static android.text.FontConfig.Customization.LocaleFallback.OPERATION_REPLACE;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.graphics.FontListParser;
26 import android.graphics.Typeface;
27 import android.os.LocaleList;
28 import android.text.FontConfig;
29 import android.util.ArrayMap;
30 import android.util.Log;
31 import android.util.SparseIntArray;
32 
33 import com.android.internal.annotations.GuardedBy;
34 import com.android.internal.annotations.VisibleForTesting;
35 
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.File;
39 import java.io.FileInputStream;
40 import java.io.IOException;
41 import java.nio.ByteBuffer;
42 import java.nio.channels.FileChannel;
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.Map;
48 import java.util.Set;
49 
50 /**
51  * Provides the system font configurations.
52  */
53 public final class SystemFonts {
54     private static final String TAG = "SystemFonts";
55 
56     private static final String FONTS_XML = "/system/etc/font_fallback.xml";
57     private static final String LEGACY_FONTS_XML = "/system/etc/fonts.xml";
58 
59     /** @hide */
60     public static final String SYSTEM_FONT_DIR = "/system/fonts/";
61     private static final String OEM_XML = "/product/etc/fonts_customization.xml";
62     /** @hide */
63     public static final String OEM_FONT_DIR = "/product/fonts/";
64 
SystemFonts()65     private SystemFonts() {}  // Do not instansiate.
66 
67     private static final Object LOCK = new Object();
68     private static @GuardedBy("sLock") Set<Font> sAvailableFonts;
69 
70     /**
71      * Returns all available font files in the system.
72      *
73      * @return a set of system fonts
74      */
getAvailableFonts()75     public static @NonNull Set<Font> getAvailableFonts() {
76         synchronized (LOCK) {
77             if (sAvailableFonts == null) {
78                 sAvailableFonts = Font.getAvailableFonts();
79             }
80             return sAvailableFonts;
81         }
82     }
83 
84     /**
85      * @hide
86      */
resetAvailableFonts()87     public static void resetAvailableFonts() {
88         synchronized (LOCK) {
89             sAvailableFonts = null;
90         }
91     }
92 
mmap(@onNull String fullPath)93     private static @Nullable ByteBuffer mmap(@NonNull String fullPath) {
94         try (FileInputStream file = new FileInputStream(fullPath)) {
95             final FileChannel fileChannel = file.getChannel();
96             final long fontSize = fileChannel.size();
97             return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
98         } catch (IOException e) {
99             return null;
100         }
101     }
102 
103     /** @hide */
104     @VisibleForTesting
resolveVarFamilyType( @onNull FontConfig.FontFamily xmlFamily, @Nullable String familyName)105     public static @FontFamily.Builder.VariableFontFamilyType int resolveVarFamilyType(
106             @NonNull FontConfig.FontFamily xmlFamily,
107             @Nullable String familyName) {
108         int wghtCount = 0;
109         int italCount = 0;
110         int targetFonts = 0;
111         boolean hasItalicFont = false;
112 
113         List<FontConfig.Font> fonts = xmlFamily.getFontList();
114         for (int i = 0; i < fonts.size(); ++i) {
115             FontConfig.Font font = fonts.get(i);
116 
117             if (familyName == null) {  // for default family
118                 if (font.getFontFamilyName() != null) {
119                     continue;  // this font is not for the default family.
120                 }
121             } else {  // for the specific family
122                 if (!familyName.equals(font.getFontFamilyName())) {
123                     continue;  // this font is not for given family.
124                 }
125             }
126 
127             final int varTypeAxes = font.getVarTypeAxes();
128             if (varTypeAxes == 0) {
129                 // If we see static font, we can immediately return as VAR_TYPE_NONE.
130                 return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
131             }
132 
133             if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_WGHT) != 0) {
134                 wghtCount++;
135             }
136 
137             if ((varTypeAxes & FontConfig.Font.VAR_TYPE_AXES_ITAL) != 0) {
138                 italCount++;
139             }
140 
141             if (font.getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC) {
142                 hasItalicFont = true;
143             }
144             targetFonts++;
145         }
146 
147         if (italCount == 0) {  // No ital font.
148             if (targetFonts == 1 && wghtCount == 1) {
149                 // If there is only single font that has wght, use it for regular style and
150                 // use synthetic bolding for italic.
151                 return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
152             } else if (targetFonts == 2 && wghtCount == 2 && hasItalicFont) {
153                 // If there are two fonts and italic font is available, use them for regular and
154                 // italic separately. (It is impossible to have two italic fonts. It will end up
155                 // with Typeface creation failure.)
156                 return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
157             }
158         } else if (italCount == 1) {
159             // If ital font is included, a single font should support both wght and ital.
160             if (wghtCount == 1 && targetFonts == 1) {
161                 return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
162             }
163         }
164         // Otherwise, unsupported.
165         return FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE;
166     }
167 
pushFamilyToFallback(@onNull FontConfig.FontFamily xmlFamily, @NonNull ArrayMap<String, NativeFamilyListSet> fallbackMap, @NonNull Map<String, ByteBuffer> cache)168     private static void pushFamilyToFallback(@NonNull FontConfig.FontFamily xmlFamily,
169             @NonNull ArrayMap<String, NativeFamilyListSet> fallbackMap,
170             @NonNull Map<String, ByteBuffer> cache) {
171         final String languageTags = xmlFamily.getLocaleList().toLanguageTags();
172         final int variant = xmlFamily.getVariant();
173 
174         final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>();
175         final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts =
176                 new ArrayMap<>();
177 
178         // Collect default fallback and specific fallback fonts.
179         for (final FontConfig.Font font : xmlFamily.getFonts()) {
180             final String fallbackName = font.getFontFamilyName();
181             if (fallbackName == null) {
182                 defaultFonts.add(font);
183             } else {
184                 ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName);
185                 if (fallback == null) {
186                     fallback = new ArrayList<>();
187                     specificFallbackFonts.put(fallbackName, fallback);
188                 }
189                 fallback.add(font);
190             }
191         }
192 
193         final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
194                 defaultFonts, languageTags, variant, resolveVarFamilyType(xmlFamily, null), false,
195                 cache);
196         // Insert family into fallback map.
197         for (int i = 0; i < fallbackMap.size(); i++) {
198             final String name = fallbackMap.keyAt(i);
199             final NativeFamilyListSet familyListSet = fallbackMap.valueAt(i);
200             int identityHash = System.identityHashCode(xmlFamily);
201             if (familyListSet.seenXmlFamilies.get(identityHash, -1) != -1) {
202                 continue;
203             } else {
204                 familyListSet.seenXmlFamilies.append(identityHash, 1);
205             }
206             final ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(name);
207             if (fallback == null) {
208                 if (defaultFamily != null) {
209                     familyListSet.familyList.add(defaultFamily);
210                 }
211             } else {
212                 final FontFamily family = createFontFamily(fallback, languageTags, variant,
213                         resolveVarFamilyType(xmlFamily, name), false, cache);
214                 if (family != null) {
215                     familyListSet.familyList.add(family);
216                 } else if (defaultFamily != null) {
217                     familyListSet.familyList.add(defaultFamily);
218                 } else {
219                     // There is no valid for default fallback. Ignore.
220                 }
221             }
222         }
223     }
224 
createFontFamily( @onNull List<FontConfig.Font> fonts, @NonNull String languageTags, @FontConfig.FontFamily.Variant int variant, int varFamilyType, boolean isDefaultFallback, @NonNull Map<String, ByteBuffer> cache)225     private static @Nullable FontFamily createFontFamily(
226             @NonNull List<FontConfig.Font> fonts,
227             @NonNull String languageTags,
228             @FontConfig.FontFamily.Variant int variant,
229             int varFamilyType,
230             boolean isDefaultFallback,
231             @NonNull Map<String, ByteBuffer> cache) {
232         if (fonts.size() == 0) {
233             return null;
234         }
235 
236         FontFamily.Builder b = null;
237         for (int i = 0; i < fonts.size(); i++) {
238             final FontConfig.Font fontConfig = fonts.get(i);
239             final String fullPath = fontConfig.getFile().getAbsolutePath();
240             ByteBuffer buffer = cache.get(fullPath);
241             if (buffer == null) {
242                 if (cache.containsKey(fullPath)) {
243                     continue;  // Already failed to mmap. Skip it.
244                 }
245                 buffer = mmap(fullPath);
246                 cache.put(fullPath, buffer);
247                 if (buffer == null) {
248                     continue;
249                 }
250             }
251 
252             final Font font;
253             try {
254                 font = new Font.Builder(buffer, new File(fullPath), languageTags)
255                         .setWeight(fontConfig.getStyle().getWeight())
256                         .setSlant(fontConfig.getStyle().getSlant())
257                         .setTtcIndex(fontConfig.getTtcIndex())
258                         .setFontVariationSettings(fontConfig.getFontVariationSettings())
259                         .build();
260             } catch (IOException e) {
261                 throw new RuntimeException(e);  // Never reaches here
262             }
263 
264             if (b == null) {
265                 b = new FontFamily.Builder(font);
266             } else {
267                 b.addFont(font);
268             }
269         }
270         return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */,
271                 isDefaultFallback, varFamilyType);
272     }
273 
appendNamedFamilyList(@onNull FontConfig.NamedFamilyList namedFamilyList, @NonNull ArrayMap<String, ByteBuffer> bufferCache, @NonNull ArrayMap<String, NativeFamilyListSet> fallbackListMap)274     private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList namedFamilyList,
275             @NonNull ArrayMap<String, ByteBuffer> bufferCache,
276             @NonNull ArrayMap<String, NativeFamilyListSet> fallbackListMap) {
277         final String familyName = namedFamilyList.getName();
278         final NativeFamilyListSet familyListSet = new NativeFamilyListSet();
279         final List<FontConfig.FontFamily> xmlFamilies = namedFamilyList.getFamilies();
280         for (int i = 0; i < xmlFamilies.size(); ++i) {
281             FontConfig.FontFamily xmlFamily = xmlFamilies.get(i);
282             final FontFamily family = createFontFamily(
283                     xmlFamily.getFontList(),
284                     xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(),
285                     resolveVarFamilyType(xmlFamily,
286                             null /* all fonts under named family should be treated as default */),
287                     true, // named family is always default
288                     bufferCache);
289             if (family == null) {
290                 return;
291             }
292             familyListSet.familyList.add(family);
293             familyListSet.seenXmlFamilies.append(System.identityHashCode(xmlFamily), 1);
294         }
295         fallbackListMap.put(familyName, familyListSet);
296     }
297 
298     /**
299      * Get the updated FontConfig.
300      *
301      * @param updatableFontMap a font mapping of updated font files.
302      * @hide
303      */
getSystemFontConfig( @ullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )304     public static @NonNull FontConfig getSystemFontConfig(
305             @Nullable Map<String, File> updatableFontMap,
306             long lastModifiedDate,
307             int configVersion
308     ) {
309         final String fontsXml;
310         if (com.android.text.flags.Flags.newFontsFallbackXml()) {
311             fontsXml = FONTS_XML;
312         } else {
313             fontsXml = LEGACY_FONTS_XML;
314         }
315         return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR,
316                 updatableFontMap, lastModifiedDate, configVersion);
317     }
318 
319     /**
320      * Get the updated FontConfig.
321      *
322      * @param updatableFontMap a font mapping of updated font files.
323      * @hide
324      */
getSystemFontConfigForTesting( @onNull String fontsXml, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )325     public static @NonNull FontConfig getSystemFontConfigForTesting(
326             @NonNull String fontsXml,
327             @Nullable Map<String, File> updatableFontMap,
328             long lastModifiedDate,
329             int configVersion
330     ) {
331         return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR,
332                 updatableFontMap, lastModifiedDate, configVersion);
333     }
334 
335     /**
336      * Get the system preinstalled FontConfig.
337      * @hide
338      */
getSystemPreinstalledFontConfig()339     public static @NonNull FontConfig getSystemPreinstalledFontConfig() {
340         final String fontsXml;
341         if (com.android.text.flags.Flags.newFontsFallbackXml()) {
342             fontsXml = FONTS_XML;
343         } else {
344             fontsXml = LEGACY_FONTS_XML;
345         }
346         return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null,
347                 0, 0);
348     }
349 
350     /**
351      * @hide
352      */
getSystemPreinstalledFontConfigFromLegacyXml()353     public static @NonNull FontConfig getSystemPreinstalledFontConfigFromLegacyXml() {
354         return getSystemFontConfigInternal(LEGACY_FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR,
355                 null, 0, 0);
356     }
357 
getSystemFontConfigInternal( @onNull String fontsXml, @NonNull String systemFontDir, @Nullable String oemXml, @Nullable String productFontDir, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )358     /* package */ static @NonNull FontConfig getSystemFontConfigInternal(
359             @NonNull String fontsXml,
360             @NonNull String systemFontDir,
361             @Nullable String oemXml,
362             @Nullable String productFontDir,
363             @Nullable Map<String, File> updatableFontMap,
364             long lastModifiedDate,
365             int configVersion
366     ) {
367         try {
368             Log.i(TAG, "Loading font config from " + fontsXml);
369             return FontListParser.parse(fontsXml, systemFontDir, oemXml, productFontDir,
370                                                 updatableFontMap, lastModifiedDate, configVersion);
371         } catch (IOException e) {
372             Log.e(TAG, "Failed to open/read system font configurations.", e);
373             return new FontConfig(Collections.emptyList(), Collections.emptyList(),
374                     Collections.emptyList(), Collections.emptyList(), 0, 0);
375         } catch (XmlPullParserException e) {
376             Log.e(TAG, "Failed to parse the system font configuration.", e);
377             return new FontConfig(Collections.emptyList(), Collections.emptyList(),
378                     Collections.emptyList(), Collections.emptyList(), 0, 0);
379         }
380     }
381 
382     /**
383      * Build the system fallback from FontConfig.
384      * @hide
385      */
386     @VisibleForTesting
buildSystemFallback(FontConfig fontConfig)387     public static Map<String, FontFamily[]> buildSystemFallback(FontConfig fontConfig) {
388         return buildSystemFallback(fontConfig, new ArrayMap<>());
389     }
390 
391     private static final class NativeFamilyListSet {
392         public List<FontFamily> familyList = new ArrayList<>();
393         public SparseIntArray seenXmlFamilies = new SparseIntArray();
394     }
395 
396     /** @hide */
397     @VisibleForTesting
buildSystemFallback(FontConfig fontConfig, ArrayMap<String, ByteBuffer> outBufferCache)398     public static Map<String, FontFamily[]> buildSystemFallback(FontConfig fontConfig,
399             ArrayMap<String, ByteBuffer> outBufferCache) {
400 
401         final ArrayMap<String, NativeFamilyListSet> fallbackListMap = new ArrayMap<>();
402         final List<FontConfig.Customization.LocaleFallback> localeFallbacks =
403                 fontConfig.getLocaleFallbackCustomizations();
404 
405         final List<FontConfig.NamedFamilyList> namedFamilies = fontConfig.getNamedFamilyLists();
406         for (int i = 0; i < namedFamilies.size(); ++i) {
407             FontConfig.NamedFamilyList namedFamilyList = namedFamilies.get(i);
408             appendNamedFamilyList(namedFamilyList, outBufferCache, fallbackListMap);
409         }
410 
411         // Then, add fallback fonts to the fallback map.
412         final List<FontConfig.Customization.LocaleFallback> customizations = new ArrayList<>();
413         final List<FontConfig.FontFamily> xmlFamilies = fontConfig.getFontFamilies();
414         final SparseIntArray seenCustomization = new SparseIntArray();
415         for (int i = 0; i < xmlFamilies.size(); i++) {
416             final FontConfig.FontFamily xmlFamily = xmlFamilies.get(i);
417 
418             customizations.clear();
419             for (int j = 0; j < localeFallbacks.size(); ++j) {
420                 if (seenCustomization.get(j, -1) != -1) {
421                     continue;  // The customization is already applied.
422                 }
423                 FontConfig.Customization.LocaleFallback localeFallback = localeFallbacks.get(j);
424                 if (scriptMatch(xmlFamily.getLocaleList(), localeFallback.getScript())) {
425                     customizations.add(localeFallback);
426                     seenCustomization.put(j, 1);
427                 }
428             }
429 
430             if (customizations.isEmpty()) {
431                 pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache);
432             } else {
433                 for (int j = 0; j < customizations.size(); ++j) {
434                     FontConfig.Customization.LocaleFallback localeFallback = customizations.get(j);
435                     if (localeFallback.getOperation() == OPERATION_PREPEND) {
436                         pushFamilyToFallback(localeFallback.getFamily(), fallbackListMap,
437                                 outBufferCache);
438                     }
439                 }
440                 boolean isReplaced = false;
441                 for (int j = 0; j < customizations.size(); ++j) {
442                     FontConfig.Customization.LocaleFallback localeFallback = customizations.get(j);
443                     if (localeFallback.getOperation() == OPERATION_REPLACE) {
444                         pushFamilyToFallback(localeFallback.getFamily(), fallbackListMap,
445                                 outBufferCache);
446                         isReplaced = true;
447                     }
448                 }
449                 if (!isReplaced) {  // If nothing is replaced, push the original one.
450                     pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache);
451                 }
452                 for (int j = 0; j < customizations.size(); ++j) {
453                     FontConfig.Customization.LocaleFallback localeFallback = customizations.get(j);
454                     if (localeFallback.getOperation() == OPERATION_APPEND) {
455                         pushFamilyToFallback(localeFallback.getFamily(), fallbackListMap,
456                                 outBufferCache);
457                     }
458                 }
459             }
460         }
461 
462         // Build the font map and fallback map.
463         final Map<String, FontFamily[]> fallbackMap = new ArrayMap<>();
464         for (int i = 0; i < fallbackListMap.size(); i++) {
465             final String fallbackName = fallbackListMap.keyAt(i);
466             final List<FontFamily> familyList = fallbackListMap.valueAt(i).familyList;
467             fallbackMap.put(fallbackName, familyList.toArray(new FontFamily[0]));
468         }
469 
470         return fallbackMap;
471     }
472 
473     /**
474      * Build the system Typeface mappings from FontConfig and FallbackMap.
475      * @hide
476      */
477     @VisibleForTesting
buildSystemTypefaces( FontConfig fontConfig, Map<String, FontFamily[]> fallbackMap)478     public static Map<String, Typeface> buildSystemTypefaces(
479             FontConfig fontConfig,
480             Map<String, FontFamily[]> fallbackMap) {
481         final ArrayMap<String, Typeface> result = new ArrayMap<>();
482         Typeface.initSystemDefaultTypefaces(fallbackMap, fontConfig.getAliases(), result);
483         return result;
484     }
485 
scriptMatch(LocaleList localeList, String targetScript)486     private static boolean scriptMatch(LocaleList localeList, String targetScript) {
487         if (localeList == null || localeList.isEmpty()) {
488             return false;
489         }
490         for (int i = 0; i < localeList.size(); ++i) {
491             Locale locale = localeList.get(i);
492             if (locale == null) {
493                 continue;
494             }
495             String baseScript = FontConfig.resolveScript(locale);
496             if (baseScript.equals(targetScript)) {
497                 return true;
498             }
499 
500             // Subtag match
501             if (targetScript.equals("Bopo") && baseScript.equals("Hanb")) {
502                 // Hanb is Han with Bopomofo.
503                 return true;
504             } else if (targetScript.equals("Hani")) {
505                 if (baseScript.equals("Hanb") || baseScript.equals("Hans")
506                         || baseScript.equals("Hant") || baseScript.equals("Kore")
507                         || baseScript.equals("Jpan")) {
508                     // Han id suppoted by Taiwanese, Traditional Chinese, Simplified Chinese, Korean
509                     // and Japanese.
510                     return true;
511                 }
512             } else if (targetScript.equals("Hira") || targetScript.equals("Hrkt")
513                     || targetScript.equals("Kana")) {
514                 if (baseScript.equals("Jpan") || baseScript.equals("Hrkt")) {
515                     // Hiragana, Hiragana-Katakana, Katakana is supported by Japanese and
516                     // Hiragana-Katakana script.
517                     return true;
518                 }
519             }
520         }
521         return false;
522     }
523 }
524