Home | History | Annotate | Download | only in graphics
      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 
    137     public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
    138         return sManager.getDelegate(nativeFontFamily);
    139     }
    140 
    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
    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 
    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      */
    210     public boolean isValid() {
    211         return mValid;
    212     }
    213 
    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
    242     /*package*/ static String getFontLocation() {
    243         return sFontLocation;
    244     }
    245 
    246     // ---- native methods ----
    247 
    248     @LayoutlibDelegate
    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