TypefaceCompatApi26Impl.java

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.graphics;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.graphics.fonts.FontVariationAxis;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.FontResourcesParserCompat;
import androidx.core.content.res.FontResourcesParserCompat.FontFileResourceEntry;
import androidx.core.provider.FontsContractCompat;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Map;

/**
 * Implementation of the Typeface compat methods for API 26 and above.
 * @hide
 */
@RestrictTo(LIBRARY_GROUP_PREFIX)
@RequiresApi(26)
public class TypefaceCompatApi26Impl extends TypefaceCompatApi21Impl {
    private static final String TAG = "TypefaceCompatApi26Impl";

    private static final String FONT_FAMILY_CLASS = "android.graphics.FontFamily";
    private static final String ADD_FONT_FROM_ASSET_MANAGER_METHOD = "addFontFromAssetManager";
    private static final String ADD_FONT_FROM_BUFFER_METHOD = "addFontFromBuffer";
    private static final String CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD =
            "createFromFamiliesWithDefault";
    private static final String FREEZE_METHOD = "freeze";
    private static final String ABORT_CREATION_METHOD = "abortCreation";
    private static final int RESOLVE_BY_FONT_TABLE = -1;

    protected final Class<?> mFontFamily;
    protected final Constructor<?> mFontFamilyCtor;
    protected final Method mAddFontFromAssetManager;
    protected final Method mAddFontFromBuffer;
    protected final Method mFreeze;
    protected final Method mAbortCreation;
    protected final Method mCreateFromFamiliesWithDefault;

    public TypefaceCompatApi26Impl() {
        Class<?> fontFamily;
        Constructor<?> fontFamilyCtor;
        Method addFontFromAssetManager;
        Method addFontFromBuffer;
        Method freeze;
        Method abortCreation;
        Method createFromFamiliesWithDefault;
        try {
            fontFamily = obtainFontFamily();
            fontFamilyCtor = obtainFontFamilyCtor(fontFamily);
            addFontFromAssetManager = obtainAddFontFromAssetManagerMethod(fontFamily);
            addFontFromBuffer = obtainAddFontFromBufferMethod(fontFamily);
            freeze = obtainFreezeMethod(fontFamily);
            abortCreation = obtainAbortCreationMethod(fontFamily);
            createFromFamiliesWithDefault = obtainCreateFromFamiliesWithDefaultMethod(fontFamily);
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            Log.e(TAG, "Unable to collect necessary methods for class " + e.getClass().getName(),
                    e);
            fontFamily = null;
            fontFamilyCtor = null;
            addFontFromAssetManager = null;
            addFontFromBuffer = null;
            freeze = null;
            abortCreation = null;
            createFromFamiliesWithDefault = null;
        }
        mFontFamily = fontFamily;
        mFontFamilyCtor = fontFamilyCtor;
        mAddFontFromAssetManager = addFontFromAssetManager;
        mAddFontFromBuffer = addFontFromBuffer;
        mFreeze = freeze;
        mAbortCreation = abortCreation;
        mCreateFromFamiliesWithDefault = createFromFamiliesWithDefault;
    }

    /**
     * Returns true if all the necessary methods were found.
     */
    private boolean isFontFamilyPrivateAPIAvailable() {
        if (mAddFontFromAssetManager == null) {
            Log.w(TAG, "Unable to collect necessary private methods. "
                    + "Fallback to legacy implementation.");
        }
        return mAddFontFromAssetManager != null;
    }

    /**
     * Create a new FontFamily instance
     */
    @Nullable
    private Object newFamily() {
        try {
            return mFontFamilyCtor.newInstance();
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
            return null;
        }
    }

    /**
     * Call FontFamily#addFontFromAssetManager(AssetManager mgr, String path, int cookie,
     *      boolean isAsset, int ttcIndex, int weight, int isItalic, FontVariationAxis[] axes)
     */
    private boolean addFontFromAssetManager(Context context, Object family, String fileName,
            int ttcIndex, int weight, int style, @Nullable FontVariationAxis[] axes) {
        try {
            return (Boolean) mAddFontFromAssetManager.invoke(family,
                    context.getAssets(), fileName, 0 /* cookie */, false /* isAsset */, ttcIndex,
                    weight, style, axes);
        } catch (IllegalAccessException | InvocationTargetException e) {
            return false;
        }
    }

