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