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