FontFamily_Delegate.java revision a87b07d7fafd59ae26073a80cd742b17ea427ecd
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
17package android.graphics;
18
19import com.android.ide.common.rendering.api.AssetRepository;
20import com.android.ide.common.rendering.api.LayoutLog;
21import com.android.layoutlib.bridge.Bridge;
22import com.android.layoutlib.bridge.impl.DelegateManager;
23import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
24
25import android.annotation.NonNull;
26import android.annotation.Nullable;
27import android.content.res.AssetManager;
28import android.content.res.BridgeAssetManager;
29
30import java.awt.Font;
31import java.awt.FontFormatException;
32import java.io.File;
33import java.io.FileNotFoundException;
34import java.io.IOException;
35import java.io.InputStream;
36import java.util.ArrayList;
37import java.util.Collections;
38import java.util.HashSet;
39import java.util.LinkedHashMap;
40import java.util.List;
41import java.util.Map;
42import java.util.Map.Entry;
43import java.util.Scanner;
44import java.util.Set;
45
46import 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 */
60public 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(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
137    public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
138        return sManager.getDelegate(nativeFontFamily);
139    }
140
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
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
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     */
212    public boolean isValid() {
213        return mValid;
214    }
215
216    /*package*/ 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
244    /*package*/ static String getFontLocation() {
245        return sFontLocation;
246    }
247
248    // ---- native methods ----
249
250    @LayoutlibDelegate
251    /*package*/ static long nCreateFamily(String lang, int variant) {
252        // TODO: support lang. This is required for japanese locale.
253        FontFamily_Delegate delegate = new FontFamily_Delegate();
254        // variant can be 0, 1 or 2.
255        assert variant < 3;
256        delegate.mVariant = FontVariant.values()[variant];
257        if (sFontLocation != null) {
258            delegate.init();
259        } else {
260            sPostInitDelegate.add(delegate);
261        }
262        return sManager.addNewDelegate(delegate);
263    }
264
265    @LayoutlibDelegate
266    /*package*/ static void nUnrefFamily(long nativePtr) {
267        // Removing the java reference for the object doesn't mean that it's freed for garbage
268        // collection. Typeface_Delegate may still hold a reference for it.
269        sManager.removeJavaReferenceFor(nativePtr);
270    }
271
272    @LayoutlibDelegate
273    /*package*/ static boolean nAddFont(long nativeFamily, final String path) {
274        final FontFamily_Delegate delegate = getDelegate(nativeFamily);
275        if (delegate != null) {
276            if (sFontLocation == null) {
277                delegate.mPostInitRunnables.add(new Runnable() {
278                    @Override
279                    public void run() {
280                        delegate.addFont(path);
281                    }
282                });
283                return true;
284            }
285            return delegate.addFont(path);
286        }
287        return false;
288    }
289
290    @LayoutlibDelegate
291    /*package*/ static boolean nAddFontWeightStyle(long nativeFamily,
292            final String path, final int index, final List<FontListParser.Axis> axes,
293            final int weight, final boolean isItalic) {
294        // 'index' and 'axes' are not supported by java.awt.Font
295        final FontFamily_Delegate delegate = getDelegate(nativeFamily);
296        if (delegate != null) {
297            if (sFontLocation == null) {
298                delegate.mPostInitRunnables.add(new Runnable() {
299                    @Override
300                    public void run() {
301                        delegate.addFont(path, weight, isItalic);
302                    }
303                });
304                return true;
305            }
306            return delegate.addFont(path, weight, isItalic);
307        }
308        return false;
309    }
310
311    @LayoutlibDelegate
312    /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) {
313        FontFamily_Delegate ffd = sManager.getDelegate(nativeFamily);
314        ffd.mValid = true;
315        if (mgr == null) {
316            return false;
317        }
318        if (mgr instanceof BridgeAssetManager) {
319            InputStream fontStream = null;
320            try {
321                AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository();
322                if (assetRepository == null) {
323                    Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
324                            null);
325                    return false;
326                }
327                if (!assetRepository.isSupported()) {
328                    // Don't log any warnings on unsupported IDEs.
329                    return false;
330                }
331                // Check cache
332                FontInfo fontInfo = sCache.get(path);
333                if (fontInfo != null) {
334                    // renew the font's lease.
335                    sCache.put(path, fontInfo);
336                    ffd.addFont(fontInfo);
337                    return true;
338                }
339                fontStream = assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING);
340                if (fontStream == null) {
341                    Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
342                            path);
343                    return false;
344                }
345                Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream);
346                fontInfo = new FontInfo();
347                fontInfo.mFont = font;
348                fontInfo.mWeight = font.isBold() ? BOLD_FONT_WEIGHT : DEFAULT_FONT_WEIGHT;
349                fontInfo.mIsItalic = font.isItalic();
350                ffd.addFont(fontInfo);
351                return true;
352            } catch (IOException e) {
353                Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e,
354                        path);
355            } catch (FontFormatException e) {
356                if (path.endsWith(EXTENSION_OTF)) {
357                    // otf fonts are not supported on the user's config (JRE version + OS)
358                    Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
359                            "OpenType fonts are not supported yet: " + path, null, path);
360                } else {
361                    Bridge.getLog().error(LayoutLog.TAG_BROKEN,
362                            "Unable to load font " + path, e, path);
363                }
364            } finally {
365                if (fontStream != null) {
366                    try {
367                        fontStream.close();
368                    } catch (IOException ignored) {
369                    }
370                }
371            }
372            return false;
373        }
374        // This should never happen. AssetManager is a final class (from user's perspective), and
375        // we've replaced every creation of AssetManager with our implementation. We create an
376        // exception and log it, but continue with rest of the rendering, without loading this font.
377        Bridge.getLog().error(LayoutLog.TAG_BROKEN,
378                "You have found a bug in the rendering library. Please file a bug at b.android.com.",
379                new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"),
380                null);
381        return false;
382    }
383
384
385    // ---- private helper methods ----
386
387    private void init() {
388        for (Runnable postInitRunnable : mPostInitRunnables) {
389            postInitRunnable.run();
390        }
391        mPostInitRunnables = null;
392    }
393
394     private boolean addFont(@NonNull String path) {
395         return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC));
396     }
397
398    private boolean addFont(@NonNull String path, int weight, boolean isItalic) {
399        if (path.startsWith(SYSTEM_FONTS) &&
400                !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) {
401            return mValid = false;
402        }
403        // Set valid to true, even if the font fails to load.
404        mValid = true;
405        Font font = loadFont(path);
406        if (font == null) {
407            return false;
408        }
409        FontInfo fontInfo = new FontInfo();
410        fontInfo.mFont = font;
411        fontInfo.mWeight = weight;
412        fontInfo.mIsItalic = isItalic;
413        addFont(fontInfo);
414        return true;
415    }
416
417    private boolean addFont(@NonNull FontInfo fontInfo) {
418        int weight = fontInfo.mWeight;
419        boolean isItalic = fontInfo.mIsItalic;
420        // The list is usually just two fonts big. So iterating over all isn't as bad as it looks.
421        // It's biggest for roboto where the size is 12.
422        //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
423        for (int i = 0, n = mFonts.size(); i < n; i++) {
424            FontInfo font = mFonts.get(i);
425            if (font.mWeight == weight && font.mIsItalic == isItalic) {
426                return false;
427            }
428        }
429        mFonts.add(fontInfo);
430        return true;
431    }
432
433    /**
434     * Compute matching metric between two styles - 0 is an exact match.
435     */
436    private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) {
437        int score = Math.abs(font1.mWeight - font2.mWeight);
438        if (font1.mIsItalic != font2.mIsItalic) {
439            score += 200;
440        }
441        return score;
442    }
443
444    /**
445     * Try to derive a font from {@code srcFont} for the style in {@code outFont}.
446     * <p/>
447     * {@code outFont} is updated to reflect the style of the derived font.
448     * @param srcFont the source font
449     * @param outFont contains the desired font style. Updated to contain the derived font and
450     *                its style
451     * @return outFont
452     */
453    @NonNull
454    private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) {
455        int desiredWeight = outFont.mWeight;
456        int srcWeight = srcFont.mWeight;
457        Font derivedFont = srcFont.mFont;
458        // Embolden the font if required.
459        if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) {
460            derivedFont = derivedFont.deriveFont(Font.BOLD);
461            srcWeight += BOLD_FONT_WEIGHT_DELTA;
462        }
463        // Italicize the font if required.
464        if (outFont.mIsItalic && !srcFont.mIsItalic) {
465            derivedFont = derivedFont.deriveFont(Font.ITALIC);
466        } else if (outFont.mIsItalic != srcFont.mIsItalic) {
467            // The desired font is plain, but the src font is italics. We can't convert it back. So
468            // we update the value to reflect the true style of the font we're deriving.
469            outFont.mIsItalic = srcFont.mIsItalic;
470        }
471        outFont.mFont = derivedFont;
472        outFont.mWeight = srcWeight;
473        // No need to update mIsItalics, as it's already been handled above.
474        return outFont;
475    }
476}
477