    /**
     * Call FontFamily#addFontFromBuffer(ByteBuffer font, int ttcIndex, FontVariationAxis[] axes,
     *      int weight, int italic)
     */
    private boolean addFontFromBuffer(Object family, ByteBuffer buffer,
            int ttcIndex, int weight, int style) {
        try {
            return (Boolean) mAddFontFromBuffer.invoke(family,
                    buffer, ttcIndex, null /* axes */, weight, style);
        } catch (IllegalAccessException | InvocationTargetException e) {
            return false;
        }
    }

    /**
     * Call method Typeface#createFromFamiliesWithDefault(
     *      FontFamily[] families, int weight, int italic)
     */
    @Nullable
    protected Typeface createFromFamiliesWithDefault(Object family) {
        try {
            Object familyArray = Array.newInstance(mFontFamily, 1);
            Array.set(familyArray, 0, family);
            return (Typeface) mCreateFromFamiliesWithDefault.invoke(null /* static method */,
                    familyArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE);
        } catch (IllegalAccessException | InvocationTargetException e) {
            return null;
        }
    }

    /**
     * Call FontFamily#freeze()
     */
    private boolean freeze(Object family) {
        try {
            return (Boolean) mFreeze.invoke(family);
        } catch (IllegalAccessException | InvocationTargetException e) {
            return false;
        }
    }

    /**
     * Call FontFamily#abortCreation()
     */
    private void abortCreation(Object family) {
        try {
            mAbortCreation.invoke(family);
        } catch (IllegalAccessException | InvocationTargetException ignored) { }
    }

    @Override
    @Nullable
    public Typeface createFromFontFamilyFilesResourceEntry(Context context,
            FontResourcesParserCompat.FontFamilyFilesResourceEntry entry, Resources resources,
            int style) {
        if (!isFontFamilyPrivateAPIAvailable()) {
            return super.createFromFontFamilyFilesResourceEntry(context, entry, resources, style);
        }
        Object fontFamily = newFamily();
        if (fontFamily == null) {
            return null;
        }
        for (final FontFileResourceEntry fontFile : entry.getEntries()) {
            if (!addFontFromAssetManager(context, fontFamily, fontFile.getFileName(),
                    fontFile.getTtcIndex(), fontFile.getWeight(), fontFile.isItalic() ? 1 : 0,
                    FontVariationAxis.fromFontVariationSettings(fontFile.getVariationSettings()))) {
                abortCreation(fontFamily);
                return null;
            }
        }
        if (!freeze(fontFamily)) {
            return null;
        }
        return createFromFamiliesWithDefault(fontFamily);
    }

