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