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