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.annotations.NonNull;
20 import com.android.annotations.Nullable;
21 import com.android.ide.common.rendering.api.LayoutLog;
22 import com.android.layoutlib.bridge.Bridge;
23 import com.android.layoutlib.bridge.impl.DelegateManager;
24 import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
25 
26 import android.content.res.AssetManager;
27 
28 import java.awt.Font;
29 import java.awt.FontFormatException;
30 import java.io.File;
31 import java.io.FileNotFoundException;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Scanner;
37 import java.util.Set;
38 
39 import static android.graphics.Typeface_Delegate.SYSTEM_FONTS;
40 
41 /**
42  * Delegate implementing the native methods of android.graphics.FontFamily
43  *
44  * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced
45  * by calls to methods of the same name in this delegate class.
46  *
47  * This class behaves like the original native implementation, but in Java, keeping previously
48  * native data into its own objects and mapping them to int that are sent back and forth between
49  * it and the original FontFamily class.
50  *
51  * @see DelegateManager
52  */
53 public class FontFamily_Delegate {
54 
55     public static final int DEFAULT_FONT_WEIGHT = 400;
56     public static final int BOLD_FONT_WEIGHT_DELTA = 300;
57     public static final int BOLD_FONT_WEIGHT = 700;
58 
59     // FONT_SUFFIX_ITALIC will always match FONT_SUFFIX_BOLDITALIC and hence it must be checked
60     // separately.
61     private static final String FONT_SUFFIX_ITALIC = "Italic.ttf";
62     private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt";
63 
64     /**
65      * A class associating {@link Font} with its metadata.
66      */
67     private static final class FontInfo {
68         @Nullable
69         Font mFont;
70         int mWeight;
71         boolean mIsItalic;
72     }
73 
74     // ---- delegate manager ----
75     private static final DelegateManager<FontFamily_Delegate> sManager =
76             new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class);
77 
78     // ---- delegate helper data ----
79     private static String sFontLocation;
80     private static final List<FontFamily_Delegate> sPostInitDelegate = new
81             ArrayList<FontFamily_Delegate>();
82     private static Set<String> SDK_FONTS;
83 
84 
85     // ---- delegate data ----
86     private List<FontInfo> mFonts = new ArrayList<FontInfo>();
87 
88     /**
89      * The variant of the Font Family - compact or elegant.
90      * <p/>
91      * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in
92      * android.graphics.FontFamily
93      *
94      * @see Paint#setElegantTextHeight(boolean)
95      */
96     private FontVariant mVariant;
97     // List of runnables to process fonts after sFontLoader is initialized.
98     private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>();
99     /** @see #isValid() */
100     private boolean mValid = false;
101 
102 
103     // ---- Public helper class ----
104 
105     public enum FontVariant {
106         // The order needs to be kept in sync with android.graphics.FontFamily.
107         NONE, COMPACT, ELEGANT
108     }
109 
110     // ---- Public Helper methods ----
111 
getDelegate(long nativeFontFamily)112     public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
113         return sManager.getDelegate(nativeFontFamily);
114     }
115 
setFontLocation(String fontLocation)116     public static synchronized void setFontLocation(String fontLocation) {
117         sFontLocation = fontLocation;
118         // init list of bundled fonts.
119         File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST);
120         // Current number of fonts is 103. Use the next round number to leave scope for more fonts
121         // in the future.
122         Set<String> allFontsList = new HashSet<String>(128);
123         Scanner scanner = null;
124         try {
125             scanner = new Scanner(allFonts);
126             while (scanner.hasNext()) {
127                 String name = scanner.next();
128                 // Skip font configuration files.
129                 if (!name.endsWith(".xml")) {
130                     allFontsList.add(name);
131                 }
132             }
133         } catch (FileNotFoundException e) {
134             Bridge.getLog().error(LayoutLog.TAG_BROKEN,
135                     "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.",
136                     e, null);
137         } finally {
138             if (scanner != null) {
139                 scanner.close();
140             }
141         }
142         SDK_FONTS = Collections.unmodifiableSet(allFontsList);
143         for (FontFamily_Delegate fontFamily : sPostInitDelegate) {
144             fontFamily.init();
145         }
146         sPostInitDelegate.clear();
147     }
148 
149     @Nullable
getFont(int desiredWeight, boolean isItalic)150     public Font getFont(int desiredWeight, boolean isItalic) {
151         FontInfo desiredStyle = new FontInfo();
152         desiredStyle.mWeight = desiredWeight;
153         desiredStyle.mIsItalic = isItalic;
154         FontInfo bestFont = null;
155         int bestMatch = Integer.MAX_VALUE;
156         for (FontInfo font : mFonts) {
157             int match = computeMatch(font, desiredStyle);
158             if (match < bestMatch) {
159                 bestMatch = match;
160                 bestFont = font;
161             }
162         }
163         if (bestFont == null) {
164             return null;
165         }
166         if (bestMatch == 0) {
167             return bestFont.mFont;
168         }
169         // Derive the font as required and add it to the list of Fonts.
170         deriveFont(bestFont, desiredStyle);
171         addFont(desiredStyle);
172         return desiredStyle.mFont;
173     }
174 
getVariant()175     public FontVariant getVariant() {
176         return mVariant;
177     }
178 
179     /**
180      * Returns if the FontFamily should contain any fonts. If this returns true and
181      * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while
182      * loading the fonts. However, some fonts are deliberately skipped, for example they are not
183      * bundled with the SDK. In such a case, this method returns false.
184      */
isValid()185     public boolean isValid() {
186         return mValid;
187     }
188 
loadFont(String path)189     /*package*/ static Font loadFont(String path) {
190         if (path.startsWith(SYSTEM_FONTS) ) {
191             String relativePath = path.substring(SYSTEM_FONTS.length());
192             File f = new File(sFontLocation, relativePath);
193 
194             try {
195                 return Font.createFont(Font.TRUETYPE_FONT, f);
196             } catch (Exception e) {
197                 if (path.endsWith(".otf") && e instanceof FontFormatException) {
198                     // If we aren't able to load an Open Type font, don't log a warning just yet.
199                     // We wait for a case where font is being used. Only then we try to log the
200                     // warning.
201                     return null;
202                 }
203                 Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
204                         String.format("Unable to load font %1$s", relativePath),
205                         e, null);
206             }
207         } else {
208             Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
209                     "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.",
210                     null, null);
211         }
212 
213         return null;
214     }
215 
216     @Nullable
getFontLocation()217     /*package*/ static String getFontLocation() {
218         return sFontLocation;
219     }
220 
221     // ---- native methods ----
222 
223     @LayoutlibDelegate
nCreateFamily(String lang, int variant)224     /*package*/ static long nCreateFamily(String lang, int variant) {
225         // TODO: support lang. This is required for japanese locale.
226         FontFamily_Delegate delegate = new FontFamily_Delegate();
227         // variant can be 0, 1 or 2.
228         assert variant < 3;
229         delegate.mVariant = FontVariant.values()[variant];
230         if (sFontLocation != null) {
231             delegate.init();
232         } else {
233             sPostInitDelegate.add(delegate);
234         }
235         return sManager.addNewDelegate(delegate);
236     }
237 
238     @LayoutlibDelegate
239     /*package*/ static void nUnrefFamily(long nativePtr) {
240         // Removing the java reference for the object doesn't mean that it's freed for garbage
241         // collection. Typeface_Delegate may still hold a reference for it.
242         sManager.removeJavaReferenceFor(nativePtr);
243     }
244 
245     @LayoutlibDelegate
246     /*package*/ static boolean nAddFont(long nativeFamily, final String path) {
247         final FontFamily_Delegate delegate = getDelegate(nativeFamily);
248         if (delegate != null) {
249             if (sFontLocation == null) {
250                 delegate.mPostInitRunnables.add(new Runnable() {
251                     @Override
252                     public void run() {
253                         delegate.addFont(path);
254                     }
255                 });
256                 return true;
257             }
258             return delegate.addFont(path);
259         }
260         return false;
261     }
262 
263     @LayoutlibDelegate
264     /*package*/ static boolean nAddFontWeightStyle(long nativeFamily, final String path,
265             final int weight, final boolean isItalic) {
266         final FontFamily_Delegate delegate = getDelegate(nativeFamily);
267         if (delegate != null) {
268             if (sFontLocation == null) {
269                 delegate.mPostInitRunnables.add(new Runnable() {
270                     @Override
271                     public void run() {
272                         delegate.addFont(path, weight, isItalic);
273                     }
274                 });
275                 return true;
276             }
277             return delegate.addFont(path, weight, isItalic);
278         }
279         return false;
280     }
281 
282     @LayoutlibDelegate
283     /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) {
284         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
285                 "Typeface.createFromAsset is not supported.", null, null);
286         return false;
287     }
288 
289 
290     // ---- private helper methods ----
291 
292     private void init() {
293         for (Runnable postInitRunnable : mPostInitRunnables) {
294             postInitRunnable.run();
295         }
296         mPostInitRunnables = null;
297     }
298 
299      private boolean addFont(@NonNull String path) {
300          return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC));
301      }
302 
303     private boolean addFont(@NonNull String path, int weight, boolean isItalic) {
304         if (path.startsWith(SYSTEM_FONTS) &&
305                 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) {
306             return mValid = false;
307         }
308         // Set valid to true, even if the font fails to load.
309         mValid = true;
310         Font font = loadFont(path);
311         if (font == null) {
312             return false;
313         }
314         FontInfo fontInfo = new FontInfo();
315         fontInfo.mFont = font;
316         fontInfo.mWeight = weight;
317         fontInfo.mIsItalic = isItalic;
318         addFont(fontInfo);
319         return true;
320     }
321 
322     private boolean addFont(@NonNull FontInfo fontInfo) {
323         int weight = fontInfo.mWeight;
324         boolean isItalic = fontInfo.mIsItalic;
325         // The list is usually just two fonts big. So iterating over all isn't as bad as it looks.
326         // It's biggest for roboto where the size is 12.
327         for (FontInfo font : mFonts) {
328             if (font.mWeight == weight && font.mIsItalic == isItalic) {
329                 return false;
330             }
331         }
332         mFonts.add(fontInfo);
333         return true;
334     }
335 
336     /**
337      * Compute matching metric between two styles - 0 is an exact match.
338      */
339     private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) {
340         int score = Math.abs(font1.mWeight - font2.mWeight);
341         if (font1.mIsItalic != font2.mIsItalic) {
342             score += 200;
343         }
344         return score;
345     }
346 
347     /**
348      * Try to derive a font from {@code srcFont} for the style in {@code outFont}.
349      * <p/>
350      * {@code outFont} is updated to reflect the style of the derived font.
351      * @param srcFont the source font
352      * @param outFont contains the desired font style. Updated to contain the derived font and
353      *                its style
354      * @return outFont
355      */
356     @NonNull
357     private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) {
358         int desiredWeight = outFont.mWeight;
359         int srcWeight = srcFont.mWeight;
360         Font derivedFont = srcFont.mFont;
361         // Embolden the font if required.
362         if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) {
363             derivedFont = derivedFont.deriveFont(Font.BOLD);
364             srcWeight += BOLD_FONT_WEIGHT_DELTA;
365         }
366         // Italicize the font if required.
367         if (outFont.mIsItalic && !srcFont.mIsItalic) {
368             derivedFont = derivedFont.deriveFont(Font.ITALIC);
369         } else if (outFont.mIsItalic != srcFont.mIsItalic) {
370             // The desired font is plain, but the src font is italics. We can't convert it back. So
371             // we update the value to reflect the true style of the font we're deriving.
372             outFont.mIsItalic = srcFont.mIsItalic;
373         }
374         outFont.mFont = derivedFont;
375         outFont.mWeight = srcWeight;
376         // No need to update mIsItalics, as it's already been handled above.
377         return outFont;
378     }
379 }
380