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 
31 import java.awt.Font;
32 import java.awt.FontFormatException;
33 import java.io.File;
34 import java.io.FileNotFoundException;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.nio.ByteBuffer;
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.HashSet;
41 import java.util.LinkedHashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
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         @Override
equals(Object o)102         public boolean equals(Object o) {
103             if (this == o) {
104                 return true;
105             }
106             if (o == null || getClass() != o.getClass()) {
107                 return false;
108             }
109             FontInfo fontInfo = (FontInfo) o;
110             return mWeight == fontInfo.mWeight && mIsItalic == fontInfo.mIsItalic;
111         }
112 
113         @Override
hashCode()114         public int hashCode() {
115             return Objects.hash(mWeight, mIsItalic);
116         }
117 
118         @Override
toString()119         public String toString() {
120             return "FontInfo{" + "mWeight=" + mWeight + ", mIsItalic=" + mIsItalic + '}';
121         }
122     }
123 
124     // ---- delegate manager ----
125     private static final DelegateManager<FontFamily_Delegate> sManager =
126             new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class);
127 
128     // ---- delegate helper data ----
129     private static String sFontLocation;
130     private static final List<FontFamily_Delegate> sPostInitDelegate = new
131             ArrayList<FontFamily_Delegate>();
132     private static Set<String> SDK_FONTS;
133 
134 
135     // ---- delegate data ----
136 
137     // Order does not really matter but we use a LinkedHashMap to get reproducible results across
138     // render calls
139     private Map<FontInfo, Font> mFonts = new LinkedHashMap<>();
140 
141     /**
142      * The variant of the Font Family - compact or elegant.
143      * <p/>
144      * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in
145      * android.graphics.FontFamily
146      *
147      * @see Paint#setElegantTextHeight(boolean)
148      */
149     private FontVariant mVariant;
150     // List of runnables to process fonts after sFontLoader is initialized.
151     private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>();
152     /** @see #isValid() */
153     private boolean mValid = false;
154 
155 
156     // ---- Public helper class ----
157 
158     public enum FontVariant {
159         // The order needs to be kept in sync with android.graphics.FontFamily.
160         NONE, COMPACT, ELEGANT
161     }
162 
163     // ---- Public Helper methods ----
164 
getDelegate(long nativeFontFamily)165     public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
166         return sManager.getDelegate(nativeFontFamily);
167     }
168 
setFontLocation(String fontLocation)169     public static synchronized void setFontLocation(String fontLocation) {
170         sFontLocation = fontLocation;
171         // init list of bundled fonts.
172         File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST);
173         // Current number of fonts is 103. Use the next round number to leave scope for more fonts
174         // in the future.
175         Set<String> allFontsList = new HashSet<>(128);
176         Scanner scanner = null;
177         try {
178             scanner = new Scanner(allFonts);
179             while (scanner.hasNext()) {
180                 String name = scanner.next();
181                 // Skip font configuration files.
182                 if (!name.endsWith(".xml")) {
183                     allFontsList.add(name);
184                 }
185             }
186         } catch (FileNotFoundException e) {
187             Bridge.getLog().error(LayoutLog.TAG_BROKEN,
188                     "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.",
189                     e, null);
190         } finally {
191             if (scanner != null) {
192                 scanner.close();
193             }
194         }
195         SDK_FONTS = Collections.unmodifiableSet(allFontsList);
196         for (FontFamily_Delegate fontFamily : sPostInitDelegate) {
197             fontFamily.init();
198         }
199         sPostInitDelegate.clear();
200     }
201 
202     @Nullable
getFont(int desiredWeight, boolean isItalic)203     public Font getFont(int desiredWeight, boolean isItalic) {
204         FontInfo desiredStyle = new FontInfo();
205         desiredStyle.mWeight = desiredWeight;
206         desiredStyle.mIsItalic = isItalic;
207 
208         Font cachedFont = mFonts.get(desiredStyle);
209         if (cachedFont != null) {
210             return cachedFont;
211         }
212 
213         FontInfo bestFont = null;
214 
215         if (mFonts.size() == 1) {
216             // No need to compute the match since we only have one candidate
217             bestFont = mFonts.keySet().iterator().next();
218         } else {
219             int bestMatch = Integer.MAX_VALUE;
220 
221             //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
222             for (FontInfo font : mFonts.keySet()) {
223                 int match = computeMatch(font, desiredStyle);
224                 if (match < bestMatch) {
225                     bestMatch = match;
226                     bestFont = font;
227                 }
228             }
229 
230             // This would mean that we already had the font so it should be in the set
231             assert bestMatch != 0;
232         }
233 
234         if (bestFont == null) {
235             return null;
236         }
237 
238 
239         // Derive the font as required and add it to the list of Fonts.
240         deriveFont(bestFont, desiredStyle);
241         addFont(desiredStyle);
242         return desiredStyle.mFont;
243     }
244 
getVariant()245     public FontVariant getVariant() {
246         return mVariant;
247     }
248 
249     /**
250      * Returns if the FontFamily should contain any fonts. If this returns true and
251      * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while
252      * loading the fonts. However, some fonts are deliberately skipped, for example they are not
253      * bundled with the SDK. In such a case, this method returns false.
254      */
isValid()255     public boolean isValid() {
256         return mValid;
257     }
258 
loadFont(String path)259     private static Font loadFont(String path) {
260         if (path.startsWith(SYSTEM_FONTS) ) {
261             String relativePath = path.substring(SYSTEM_FONTS.length());
262             File f = new File(sFontLocation, relativePath);
263 
264             try {
265                 return Font.createFont(Font.TRUETYPE_FONT, f);
266             } catch (Exception e) {
267                 if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) {
268                     // If we aren't able to load an Open Type font, don't log a warning just yet.
269                     // We wait for a case where font is being used. Only then we try to log the
270                     // warning.
271                     return null;
272                 }
273                 Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
274                         String.format("Unable to load font %1$s", relativePath),
275                         e, null);
276             }
277         } else {
278             Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
279                     "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.",
280                     null, null);
281         }
282 
283         return null;
284     }
285 
286     @Nullable
getFontLocation()287     /*package*/ static String getFontLocation() {
288         return sFontLocation;
289     }
290 
291     // ---- delegate methods ----
292     @LayoutlibDelegate
addFont(FontFamily thisFontFamily, String path, int ttcIndex, FontVariationAxis[] axes, int weight, int italic)293     /*package*/ static boolean addFont(FontFamily thisFontFamily, String path, int ttcIndex,
294             FontVariationAxis[] axes, int weight, int italic) {
295         if (thisFontFamily.mBuilderPtr == 0) {
296             assert false : "Unable to call addFont after freezing.";
297             return false;
298         }
299         final FontFamily_Delegate delegate = getDelegate(thisFontFamily.mBuilderPtr);
300         return delegate != null && delegate.addFont(path, ttcIndex, weight, italic);
301     }
302 
303     // ---- native methods ----
304 
305     @LayoutlibDelegate
nInitBuilder(String lang, int variant)306     /*package*/ static long nInitBuilder(String lang, int variant) {
307         // TODO: support lang. This is required for japanese locale.
308         FontFamily_Delegate delegate = new FontFamily_Delegate();
309         // variant can be 0, 1 or 2.
310         assert variant < 3;
311         delegate.mVariant = FontVariant.values()[variant];
312         if (sFontLocation != null) {
313             delegate.init();
314         } else {
315             sPostInitDelegate.add(delegate);
316         }
317         return sManager.addNewDelegate(delegate);
318     }
319 
320     @LayoutlibDelegate
321     /*package*/ static long nCreateFamily(long builderPtr) {
322         return builderPtr;
323     }
324 
325     @LayoutlibDelegate
326     /*package*/ static void nUnrefFamily(long nativePtr) {
327         // Removing the java reference for the object doesn't mean that it's freed for garbage
328         // collection. Typeface_Delegate may still hold a reference for it.
329         sManager.removeJavaReferenceFor(nativePtr);
330     }
331 
332     @LayoutlibDelegate
333     /*package*/ static boolean nAddFont(long builderPtr, ByteBuffer font, int ttcIndex,
334             int weight, int isItalic) {
335         assert false : "The only client of this method has been overridden.";
336         return false;
337     }
338 
339     @LayoutlibDelegate
340     /*package*/ static boolean nAddFontWeightStyle(long builderPtr, ByteBuffer font,
341             int ttcIndex, int weight, int isItalic) {
342         assert false : "The only client of this method has been overridden.";
343         return false;
344     }
345 
346     @LayoutlibDelegate
347     /*package*/ static void nAddAxisValue(long builderPtr, int tag, float value) {
348         assert false : "The only client of this method has been overridden.";
349     }
350 
351     static boolean addFont(long builderPtr, final String path, final int weight,
352             final boolean isItalic) {
353         final FontFamily_Delegate delegate = getDelegate(builderPtr);
354         int italic = isItalic ? 1 : 0;
355         if (delegate != null) {
356             if (sFontLocation == null) {
357                 delegate.mPostInitRunnables.add(() -> delegate.addFont(path, weight, italic));
358                 return true;
359             }
360             return delegate.addFont(path, weight, italic);
361         }
362         return false;
363     }
364 
365     @LayoutlibDelegate
366     /*package*/ static boolean nAddFontFromAssetManager(long builderPtr, AssetManager mgr, String path,
367             int cookie, boolean isAsset, int ttcIndex, int weight, int isItalic) {
368         FontFamily_Delegate ffd = sManager.getDelegate(builderPtr);
369         if (ffd == null) {
370             return false;
371         }
372         ffd.mValid = true;
373         if (mgr == null) {
374             return false;
375         }
376         if (mgr instanceof BridgeAssetManager) {
377             InputStream fontStream = null;
378             try {
379                 AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository();
380                 if (assetRepository == null) {
381                     Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
382                             null);
383                     return false;
384                 }
385                 if (!assetRepository.isSupported()) {
386                     // Don't log any warnings on unsupported IDEs.
387                     return false;
388                 }
389                 // Check cache
390                 FontInfo fontInfo = sCache.get(path);
391                 if (fontInfo != null) {
392                     // renew the font's lease.
393                     sCache.put(path, fontInfo);
394                     ffd.addFont(fontInfo);
395                     return true;
396                 }
397                 fontStream = isAsset ?
398                         assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING) :
399                         assetRepository.openNonAsset(cookie, path, AssetManager.ACCESS_STREAMING);
400                 if (fontStream == null) {
401                     Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
402                             path);
403                     return false;
404                 }
405                 Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream);
406                 fontInfo = new FontInfo();
407                 fontInfo.mFont = font;
408                 if (weight == RESOLVE_BY_FONT_TABLE) {
409                     fontInfo.mWeight = font.isBold() ? BOLD_FONT_WEIGHT : DEFAULT_FONT_WEIGHT;
410                 } else {
411                     fontInfo.mWeight = weight;
412                 }
413                 fontInfo.mIsItalic = isItalic == RESOLVE_BY_FONT_TABLE ? font.isItalic() :
414                         isItalic == 1;
415                 ffd.addFont(fontInfo);
416                 return true;
417             } catch (IOException e) {
418                 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e,
419                         path);
420             } catch (FontFormatException e) {
421                 if (path.endsWith(EXTENSION_OTF)) {
422                     // otf fonts are not supported on the user's config (JRE version + OS)
423                     Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
424                             "OpenType fonts are not supported yet: " + path, null, path);
425                 } else {
426                     Bridge.getLog().error(LayoutLog.TAG_BROKEN,
427                             "Unable to load font " + path, e, path);
428                 }
429             } finally {
430                 if (fontStream != null) {
431                     try {
432                         fontStream.close();
433                     } catch (IOException ignored) {
434                     }
435                 }
436             }
437             return false;
438         }
439         // This should never happen. AssetManager is a final class (from user's perspective), and
440         // we've replaced every creation of AssetManager with our implementation. We create an
441         // exception and log it, but continue with rest of the rendering, without loading this font.
442         Bridge.getLog().error(LayoutLog.TAG_BROKEN,
443                 "You have found a bug in the rendering library. Please file a bug at b.android.com.",
444                 new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"),
445                 null);
446         return false;
447     }
448 
449     @LayoutlibDelegate
450     /*package*/ static void nAbort(long builderPtr) {
451         sManager.removeJavaReferenceFor(builderPtr);
452     }
453 
454     // ---- private helper methods ----
455 
456     private void init() {
457         for (Runnable postInitRunnable : mPostInitRunnables) {
458             postInitRunnable.run();
459         }
460         mPostInitRunnables = null;
461     }
462 
463     private boolean addFont(final String path, int ttcIndex, int weight, int italic) {
464         // FIXME: support ttc fonts. Hack JRE??
465         if (sFontLocation == null) {
466             mPostInitRunnables.add(() -> addFont(path, weight, italic));
467             return true;
468         }
469         return addFont(path, weight, italic);
470     }
471 
472      private boolean addFont(@NonNull String path) {
473          return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC) ? 1 : RESOLVE_BY_FONT_TABLE);
474      }
475 
476     private boolean addFont(@NonNull String path, int weight, int italic) {
477         if (path.startsWith(SYSTEM_FONTS) &&
478                 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) {
479             return mValid = false;
480         }
481         // Set valid to true, even if the font fails to load.
482         mValid = true;
483         Font font = loadFont(path);
484         if (font == null) {
485             return false;
486         }
487         FontInfo fontInfo = new FontInfo();
488         fontInfo.mFont = font;
489         fontInfo.mWeight = weight;
490         fontInfo.mIsItalic = italic == RESOLVE_BY_FONT_TABLE ? font.isItalic() : italic == 1;
491         addFont(fontInfo);
492         return true;
493     }
494 
495     private boolean addFont(@NonNull FontInfo fontInfo) {
496         return mFonts.putIfAbsent(fontInfo, fontInfo.mFont) == null;
497     }
498 
499     /**
500      * Compute matching metric between two styles - 0 is an exact match.
501      */
502     private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) {
503         int score = Math.abs(font1.mWeight - font2.mWeight);
504         if (font1.mIsItalic != font2.mIsItalic) {
505             score += 200;
506         }
507         return score;
508     }
509 
510     /**
511      * Try to derive a font from {@code srcFont} for the style in {@code outFont}.
512      * <p/>
513      * {@code outFont} is updated to reflect the style of the derived font.
514      * @param srcFont the source font
515      * @param outFont contains the desired font style. Updated to contain the derived font and
516      *                its style
517      * @return outFont
518      */
519     private void deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) {
520         int desiredWeight = outFont.mWeight;
521         int srcWeight = srcFont.mWeight;
522         assert srcFont.mFont != null;
523         Font derivedFont = srcFont.mFont;
524         int derivedStyle = 0;
525         // Embolden the font if required.
526         if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) {
527             derivedStyle |= Font.BOLD;
528             srcWeight += BOLD_FONT_WEIGHT_DELTA;
529         }
530         // Italicize the font if required.
531         if (outFont.mIsItalic && !srcFont.mIsItalic) {
532             derivedStyle |= Font.ITALIC;
533         } else if (outFont.mIsItalic != srcFont.mIsItalic) {
534             // The desired font is plain, but the src font is italics. We can't convert it back. So
535             // we update the value to reflect the true style of the font we're deriving.
536             outFont.mIsItalic = srcFont.mIsItalic;
537         }
538 
539         if (derivedStyle != 0) {
540             derivedFont = derivedFont.deriveFont(derivedStyle);
541         }
542 
543         outFont.mFont = derivedFont;
544         outFont.mWeight = srcWeight;
545         // No need to update mIsItalics, as it's already been handled above.
546     }
547 }
548