1 /* 2 * Copyright (C) 2014 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; 18 19 import com.android.ide.common.rendering.api.AssetRepository; 20 import com.android.ide.common.rendering.api.LayoutLog; 21 import com.android.layoutlib.bridge.Bridge; 22 import com.android.layoutlib.bridge.impl.DelegateManager; 23 import com.android.tools.layoutlib.annotations.LayoutlibDelegate; 24 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.content.res.AssetManager; 28 import android.content.res.BridgeAssetManager; 29 import android.graphics.fonts.FontVariationAxis; 30 31 import java.awt.Font; 32 import java.awt.FontFormatException; 33 import java.io.File; 34 import java.io.FileNotFoundException; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.nio.ByteBuffer; 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.HashSet; 41 import java.util.LinkedHashMap; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Objects; 45 import java.util.Scanner; 46 import java.util.Set; 47 import java.util.logging.Logger; 48 49 import libcore.util.NativeAllocationRegistry_Delegate; 50 import sun.font.FontUtilities; 51 52 import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE; 53 import static android.graphics.Typeface_Delegate.SYSTEM_FONTS; 54 55 /** 56 * Delegate implementing the native methods of android.graphics.FontFamily 57 * 58 * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced 59 * by calls to methods of the same name in this delegate class. 60 * 61 * This class behaves like the original native implementation, but in Java, keeping previously 62 * native data into its own objects and mapping them to int that are sent back and forth between 63 * it and the original FontFamily class. 64 * 65 * @see DelegateManager 66 */ 67 public class FontFamily_Delegate { 68 69 public static final int DEFAULT_FONT_WEIGHT = 400; 70 public static final int BOLD_FONT_WEIGHT_DELTA = 300; 71 public static final int BOLD_FONT_WEIGHT = 700; 72 73 private static final String FONT_SUFFIX_ITALIC = "Italic.ttf"; 74 private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt"; 75 private static final String EXTENSION_OTF = ".otf"; 76 77 private static final int CACHE_SIZE = 10; 78 // The cache has a drawback that if the font file changed after the font object was created, 79 // we will not update it. 80 private static final Map<String, FontInfo> sCache = 81 new LinkedHashMap<String, FontInfo>(CACHE_SIZE) { 82 @Override 83 protected boolean removeEldestEntry(Map.Entry<String, FontInfo> eldest) { 84 return size() > CACHE_SIZE; 85 } 86 87 @Override 88 public FontInfo put(String key, FontInfo value) { 89 // renew this entry. 90 FontInfo removed = remove(key); 91 super.put(key, value); 92 return removed; 93 } 94 }; 95 96 /** 97 * A class associating {@link Font} with its metadata. 98 */ 99 public static final class FontInfo { 100 @Nullable 101 public Font mFont; 102 public int mWeight; 103 public boolean mIsItalic; 104 105 @Override equals(Object o)106 public boolean equals(Object o) { 107 if (this == o) { 108 return true; 109 } 110 if (o == null || getClass() != o.getClass()) { 111 return false; 112 } 113 FontInfo fontInfo = (FontInfo) o; 114 return mWeight == fontInfo.mWeight && mIsItalic == fontInfo.mIsItalic; 115 } 116 117 @Override hashCode()118 public int hashCode() { 119 return Objects.hash(mWeight, mIsItalic); 120 } 121 122 @Override toString()123 public String toString() { 124 return "FontInfo{" + "mWeight=" + mWeight + ", mIsItalic=" + mIsItalic + '}'; 125 } 126 } 127 128 // ---- delegate manager ---- 129 private static final DelegateManager<FontFamily_Delegate> sManager = 130 new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class); 131 private static long sFamilyFinalizer = -1; 132 133 // ---- delegate helper data ---- 134 private static String sFontLocation; 135 private static final List<FontFamily_Delegate> sPostInitDelegate = new 136 ArrayList<FontFamily_Delegate>(); 137 private static Set<String> SDK_FONTS; 138 139 140 // ---- delegate data ---- 141 142 // Order does not really matter but we use a LinkedHashMap to get reproducible results across 143 // render calls 144 private Map<FontInfo, Font> mFonts = new LinkedHashMap<>(); 145 146 /** 147 * The variant of the Font Family - compact or elegant. 148 * <p/> 149 * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in 150 * android.graphics.FontFamily 151 * 152 * @see Paint#setElegantTextHeight(boolean) 153 */ 154 private FontVariant mVariant; 155 // List of runnables to process fonts after sFontLoader is initialized. 156 private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>(); 157 /** @see #isValid() */ 158 private boolean mValid = false; 159 160 161 // ---- Public helper class ---- 162 163 public enum FontVariant { 164 // The order needs to be kept in sync with android.graphics.FontFamily. 165 NONE, COMPACT, ELEGANT 166 } 167 168 // ---- Public Helper methods ---- 169 getDelegate(long nativeFontFamily)170 public static FontFamily_Delegate getDelegate(long nativeFontFamily) { 171 return sManager.getDelegate(nativeFontFamily); 172 } 173 setFontLocation(String fontLocation)174 public static synchronized void setFontLocation(String fontLocation) { 175 sFontLocation = fontLocation; 176 // init list of bundled fonts. 177 File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST); 178 // Current number of fonts is 103. Use the next round number to leave scope for more fonts 179 // in the future. 180 Set<String> allFontsList = new HashSet<>(128); 181 Scanner scanner = null; 182 try { 183 scanner = new Scanner(allFonts); 184 while (scanner.hasNext()) { 185 String name = scanner.next(); 186 // Skip font configuration files. 187 if (!name.endsWith(".xml")) { 188 allFontsList.add(name); 189 } 190 } 191 } catch (FileNotFoundException e) { 192 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 193 "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.", 194 e, null); 195 } finally { 196 if (scanner != null) { 197 scanner.close(); 198 } 199 } 200 SDK_FONTS = Collections.unmodifiableSet(allFontsList); 201 for (FontFamily_Delegate fontFamily : sPostInitDelegate) { 202 fontFamily.init(); 203 } 204 sPostInitDelegate.clear(); 205 } 206 207 @Nullable getFont(int desiredWeight, boolean isItalic)208 public Font getFont(int desiredWeight, boolean isItalic) { 209 FontInfo desiredStyle = new FontInfo(); 210 desiredStyle.mWeight = desiredWeight; 211 desiredStyle.mIsItalic = isItalic; 212 213 Font cachedFont = mFonts.get(desiredStyle); 214 if (cachedFont != null) { 215 return cachedFont; 216 } 217 218 FontInfo bestFont = null; 219 220 if (mFonts.size() == 1) { 221 // No need to compute the match since we only have one candidate 222 bestFont = mFonts.keySet().iterator().next(); 223 } else { 224 int bestMatch = Integer.MAX_VALUE; 225 226 for (FontInfo font : mFonts.keySet()) { 227 int match = computeMatch(font, desiredStyle); 228 if (match < bestMatch) { 229 bestMatch = match; 230 bestFont = font; 231 if (bestMatch == 0) { 232 break; 233 } 234 } 235 } 236 } 237 238 if (bestFont == null) { 239 return null; 240 } 241 242 243 // Derive the font as required and add it to the list of Fonts. 244 deriveFont(bestFont, desiredStyle); 245 addFont(desiredStyle); 246 return desiredStyle.mFont; 247 } 248 getVariant()249 public FontVariant getVariant() { 250 return mVariant; 251 } 252 253 /** 254 * Returns if the FontFamily should contain any fonts. If this returns true and 255 * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while 256 * loading the fonts. However, some fonts are deliberately skipped, for example they are not 257 * bundled with the SDK. In such a case, this method returns false. 258 */ isValid()259 public boolean isValid() { 260 return mValid; 261 } 262 loadFont(String path)263 private static Font loadFont(String path) { 264 if (path.startsWith(SYSTEM_FONTS) ) { 265 String relativePath = path.substring(SYSTEM_FONTS.length()); 266 File f = new File(sFontLocation, relativePath); 267 268 try { 269 return Font.createFont(Font.TRUETYPE_FONT, f); 270 } catch (Exception e) { 271 if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) { 272 // If we aren't able to load an Open Type font, don't log a warning just yet. 273 // We wait for a case where font is being used. Only then we try to log the 274 // warning. 275 return null; 276 } 277 Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, 278 String.format("Unable to load font %1$s", relativePath), 279 e, null); 280 } 281 } else { 282 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 283 "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.", 284 null, null); 285 } 286 287 return null; 288 } 289 290 @Nullable getFontLocation()291 public static String getFontLocation() { 292 return sFontLocation; 293 } 294 295 // ---- delegate methods ---- 296 @LayoutlibDelegate addFont(FontFamily thisFontFamily, String path, int ttcIndex, FontVariationAxis[] axes, int weight, int italic)297 /*package*/ static boolean addFont(FontFamily thisFontFamily, String path, int ttcIndex, 298 FontVariationAxis[] axes, int weight, int italic) { 299 if (thisFontFamily.mBuilderPtr == 0) { 300 assert false : "Unable to call addFont after freezing."; 301 return false; 302 } 303 final FontFamily_Delegate delegate = getDelegate(thisFontFamily.mBuilderPtr); 304 return delegate != null && delegate.addFont(path, ttcIndex, weight, italic); 305 } 306 307 // ---- native methods ---- 308 309 @LayoutlibDelegate nInitBuilder(String lang, int variant)310 /*package*/ static long nInitBuilder(String lang, int variant) { 311 // TODO: support lang. This is required for japanese locale. 312 FontFamily_Delegate delegate = new FontFamily_Delegate(); 313 // variant can be 0, 1 or 2. 314 assert variant < 3; 315 delegate.mVariant = FontVariant.values()[variant]; 316 if (sFontLocation != null) { 317 delegate.init(); 318 } else { 319 sPostInitDelegate.add(delegate); 320 } 321 return sManager.addNewDelegate(delegate); 322 } 323 324 @LayoutlibDelegate 325 /*package*/ static long nCreateFamily(long builderPtr) { 326 return builderPtr; 327 } 328 329 @LayoutlibDelegate 330 /*package*/ static long nGetFamilyReleaseFunc() { 331 synchronized (FontFamily_Delegate.class) { 332 if (sFamilyFinalizer == -1) { 333 sFamilyFinalizer = NativeAllocationRegistry_Delegate.createFinalizer( 334 sManager::removeJavaReferenceFor); 335 } 336 } 337 return sFamilyFinalizer; 338 } 339 340 @LayoutlibDelegate 341 /*package*/ static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex, 342 int weight, int isItalic) { 343 assert false : "The only client of this method has been overridden."; 344 return false; 345 } 346 347 @LayoutlibDelegate 348 /*package*/ static boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font, 349 int ttcIndex, int weight, int isItalic) { 350 assert false : "The only client of this method has been overridden."; 351 return false; 352 } 353 354 @LayoutlibDelegate 355 /*package*/ static void nAddAxisValue(long builderPtr, int tag, float value) { 356 assert false : "The only client of this method has been overridden."; 357 } 358 359 static boolean addFont(long builderPtr, final String path, final int weight, 360 final boolean isItalic) { 361 final FontFamily_Delegate delegate = getDelegate(builderPtr); 362 int italic = isItalic ? 1 : 0; 363 if (delegate != null) { 364 if (sFontLocation == null) { 365 delegate.mPostInitRunnables.add(() -> delegate.addFont(path, weight, italic)); 366 return true; 367 } 368 return delegate.addFont(path, weight, italic); 369 } 370 return false; 371 } 372 373 @LayoutlibDelegate 374 /*package*/ static boolean nAddFontFromAssetManager(long builderPtr, AssetManager mgr, String path, 375 int cookie, boolean isAsset, int ttcIndex, int weight, int isItalic) { 376 FontFamily_Delegate ffd = sManager.getDelegate(builderPtr); 377 if (ffd == null) { 378 return false; 379 } 380 ffd.mValid = true; 381 if (mgr == null) { 382 return false; 383 } 384 if (mgr instanceof BridgeAssetManager) { 385 InputStream fontStream = null; 386 try { 387 AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository(); 388 if (assetRepository == null) { 389 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 390 null); 391 return false; 392 } 393 if (!assetRepository.isSupported()) { 394 // Don't log any warnings on unsupported IDEs. 395 return false; 396 } 397 // Check cache 398 FontInfo fontInfo = sCache.get(path); 399 if (fontInfo != null) { 400 // renew the font's lease. 401 sCache.put(path, fontInfo); 402 ffd.addFont(fontInfo); 403 return true; 404 } 405 fontStream = isAsset ? 406 assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING) : 407 assetRepository.openNonAsset(cookie, path, AssetManager.ACCESS_STREAMING); 408 if (fontStream == null) { 409 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 410 path); 411 return false; 412 } 413 Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream); 414 fontInfo = new FontInfo(); 415 fontInfo.mFont = font; 416 if (weight == RESOLVE_BY_FONT_TABLE) { 417 fontInfo.mWeight = FontUtilities.getFont2D(font).getWeight(); 418 } else { 419 fontInfo.mWeight = weight; 420 } 421 if (isItalic == RESOLVE_BY_FONT_TABLE) { 422 fontInfo.mIsItalic = 423 (FontUtilities.getFont2D(font).getStyle() & Font.ITALIC) != 0; 424 } else { 425 fontInfo.mIsItalic = isItalic == 1; 426 } 427 ffd.addFont(fontInfo); 428 return true; 429 } catch (IOException e) { 430 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e, 431 path); 432 } catch (FontFormatException e) { 433 if (path.endsWith(EXTENSION_OTF)) { 434 // otf fonts are not supported on the user's config (JRE version + OS) 435 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 436 "OpenType fonts are not supported yet: " + path, null, path); 437 } else { 438 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 439 "Unable to load font " + path, e, path); 440 } 441 } finally { 442 if (fontStream != null) { 443 try { 444 fontStream.close(); 445 } catch (IOException ignored) { 446 } 447 } 448 } 449 return false; 450 } 451 // This should never happen. AssetManager is a final class (from user's perspective), and 452 // we've replaced every creation of AssetManager with our implementation. We create an 453 // exception and log it, but continue with rest of the rendering, without loading this font. 454 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 455 "You have found a bug in the rendering library. Please file a bug at b.android.com.", 456 new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"), 457 null); 458 return false; 459 } 460 461 @LayoutlibDelegate 462 /*package*/ static long nGetBuilderReleaseFunc() { 463 // Layoutlib uses the same reference for the builder and the font family, 464 // so it should not release that reference at the builder stage. 465 return -1; 466 } 467 468 // ---- private helper methods ---- 469 470 private void init() { 471 for (Runnable postInitRunnable : mPostInitRunnables) { 472 postInitRunnable.run(); 473 } 474 mPostInitRunnables = null; 475 } 476 477 private boolean addFont(final String path, int ttcIndex, int weight, int italic) { 478 // FIXME: support ttc fonts. Hack JRE?? 479 if (sFontLocation == null) { 480 mPostInitRunnables.add(() -> addFont(path, weight, italic)); 481 return true; 482 } 483 return addFont(path, weight, italic); 484 } 485 486 private boolean addFont(@NonNull String path) { 487 return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC) ? 1 : RESOLVE_BY_FONT_TABLE); 488 } 489 490 private boolean addFont(@NonNull String path, int weight, int italic) { 491 if (path.startsWith(SYSTEM_FONTS) && 492 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) { 493 Logger.getLogger(FontFamily_Delegate.class.getSimpleName()).warning("Unable to load font " + path); 494 return mValid = false; 495 } 496 // Set valid to true, even if the font fails to load. 497 mValid = true; 498 Font font = loadFont(path); 499 if (font == null) { 500 return false; 501 } 502 FontInfo fontInfo = new FontInfo(); 503 fontInfo.mFont = font; 504 fontInfo.mWeight = weight; 505 fontInfo.mIsItalic = italic == RESOLVE_BY_FONT_TABLE ? font.isItalic() : italic == 1; 506 addFont(fontInfo); 507 return true; 508 } 509 510 private boolean addFont(@NonNull FontInfo fontInfo) { 511 return mFonts.putIfAbsent(fontInfo, fontInfo.mFont) == null; 512 } 513 514 /** 515 * Compute matching metric between two styles - 0 is an exact match. 516 */ 517 public static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) { 518 int score = Math.abs(font1.mWeight / 100 - font2.mWeight / 100); 519 if (font1.mIsItalic != font2.mIsItalic) { 520 score += 2; 521 } 522 return score; 523 } 524 525 /** 526 * Try to derive a font from {@code srcFont} for the style in {@code outFont}. 527 * <p/> 528 * {@code outFont} is updated to reflect the style of the derived font. 529 * @param srcFont the source font 530 * @param outFont contains the desired font style. Updated to contain the derived font and 531 * its style 532 */ 533 public static void deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) { 534 int desiredWeight = outFont.mWeight; 535 int srcWeight = srcFont.mWeight; 536 assert srcFont.mFont != null; 537 Font derivedFont = srcFont.mFont; 538 int derivedStyle = 0; 539 // Embolden the font if required. 540 if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) { 541 derivedStyle |= Font.BOLD; 542 srcWeight += BOLD_FONT_WEIGHT_DELTA; 543 } 544 // Italicize the font if required. 545 if (outFont.mIsItalic && !srcFont.mIsItalic) { 546 derivedStyle |= Font.ITALIC; 547 } else if (outFont.mIsItalic != srcFont.mIsItalic) { 548 // The desired font is plain, but the src font is italics. We can't convert it back. So 549 // we update the value to reflect the true style of the font we're deriving. 550 outFont.mIsItalic = srcFont.mIsItalic; 551 } 552 553 if (derivedStyle != 0) { 554 derivedFont = derivedFont.deriveFont(derivedStyle); 555 } 556 557 outFont.mFont = derivedFont; 558 outFont.mWeight = srcWeight; 559 // No need to update mIsItalics, as it's already been handled above. 560 } 561 } 562