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