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.util.ArrayList; 37 import java.util.Collections; 38 import java.util.HashSet; 39 import java.util.LinkedHashMap; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Map.Entry; 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(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 for (FontInfo font : mFonts) { 182 int match = computeMatch(font, desiredStyle); 183 if (match < bestMatch) { 184 bestMatch = match; 185 bestFont = font; 186 } 187 } 188 if (bestFont == null) { 189 return null; 190 } 191 if (bestMatch == 0) { 192 return bestFont.mFont; 193 } 194 // Derive the font as required and add it to the list of Fonts. 195 deriveFont(bestFont, desiredStyle); 196 addFont(desiredStyle); 197 return desiredStyle.mFont; 198 } 199 getVariant()200 public FontVariant getVariant() { 201 return mVariant; 202 } 203 204 /** 205 * Returns if the FontFamily should contain any fonts. If this returns true and 206 * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while 207 * loading the fonts. However, some fonts are deliberately skipped, for example they are not 208 * bundled with the SDK. In such a case, this method returns false. 209 */ isValid()210 public boolean isValid() { 211 return mValid; 212 } 213 loadFont(String path)214 /*package*/ static Font loadFont(String path) { 215 if (path.startsWith(SYSTEM_FONTS) ) { 216 String relativePath = path.substring(SYSTEM_FONTS.length()); 217 File f = new File(sFontLocation, relativePath); 218 219 try { 220 return Font.createFont(Font.TRUETYPE_FONT, f); 221 } catch (Exception e) { 222 if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) { 223 // If we aren't able to load an Open Type font, don't log a warning just yet. 224 // We wait for a case where font is being used. Only then we try to log the 225 // warning. 226 return null; 227 } 228 Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, 229 String.format("Unable to load font %1$s", relativePath), 230 e, null); 231 } 232 } else { 233 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 234 "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.", 235 null, null); 236 } 237 238 return null; 239 } 240 241 @Nullable getFontLocation()242 /*package*/ static String getFontLocation() { 243 return sFontLocation; 244 } 245 246 // ---- native methods ---- 247 248 @LayoutlibDelegate nCreateFamily(String lang, int variant)249 /*package*/ static long nCreateFamily(String lang, int variant) { 250 // TODO: support lang. This is required for japanese locale. 251 FontFamily_Delegate delegate = new FontFamily_Delegate(); 252 // variant can be 0, 1 or 2. 253 assert variant < 3; 254 delegate.mVariant = FontVariant.values()[variant]; 255 if (sFontLocation != null) { 256 delegate.init(); 257 } else { 258 sPostInitDelegate.add(delegate); 259 } 260 return sManager.addNewDelegate(delegate); 261 } 262 263 @LayoutlibDelegate 264 /*package*/ static void nUnrefFamily(long nativePtr) { 265 // Removing the java reference for the object doesn't mean that it's freed for garbage 266 // collection. Typeface_Delegate may still hold a reference for it. 267 sManager.removeJavaReferenceFor(nativePtr); 268 } 269 270 @LayoutlibDelegate 271 /*package*/ static boolean nAddFont(long nativeFamily, final String path) { 272 final FontFamily_Delegate delegate = getDelegate(nativeFamily); 273 if (delegate != null) { 274 if (sFontLocation == null) { 275 delegate.mPostInitRunnables.add(new Runnable() { 276 @Override 277 public void run() { 278 delegate.addFont(path); 279 } 280 }); 281 return true; 282 } 283 return delegate.addFont(path); 284 } 285 return false; 286 } 287 288 @LayoutlibDelegate 289 /*package*/ static boolean nAddFontWeightStyle(long nativeFamily, final String path, 290 final int weight, final boolean isItalic) { 291 final FontFamily_Delegate delegate = getDelegate(nativeFamily); 292 if (delegate != null) { 293 if (sFontLocation == null) { 294 delegate.mPostInitRunnables.add(new Runnable() { 295 @Override 296 public void run() { 297 delegate.addFont(path, weight, isItalic); 298 } 299 }); 300 return true; 301 } 302 return delegate.addFont(path, weight, isItalic); 303 } 304 return false; 305 } 306 307 @LayoutlibDelegate 308 /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) { 309 FontFamily_Delegate ffd = sManager.getDelegate(nativeFamily); 310 ffd.mValid = true; 311 if (mgr == null) { 312 return false; 313 } 314 if (mgr instanceof BridgeAssetManager) { 315 InputStream fontStream = null; 316 try { 317 AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository(); 318 if (assetRepository == null) { 319 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 320 null); 321 return false; 322 } 323 if (!assetRepository.isSupported()) { 324 // Don't log any warnings on unsupported IDEs. 325 return false; 326 } 327 // Check cache 328 FontInfo fontInfo = sCache.get(path); 329 if (fontInfo != null) { 330 // renew the font's lease. 331 sCache.put(path, fontInfo); 332 ffd.addFont(fontInfo); 333 return true; 334 } 335 fontStream = assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING); 336 if (fontStream == null) { 337 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 338 path); 339 return false; 340 } 341 Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream); 342 fontInfo = new FontInfo(); 343 fontInfo.mFont = font; 344 fontInfo.mWeight = font.isBold() ? BOLD_FONT_WEIGHT : DEFAULT_FONT_WEIGHT; 345 fontInfo.mIsItalic = font.isItalic(); 346 ffd.addFont(fontInfo); 347 return true; 348 } catch (IOException e) { 349 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e, 350 path); 351 } catch (FontFormatException e) { 352 if (path.endsWith(EXTENSION_OTF)) { 353 // otf fonts are not supported on the user's config (JRE version + OS) 354 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 355 "OpenType fonts are not supported yet: " + path, null, path); 356 } else { 357 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 358 "Unable to load font " + path, e, path); 359 } 360 } finally { 361 if (fontStream != null) { 362 try { 363 fontStream.close(); 364 } catch (IOException ignored) { 365 } 366 } 367 } 368 return false; 369 } 370 // This should never happen. AssetManager is a final class (from user's perspective), and 371 // we've replaced every creation of AssetManager with our implementation. We create an 372 // exception and log it, but continue with rest of the rendering, without loading this font. 373 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 374 "You have found a bug in the rendering library. Please file a bug at b.android.com.", 375 new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"), 376 null); 377 return false; 378 } 379 380 381 // ---- private helper methods ---- 382 383 private void init() { 384 for (Runnable postInitRunnable : mPostInitRunnables) { 385 postInitRunnable.run(); 386 } 387 mPostInitRunnables = null; 388 } 389 390 private boolean addFont(@NonNull String path) { 391 return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC)); 392 } 393 394 private boolean addFont(@NonNull String path, int weight, boolean isItalic) { 395 if (path.startsWith(SYSTEM_FONTS) && 396 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) { 397 return mValid = false; 398 } 399 // Set valid to true, even if the font fails to load. 400 mValid = true; 401 Font font = loadFont(path); 402 if (font == null) { 403 return false; 404 } 405 FontInfo fontInfo = new FontInfo(); 406 fontInfo.mFont = font; 407 fontInfo.mWeight = weight; 408 fontInfo.mIsItalic = isItalic; 409 addFont(fontInfo); 410 return true; 411 } 412 413 private boolean addFont(@NonNull FontInfo fontInfo) { 414 int weight = fontInfo.mWeight; 415 boolean isItalic = fontInfo.mIsItalic; 416 // The list is usually just two fonts big. So iterating over all isn't as bad as it looks. 417 // It's biggest for roboto where the size is 12. 418 for (FontInfo font : mFonts) { 419 if (font.mWeight == weight && font.mIsItalic == isItalic) { 420 return false; 421 } 422 } 423 mFonts.add(fontInfo); 424 return true; 425 } 426 427 /** 428 * Compute matching metric between two styles - 0 is an exact match. 429 */ 430 private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) { 431 int score = Math.abs(font1.mWeight - font2.mWeight); 432 if (font1.mIsItalic != font2.mIsItalic) { 433 score += 200; 434 } 435 return score; 436 } 437 438 /** 439 * Try to derive a font from {@code srcFont} for the style in {@code outFont}. 440 * <p/> 441 * {@code outFont} is updated to reflect the style of the derived font. 442 * @param srcFont the source font 443 * @param outFont contains the desired font style. Updated to contain the derived font and 444 * its style 445 * @return outFont 446 */ 447 @NonNull 448 private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) { 449 int desiredWeight = outFont.mWeight; 450 int srcWeight = srcFont.mWeight; 451 Font derivedFont = srcFont.mFont; 452 // Embolden the font if required. 453 if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) { 454 derivedFont = derivedFont.deriveFont(Font.BOLD); 455 srcWeight += BOLD_FONT_WEIGHT_DELTA; 456 } 457 // Italicize the font if required. 458 if (outFont.mIsItalic && !srcFont.mIsItalic) { 459 derivedFont = derivedFont.deriveFont(Font.ITALIC); 460 } else if (outFont.mIsItalic != srcFont.mIsItalic) { 461 // The desired font is plain, but the src font is italics. We can't convert it back. So 462 // we update the value to reflect the true style of the font we're deriving. 463 outFont.mIsItalic = srcFont.mIsItalic; 464 } 465 outFont.mFont = derivedFont; 466 outFont.mWeight = srcWeight; 467 // No need to update mIsItalics, as it's already been handled above. 468 return outFont; 469 } 470 } 471