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