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