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