1/*
2 * Copyright (C) 2017 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 androidx.core.graphics;
18
19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.res.AssetManager;
24import android.content.res.Resources;
25import android.graphics.Typeface;
26import android.graphics.fonts.FontVariationAxis;
27import android.net.Uri;
28import android.os.CancellationSignal;
29import android.os.ParcelFileDescriptor;
30import android.util.Log;
31
32import androidx.annotation.NonNull;
33import androidx.annotation.Nullable;
34import androidx.annotation.RequiresApi;
35import androidx.annotation.RestrictTo;
36import androidx.core.content.res.FontResourcesParserCompat;
37import androidx.core.content.res.FontResourcesParserCompat.FontFileResourceEntry;
38import androidx.core.provider.FontsContractCompat;
39
40import java.io.IOException;
41import java.lang.reflect.Array;
42import java.lang.reflect.Constructor;
43import java.lang.reflect.InvocationTargetException;
44import java.lang.reflect.Method;
45import java.nio.ByteBuffer;
46import java.util.Map;
47
48/**
49 * Implementation of the Typeface compat methods for API 26 and above.
50 * @hide
51 */
52@RestrictTo(LIBRARY_GROUP)
53@RequiresApi(26)
54public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
55    private static final String TAG = "TypefaceCompatApi26Impl";
56
57    private static final String FONT_FAMILY_CLASS = "android.graphics.FontFamily";
58    private static final String ADD_FONT_FROM_ASSET_MANAGER_METHOD = "addFontFromAssetManager";
59    private static final String ADD_FONT_FROM_BUFFER_METHOD = "addFontFromBuffer";
60    private static final String CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD =
61            "createFromFamiliesWithDefault";
62    private static final String FREEZE_METHOD = "freeze";
63    private static final String ABORT_CREATION_METHOD = "abortCreation";
64    private static final int RESOLVE_BY_FONT_TABLE = -1;
65    private static final String DEFAULT_FAMILY = "sans-serif";
66
67    protected final Class mFontFamily;
68    protected final Constructor mFontFamilyCtor;
69    protected final Method mAddFontFromAssetManager;
70    protected final Method mAddFontFromBuffer;
71    protected final Method mFreeze;
72    protected final Method mAbortCreation;
73    protected final Method mCreateFromFamiliesWithDefault;
74
75    public TypefaceCompatApi26Impl() {
76        Class fontFamily;
77        Constructor fontFamilyCtor;
78        Method addFontFromAssetManager;
79        Method addFontFromBuffer;
80        Method freeze;
81        Method abortCreation;
82        Method createFromFamiliesWithDefault;
83        try {
84            fontFamily = obtainFontFamily();
85            fontFamilyCtor = obtainFontFamilyCtor(fontFamily);
86            addFontFromAssetManager = obtainAddFontFromAssetManagerMethod(fontFamily);
87            addFontFromBuffer = obtainAddFontFromBufferMethod(fontFamily);
88            freeze = obtainFreezeMethod(fontFamily);
89            abortCreation = obtainAbortCreationMethod(fontFamily);
90            createFromFamiliesWithDefault = obtainCreateFromFamiliesWithDefaultMethod(fontFamily);
91        } catch (ClassNotFoundException | NoSuchMethodException e) {
92            Log.e(TAG, "Unable to collect necessary methods for class " + e.getClass().getName(),
93                    e);
94            fontFamily = null;
95            fontFamilyCtor = null;
96            addFontFromAssetManager = null;
97            addFontFromBuffer = null;
98            freeze = null;
99            abortCreation = null;
100            createFromFamiliesWithDefault = null;
101        }
102        mFontFamily = fontFamily;
103        mFontFamilyCtor = fontFamilyCtor;
104        mAddFontFromAssetManager = addFontFromAssetManager;
105        mAddFontFromBuffer = addFontFromBuffer;
106        mFreeze = freeze;
107        mAbortCreation = abortCreation;
108        mCreateFromFamiliesWithDefault = createFromFamiliesWithDefault;
109    }
110
111    /**
112     * Returns true if all the necessary methods were found.
113     */
114    private boolean isFontFamilyPrivateAPIAvailable() {
115        if (mAddFontFromAssetManager == null) {
116            Log.w(TAG, "Unable to collect necessary private methods. "
117                    + "Fallback to legacy implementation.");
118        }
119        return mAddFontFromAssetManager != null;
120    }
121
122    /**
123     * Create a new FontFamily instance
124     */
125    private Object newFamily() {
126        try {
127            return mFontFamilyCtor.newInstance();
128        } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
129            throw new RuntimeException(e);
130        }
131    }
132
133    /**
134     * Call FontFamily#addFontFromAssetManager(AssetManager mgr, String path, int cookie,
135     *      boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes)
136     */
137    private boolean addFontFromAssetManager(Context context, Object family, String fileName,
138            int ttcIndex, int weight, int style, @Nullable FontVariationAxis[] axes) {
139        try {
140            final Boolean result = (Boolean) mAddFontFromAssetManager.invoke(family,
141                    context.getAssets(), fileName, 0 /* cookie */, false /* isAsset */, ttcIndex,
142                    weight, style, axes);
143            return result.booleanValue();
144        } catch (IllegalAccessException | InvocationTargetException e) {
145            throw new RuntimeException(e);
146        }
147    }
148
149    /**
150     * Call FontFamily#addFontFromBuffer(ByteBuffer font, int ttcIndex, FontVariationAxis[] axes,
151     *      int weight, int italic)
152     */
153    private boolean addFontFromBuffer(Object family, ByteBuffer buffer,
154            int ttcIndex, int weight, int style) {
155        try {
156            final Boolean result = (Boolean) mAddFontFromBuffer.invoke(family,
157                    buffer, ttcIndex, null /* axes */, weight, style);
158            return result.booleanValue();
159        } catch (IllegalAccessException | InvocationTargetException e) {
160            throw new RuntimeException(e);
161        }
162    }
163
164    /**
165     * Call method Typeface#createFromFamiliesWithDefault(
166     *      FontFamily[] families, int weight, int italic)
167     */
168    protected Typeface createFromFamiliesWithDefault(Object family) {
169        try {
170            Object familyArray = Array.newInstance(mFontFamily, 1);
171            Array.set(familyArray, 0, family);
172            return (Typeface) mCreateFromFamiliesWithDefault.invoke(null /* static method */,
173                    familyArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
174        } catch (IllegalAccessException | InvocationTargetException e) {
175            throw new RuntimeException(e);
176        }
177    }
178
179    /**
180     * Call FontFamily#freeze()
181     */
182    private boolean freeze(Object family) {
183        try {
184            Boolean result = (Boolean) mFreeze.invoke(family);
185            return result.booleanValue();
186        } catch (IllegalAccessException | InvocationTargetException e) {
187            throw new RuntimeException(e);
188        }
189    }
190
191    /**
192     * Call FontFamily#abortCreation()
193     */
194    private void abortCreation(Object family) {
195        try {
196            mAbortCreation.invoke(family);
197        } catch (IllegalAccessException | InvocationTargetException e) {
198            throw new RuntimeException(e);
199        }
200    }
201
202    @Override
203    public Typeface createFromFontFamilyFilesResourceEntry(Context context,
204            FontResourcesParserCompat.FontFamilyFilesResourceEntry entry, Resources resources,
205            int style) {
206        if (!isFontFamilyPrivateAPIAvailable()) {
207            return super.createFromFontFamilyFilesResourceEntry(context, entry, resources, style);
208        }
209        Object fontFamily = newFamily();
210        for (final FontFileResourceEntry fontFile : entry.getEntries()) {
211            if (!addFontFromAssetManager(context, fontFamily, fontFile.getFileName(),
212                    fontFile.getTtcIndex(), fontFile.getWeight(), fontFile.isItalic() ? 1 : 0,
213                    FontVariationAxis.fromFontVariationSettings(fontFile.getVariationSettings()))) {
214                abortCreation(fontFamily);
215                return null;
216            }
217        }
218        if (!freeze(fontFamily)) {
219            return null;
220        }
221        return createFromFamiliesWithDefault(fontFamily);
222    }
223
224    @Override
225    public Typeface createFromFontInfo(Context context,
226            @Nullable CancellationSignal cancellationSignal,
227            @NonNull FontsContractCompat.FontInfo[] fonts, int style) {
228        if (fonts.length < 1) {
229            return null;
230        }
231        if (!isFontFamilyPrivateAPIAvailable()) {
232            // Even if the private API is not avaiable, don't use API 21 implemenation and use
233            // public API to create Typeface from file descriptor.
234            final FontsContractCompat.FontInfo bestFont = findBestInfo(fonts, style);
235            final ContentResolver resolver = context.getContentResolver();
236            try (ParcelFileDescriptor pfd =
237                    resolver.openFileDescriptor(bestFont.getUri(), "r", cancellationSignal)) {
238                if (pfd == null) {
239                    return null;
240                }
241                return new Typeface.Builder(pfd.getFileDescriptor())
242                        .setWeight(bestFont.getWeight())
243                        .setItalic(bestFont.isItalic())
244                        .build();
245            } catch (IOException e) {
246                return null;
247            }
248        }
249        Map<Uri, ByteBuffer> uriBuffer = FontsContractCompat.prepareFontData(
250                context, fonts, cancellationSignal);
251        final Object fontFamily = newFamily();
252        boolean atLeastOneFont = false;
253        for (FontsContractCompat.FontInfo font : fonts) {
254            final ByteBuffer fontBuffer = uriBuffer.get(font.getUri());
255            if (fontBuffer == null) {
256                continue;  // skip
257            }
258            final boolean success = addFontFromBuffer(fontFamily, fontBuffer,
259                    font.getTtcIndex(), font.getWeight(), font.isItalic() ? 1 : 0);
260            if (!success) {
261                abortCreation(fontFamily);
262                return null;
263            }
264            atLeastOneFont = true;
265        }
266        if (!atLeastOneFont) {
267            abortCreation(fontFamily);
268            return null;
269        }
270        if (!freeze(fontFamily)) {
271            return null;
272        }
273        final Typeface typeface = createFromFamiliesWithDefault(fontFamily);
274        return Typeface.create(typeface, style);
275    }
276
277    /**
278     * Used by Resources to load a font resource of type font file.
279     */
280    @Nullable
281    @Override
282    public Typeface createFromResourcesFontFile(
283            Context context, Resources resources, int id, String path, int style) {
284        if (!isFontFamilyPrivateAPIAvailable()) {
285            return super.createFromResourcesFontFile(context, resources, id, path, style);
286        }
287        Object fontFamily = newFamily();
288        if (!addFontFromAssetManager(context, fontFamily, path,
289                0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */,
290                RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) {
291            abortCreation(fontFamily);
292            return null;
293        }
294        if (!freeze(fontFamily)) {
295            return null;
296        }
297        return createFromFamiliesWithDefault(fontFamily);
298    }
299
300    // The following getters retrieve by reflection the Typeface methods, belonging to the
301    // framework code, which will be invoked. Since the definitions of these methods can change
302    // across different API versions, inheriting classes should override these getters in order to
303    // reflect the method definitions in the API versions they represent.
304    //===========================================================================================
305    protected Class obtainFontFamily() throws ClassNotFoundException {
306        return Class.forName(FONT_FAMILY_CLASS);
307    }
308
309    protected Constructor obtainFontFamilyCtor(Class fontFamily) throws NoSuchMethodException {
310        return fontFamily.getConstructor();
311    }
312
313    protected Method obtainAddFontFromAssetManagerMethod(Class fontFamily)
314            throws NoSuchMethodException {
315        return fontFamily.getMethod(ADD_FONT_FROM_ASSET_MANAGER_METHOD,
316                AssetManager.class, String.class, Integer.TYPE, Boolean.TYPE, Integer.TYPE,
317                Integer.TYPE, Integer.TYPE, FontVariationAxis[].class);
318    }
319
320    protected Method obtainAddFontFromBufferMethod(Class fontFamily) throws NoSuchMethodException {
321        return fontFamily.getMethod(ADD_FONT_FROM_BUFFER_METHOD,
322                ByteBuffer.class, Integer.TYPE, FontVariationAxis[].class, Integer.TYPE,
323                Integer.TYPE);
324    }
325
326    protected Method obtainFreezeMethod(Class fontFamily) throws NoSuchMethodException {
327        return fontFamily.getMethod(FREEZE_METHOD);
328    }
329
330    protected Method obtainAbortCreationMethod(Class fontFamily) throws NoSuchMethodException {
331        return fontFamily.getMethod(ABORT_CREATION_METHOD);
332    }
333
334    protected Method obtainCreateFromFamiliesWithDefaultMethod(Class fontFamily)
335            throws NoSuchMethodException {
336        Object familyArray = Array.newInstance(fontFamily, 1);
337        Method m =  Typeface.class.getDeclaredMethod(CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD,
338                familyArray.getClass(), Integer.TYPE, Integer.TYPE);
339        m.setAccessible(true);
340        return m;
341    }
342}
343