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.graphics.Typeface;
23 import android.text.FontConfig;
24 import android.util.ArrayMap;
25 import android.util.Log;
26 import android.util.SparseIntArray;
27 
28 import com.android.internal.annotations.GuardedBy;
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import org.xmlpull.v1.XmlPullParserException;
32 
33 import java.io.File;
34 import java.io.FileInputStream;
35 import java.io.IOException;
36 import java.nio.ByteBuffer;
37 import java.nio.channels.FileChannel;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Set;
43 
44 /**
45  * Provides the system font configurations.
46  */
47 public final class SystemFonts {
48     private static final String TAG = "SystemFonts";
49 
50     private static final String FONTS_XML = "/system/etc/fonts.xml";
51     /** @hide */
52     public static final String SYSTEM_FONT_DIR = "/system/fonts/";
53     private static final String OEM_XML = "/product/etc/fonts_customization.xml";
54     /** @hide */
55     public static final String OEM_FONT_DIR = "/product/fonts/";
56 
SystemFonts()57     private SystemFonts() {}  // Do not instansiate.
58 
59     private static final Object LOCK = new Object();
60     private static @GuardedBy("sLock") Set<Font> sAvailableFonts;
61 
62     /**
63      * Returns all available font files in the system.
64      *
65      * @return a set of system fonts
66      */
getAvailableFonts()67     public static @NonNull Set<Font> getAvailableFonts() {
68         synchronized (LOCK) {
69             if (sAvailableFonts == null) {
70                 sAvailableFonts = Font.getAvailableFonts();
71             }
72             return sAvailableFonts;
73         }
74     }
75 
76     /**
77      * @hide
78      */
resetAvailableFonts()79     public static void resetAvailableFonts() {
80         synchronized (LOCK) {
81             sAvailableFonts = null;
82         }
83     }
84 
mmap(@onNull String fullPath)85     private static @Nullable ByteBuffer mmap(@NonNull String fullPath) {
86         try (FileInputStream file = new FileInputStream(fullPath)) {
87             final FileChannel fileChannel = file.getChannel();
88             final long fontSize = fileChannel.size();
89             return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
90         } catch (IOException e) {
91             return null;
92         }
93     }
94 
pushFamilyToFallback(@onNull FontConfig.FontFamily xmlFamily, @NonNull ArrayMap<String, NativeFamilyListSet> fallbackMap, @NonNull Map<String, ByteBuffer> cache)95     private static void pushFamilyToFallback(@NonNull FontConfig.FontFamily xmlFamily,
96             @NonNull ArrayMap<String, NativeFamilyListSet> fallbackMap,
97             @NonNull Map<String, ByteBuffer> cache) {
98         final String languageTags = xmlFamily.getLocaleList().toLanguageTags();
99         final int variant = xmlFamily.getVariant();
100 
101         final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>();
102         final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts =
103                 new ArrayMap<>();
104 
105         // Collect default fallback and specific fallback fonts.
106         for (final FontConfig.Font font : xmlFamily.getFonts()) {
107             final String fallbackName = font.getFontFamilyName();
108             if (fallbackName == null) {
109                 defaultFonts.add(font);
110             } else {
111                 ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName);
112                 if (fallback == null) {
113                     fallback = new ArrayList<>();
114                     specificFallbackFonts.put(fallbackName, fallback);
115                 }
116                 fallback.add(font);
117             }
118         }
119 
120 
121         final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
122                 defaultFonts, languageTags, variant, false, cache);
123         // Insert family into fallback map.
124         for (int i = 0; i < fallbackMap.size(); i++) {
125             final String name = fallbackMap.keyAt(i);
126             final NativeFamilyListSet familyListSet = fallbackMap.valueAt(i);
127             int identityHash = System.identityHashCode(xmlFamily);
128             if (familyListSet.seenXmlFamilies.get(identityHash, -1) != -1) {
129                 continue;
130             } else {
131                 familyListSet.seenXmlFamilies.append(identityHash, 1);
132             }
133             final ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(name);
134             if (fallback == null) {
135                 if (defaultFamily != null) {
136                     familyListSet.familyList.add(defaultFamily);
137                 }
138             } else {
139                 final FontFamily family = createFontFamily(fallback, languageTags, variant, false,
140                         cache);
141                 if (family != null) {
142                     familyListSet.familyList.add(family);
143                 } else if (defaultFamily != null) {
144                     familyListSet.familyList.add(defaultFamily);
145                 } else {
146                     // There is no valid for for default fallback. Ignore.
147                 }
148             }
149         }
150     }
151 
createFontFamily( @onNull List<FontConfig.Font> fonts, @NonNull String languageTags, @FontConfig.FontFamily.Variant int variant, boolean isDefaultFallback, @NonNull Map<String, ByteBuffer> cache)152     private static @Nullable FontFamily createFontFamily(
153             @NonNull List<FontConfig.Font> fonts,
154             @NonNull String languageTags,
155             @FontConfig.FontFamily.Variant int variant,
156             boolean isDefaultFallback,
157             @NonNull Map<String, ByteBuffer> cache) {
158         if (fonts.size() == 0) {
159             return null;
160         }
161 
162         FontFamily.Builder b = null;
163         for (int i = 0; i < fonts.size(); i++) {
164             final FontConfig.Font fontConfig = fonts.get(i);
165             final String fullPath = fontConfig.getFile().getAbsolutePath();
166             ByteBuffer buffer = cache.get(fullPath);
167             if (buffer == null) {
168                 if (cache.containsKey(fullPath)) {
169                     continue;  // Already failed to mmap. Skip it.
170                 }
171                 buffer = mmap(fullPath);
172                 cache.put(fullPath, buffer);
173                 if (buffer == null) {
174                     continue;
175                 }
176             }
177 
178             final Font font;
179             try {
180                 font = new Font.Builder(buffer, new File(fullPath), languageTags)
181                         .setWeight(fontConfig.getStyle().getWeight())
182                         .setSlant(fontConfig.getStyle().getSlant())
183                         .setTtcIndex(fontConfig.getTtcIndex())
184                         .setFontVariationSettings(fontConfig.getFontVariationSettings())
185                         .build();
186             } catch (IOException e) {
187                 throw new RuntimeException(e);  // Never reaches here
188             }
189 
190             if (b == null) {
191                 b = new FontFamily.Builder(font);
192             } else {
193                 b.addFont(font);
194             }
195         }
196         return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */,
197                 isDefaultFallback);
198     }
199 
appendNamedFamilyList(@onNull FontConfig.NamedFamilyList namedFamilyList, @NonNull ArrayMap<String, ByteBuffer> bufferCache, @NonNull ArrayMap<String, NativeFamilyListSet> fallbackListMap)200     private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList namedFamilyList,
201             @NonNull ArrayMap<String, ByteBuffer> bufferCache,
202             @NonNull ArrayMap<String, NativeFamilyListSet> fallbackListMap) {
203         final String familyName = namedFamilyList.getName();
204         final NativeFamilyListSet familyListSet = new NativeFamilyListSet();
205         final List<FontConfig.FontFamily> xmlFamilies = namedFamilyList.getFamilies();
206         for (int i = 0; i < xmlFamilies.size(); ++i) {
207             FontConfig.FontFamily xmlFamily = xmlFamilies.get(i);
208             final FontFamily family = createFontFamily(
209                     xmlFamily.getFontList(),
210                     xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(),
211                     true, // named family is always default
212                     bufferCache);
213             if (family == null) {
214                 return;
215             }
216             familyListSet.familyList.add(family);
217             familyListSet.seenXmlFamilies.append(System.identityHashCode(xmlFamily), 1);
218         }
219         fallbackListMap.put(familyName, familyListSet);
220     }
221 
222     /**
223      * Get the updated FontConfig.
224      *
225      * @param updatableFontMap a font mapping of updated font files.
226      * @hide
227      */
getSystemFontConfig( @ullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )228     public static @NonNull FontConfig getSystemFontConfig(
229             @Nullable Map<String, File> updatableFontMap,
230             long lastModifiedDate,
231             int configVersion
232     ) {
233         return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR,
234                 updatableFontMap, lastModifiedDate, configVersion);
235     }
236 
237     /**
238      * Get the system preinstalled FontConfig.
239      * @hide
240      */
getSystemPreinstalledFontConfig()241     public static @NonNull FontConfig getSystemPreinstalledFontConfig() {
242         return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null,
243                 0, 0);
244     }
245 
getSystemFontConfigInternal( @onNull String fontsXml, @NonNull String systemFontDir, @Nullable String oemXml, @Nullable String productFontDir, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )246     /* package */ static @NonNull FontConfig getSystemFontConfigInternal(
247             @NonNull String fontsXml,
248             @NonNull String systemFontDir,
249             @Nullable String oemXml,
250             @Nullable String productFontDir,
251             @Nullable Map<String, File> updatableFontMap,
252             long lastModifiedDate,
253             int configVersion
254     ) {
255         try {
256             return FontListParser.parse(fontsXml, systemFontDir, oemXml, productFontDir,
257                                                 updatableFontMap, lastModifiedDate, configVersion);
258         } catch (IOException e) {
259             Log.e(TAG, "Failed to open/read system font configurations.", e);
260             return new FontConfig(Collections.emptyList(), Collections.emptyList(),
261                     Collections.emptyList(), 0, 0);
262         } catch (XmlPullParserException e) {
263             Log.e(TAG, "Failed to parse the system font configuration.", e);
264             return new FontConfig(Collections.emptyList(), Collections.emptyList(),
265                     Collections.emptyList(), 0, 0);
266         }
267     }
268 
269     /**
270      * Build the system fallback from FontConfig.
271      * @hide
272      */
273     @VisibleForTesting
buildSystemFallback(FontConfig fontConfig)274     public static Map<String, FontFamily[]> buildSystemFallback(FontConfig fontConfig) {
275         return buildSystemFallback(fontConfig, new ArrayMap<>());
276     }
277 
278     private static final class NativeFamilyListSet {
279         public List<FontFamily> familyList = new ArrayList<>();
280         public SparseIntArray seenXmlFamilies = new SparseIntArray();
281     }
282 
283     /** @hide */
284     @VisibleForTesting
buildSystemFallback(FontConfig fontConfig, ArrayMap<String, ByteBuffer> outBufferCache)285     public static Map<String, FontFamily[]> buildSystemFallback(FontConfig fontConfig,
286             ArrayMap<String, ByteBuffer> outBufferCache) {
287 
288         final ArrayMap<String, NativeFamilyListSet> fallbackListMap = new ArrayMap<>();
289 
290         final List<FontConfig.NamedFamilyList> namedFamilies = fontConfig.getNamedFamilyLists();
291         for (int i = 0; i < namedFamilies.size(); ++i) {
292             FontConfig.NamedFamilyList namedFamilyList = namedFamilies.get(i);
293             appendNamedFamilyList(namedFamilyList, outBufferCache, fallbackListMap);
294         }
295 
296         // Then, add fallback fonts to the fallback map.
297         final List<FontConfig.FontFamily> xmlFamilies = fontConfig.getFontFamilies();
298         for (int i = 0; i < xmlFamilies.size(); i++) {
299             final FontConfig.FontFamily xmlFamily = xmlFamilies.get(i);
300             pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache);
301         }
302 
303         // Build the font map and fallback map.
304         final Map<String, FontFamily[]> fallbackMap = new ArrayMap<>();
305         for (int i = 0; i < fallbackListMap.size(); i++) {
306             final String fallbackName = fallbackListMap.keyAt(i);
307             final List<FontFamily> familyList = fallbackListMap.valueAt(i).familyList;
308             fallbackMap.put(fallbackName, familyList.toArray(new FontFamily[0]));
309         }
310 
311         return fallbackMap;
312     }
313 
314     /**
315      * Build the system Typeface mappings from FontConfig and FallbackMap.
316      * @hide
317      */
318     @VisibleForTesting
buildSystemTypefaces( FontConfig fontConfig, Map<String, FontFamily[]> fallbackMap)319     public static Map<String, Typeface> buildSystemTypefaces(
320             FontConfig fontConfig,
321             Map<String, FontFamily[]> fallbackMap) {
322         final ArrayMap<String, Typeface> result = new ArrayMap<>();
323         Typeface.initSystemDefaultTypefaces(fallbackMap, fontConfig.getAliases(), result);
324         return result;
325     }
326 }
327