    @Override
    @Nullable
    public Typeface createFromFontInfo(Context context,
            @Nullable CancellationSignal cancellationSignal,
            @NonNull FontsContractCompat.FontInfo[] fonts, int style) {
        if (fonts.length < 1) {
            return null;
        }
        if (!isFontFamilyPrivateAPIAvailable()) {
            // Even if the private API is not avaiable, don't use API 21 implemenation and use
            // public API to create Typeface from file descriptor.
            final FontsContractCompat.FontInfo bestFont = findBestInfo(fonts, style);
            final ContentResolver resolver = context.getContentResolver();
            try (ParcelFileDescriptor pfd =
                    resolver.openFileDescriptor(bestFont.getUri(), "r", cancellationSignal)) {
                if (pfd == null) {
                    return null;
                }
                return new Typeface.Builder(pfd.getFileDescriptor())
                        .setWeight(bestFont.getWeight())
                        .setItalic(bestFont.isItalic())
                        .build();
            } catch (IOException e) {
                return null;
            }
        }
        Map<Uri, ByteBuffer> uriBuffer = FontsContractCompat.prepareFontData(
                context, fonts, cancellationSignal);
        final Object fontFamily = newFamily();
        if (fontFamily == null) {
            return null;
        }
        boolean atLeastOneFont = false;
        for (FontsContractCompat.FontInfo font : fonts) {
            final ByteBuffer fontBuffer = uriBuffer.get(font.getUri());
            if (fontBuffer == null) {
                continue;  // skip
            }
            final boolean success = addFontFromBuffer(fontFamily, fontBuffer,
                    font.getTtcIndex(), font.getWeight(), font.isItalic() ? 1 : 0);
            if (!success) {
                abortCreation(fontFamily);
                return null;
            }
            atLeastOneFont = true;
        }
        if (!atLeastOneFont) {
            abortCreation(fontFamily);
            return null;
        }
        if (!freeze(fontFamily)) {
            return null;
        }
        final Typeface typeface = createFromFamiliesWithDefault(fontFamily);
        if (typeface == null) {
            return null;
        }
        return Typeface.create(typeface, style);
    }

    /**
     * Used by Resources to load a font resource of type font file.
     */
    @Nullable
    @Override
    public Typeface createFromResourcesFontFile(
            Context context, Resources resources, int id, String path, int style) {
        if (!isFontFamilyPrivateAPIAvailable()) {
            return super.createFromResourcesFontFile(context, resources, id, path, style);
        }
        Object fontFamily = newFamily();
        if (fontFamily == null) {
            return null;
        }
        if (!addFontFromAssetManager(context, fontFamily, path,
                0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */,
                RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) {
            abortCreation(fontFamily);
            return null;
        }
        if (!freeze(fontFamily)) {
            return null;
        }
        return createFromFamiliesWithDefault(fontFamily);
    }

    // The following getters retrieve by reflection the Typeface methods, belonging to the
    // framework code, which will be invoked. Since the definitions of these methods can change
    // across different API versions, inheriting classes should override these getters in order to
    // reflect the method definitions in the API versions they represent.
    //===========================================================================================
    protected Class<?> obtainFontFamily() throws ClassNotFoundException {
        return Class.forName(FONT_FAMILY_CLASS);
    }

    protected Constructor<?> obtainFontFamilyCtor(Class<?> fontFamily)
            throws NoSuchMethodException {
        return fontFamily.getConstructor();
    }

    protected Method obtainAddFontFromAssetManagerMethod(Class<?> fontFamily)
            throws NoSuchMethodException {
        return fontFamily.getMethod(ADD_FONT_FROM_ASSET_MANAGER_METHOD,
                AssetManager.class, String.class, Integer.TYPE, Boolean.TYPE, Integer.TYPE,
                Integer.TYPE, Integer.TYPE, FontVariationAxis[].class);
    }

    protected Method obtainAddFontFromBufferMethod(Class<?> fontFamily)
            throws NoSuchMethodException {
        return fontFamily.getMethod(ADD_FONT_FROM_BUFFER_METHOD,
                ByteBuffer.class, Integer.TYPE, FontVariationAxis[].class, Integer.TYPE,
                Integer.TYPE);
    }

    protected Method obtainFreezeMethod(Class<?> fontFamily) throws NoSuchMethodException {
        return fontFamily.getMethod(FREEZE_METHOD);
    }

    protected Method obtainAbortCreationMethod(Class<?> fontFamily) throws NoSuchMethodException {
        return fontFamily.getMethod(ABORT_CREATION_METHOD);
    }

    protected Method obtainCreateFromFamiliesWithDefaultMethod(Class<?> fontFamily)
            throws NoSuchMethodException {
        Object familyArray = Array.newInstance(fontFamily, 1);
        Method m =  Typeface.class.getDeclaredMethod(CREATE_FROM_FAMILIES_WITH_DEFAULT_METHOD,
                familyArray.getClass(), Integer.TYPE, Integer.TYPE);
        m.setAccessible(true);
        return m;
    }
}