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