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