FontProvider.java

/*
 * Copyright 2021 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.provider;

import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.Signature;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.CancellationSignal;

import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.FontResourcesParserCompat;
import androidx.core.provider.FontsContractCompat.FontFamilyResult;
import androidx.core.provider.FontsContractCompat.FontInfo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class FontProvider {
    private FontProvider() {}

    @NonNull
    static FontFamilyResult getFontFamilyResult(@NonNull Context context,
            @NonNull FontRequest request, @Nullable CancellationSignal cancellationSignal)
            throws PackageManager.NameNotFoundException {
        ProviderInfo providerInfo = getProvider(
                context.getPackageManager(), request, context.getResources());
        if (providerInfo == null) {
            return FontFamilyResult.create(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);

        }
        FontInfo[] fonts = query(
                context, request, providerInfo.authority, cancellationSignal);
        return FontFamilyResult.create(FontFamilyResult.STATUS_OK, fonts);
    }

    /**
     * Do not access directly, visible for testing only.
     */
    @VisibleForTesting
    @Nullable
    static ProviderInfo getProvider(
            @NonNull PackageManager packageManager,
            @NonNull FontRequest request,
            @Nullable Resources resources
    )
            throws PackageManager.NameNotFoundException {
        String providerAuthority = request.getProviderAuthority();
        ProviderInfo info = packageManager.resolveContentProvider(providerAuthority, 0);
        if (info == null) {
            throw new PackageManager.NameNotFoundException("No package found for authority: "
                    + providerAuthority);
        }

        if (!info.packageName.equals(request.getProviderPackage())) {
            throw new PackageManager.NameNotFoundException("Found content provider "
                    + providerAuthority
                    + ", but package was not " + request.getProviderPackage());
        }

        List<byte[]> signatures;
        // We correctly check all signatures returned, as advised in the lint error.
        @SuppressLint("PackageManagerGetSignatures")
        PackageInfo packageInfo = packageManager.getPackageInfo(info.packageName,
                PackageManager.GET_SIGNATURES);
        signatures = convertToByteArrayList(packageInfo.signatures);
        Collections.sort(signatures, sByteArrayComparator);
        List<List<byte[]>> requestCertificatesList = getCertificates(request, resources);
        for (int i = 0; i < requestCertificatesList.size(); ++i) {
            // Make a copy so we can sort it without modifying the incoming data.
            List<byte[]> requestSignatures = new ArrayList<>(requestCertificatesList.get(i));
            Collections.sort(requestSignatures, sByteArrayComparator);
            if (equalsByteArrayList(signatures, requestSignatures)) {
                return info;
            }
        }
        return null;
    }

    /**
     * Do not access directly, visible for testing only.
     */
    @VisibleForTesting
    @NonNull
    static FontInfo[] query(
            Context context,
            FontRequest request,
            String authority,
            CancellationSignal cancellationSignal
    ) {
        ArrayList<FontInfo> result = new ArrayList<>();
        final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                .authority(authority)
                .build();
        final Uri fileBaseUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                .authority(authority)
                .appendPath("file")
                .build();
        Cursor cursor = null;
        try {
            String[] projection = {
                    FontsContractCompat.Columns._ID, FontsContractCompat.Columns.FILE_ID,
                    FontsContractCompat.Columns.TTC_INDEX,
                    FontsContractCompat.Columns.VARIATION_SETTINGS,
                    FontsContractCompat.Columns.WEIGHT, FontsContractCompat.Columns.ITALIC,
                    FontsContractCompat.Columns.RESULT_CODE};

            ContentResolver resolver = context.getContentResolver();
            if (Build.VERSION.SDK_INT > 16) {
                cursor = Api16Impl.query(resolver, uri, projection, "query = ?",
                        new String[]{request.getQuery()}, null, cancellationSignal);
            } else {
                // No cancellation signal.
                cursor = resolver.query(uri, projection, "query = ?",
                        new String[]{request.getQuery()}, null);
            }

            if (cursor != null && cursor.getCount() > 0) {
                final int resultCodeColumnIndex = cursor.getColumnIndex(
                        FontsContractCompat.Columns.RESULT_CODE);
                result = new ArrayList<>();
                final int idColumnIndex = cursor.getColumnIndex(FontsContractCompat.Columns._ID);
                final int fileIdColumnIndex = cursor.getColumnIndex(
                        FontsContractCompat.Columns.FILE_ID);
                final int ttcIndexColumnIndex = cursor.getColumnIndex(
                        FontsContractCompat.Columns.TTC_INDEX);
                final int weightColumnIndex = cursor.getColumnIndex(
                        FontsContractCompat.Columns.WEIGHT);
                final int italicColumnIndex = cursor.getColumnIndex(
                        FontsContractCompat.Columns.ITALIC);
                while (cursor.moveToNext()) {
                    int resultCode = resultCodeColumnIndex != -1
                            ? cursor.getInt(resultCodeColumnIndex)
                            : FontsContractCompat.Columns.RESULT_CODE_OK;
                    final int ttcIndex = ttcIndexColumnIndex != -1
                            ? cursor.getInt(ttcIndexColumnIndex) : 0;
                    Uri fileUri;
                    if (fileIdColumnIndex == -1) {
                        long id = cursor.getLong(idColumnIndex);
                        fileUri = ContentUris.withAppendedId(uri, id);
                    } else {
                        long id = cursor.getLong(fileIdColumnIndex);
                        fileUri = ContentUris.withAppendedId(fileBaseUri, id);
                    }

                    int weight = weightColumnIndex != -1 ? cursor.getInt(weightColumnIndex) : 400;
                    boolean italic = italicColumnIndex != -1 && cursor.getInt(italicColumnIndex)
                            == 1;
                    result.add(FontInfo.create(fileUri, ttcIndex, weight, italic, resultCode));
                }
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return result.toArray(new FontInfo[0]);
    }

    private static List<List<byte[]>> getCertificates(FontRequest request, Resources resources) {
        if (request.getCertificates() != null) {
            return request.getCertificates();
        }
        int resourceId = request.getCertificatesArrayResId();
        return FontResourcesParserCompat.readCerts(resources, resourceId);
    }

    private static final Comparator<byte[]> sByteArrayComparator = (l, r) -> {
        if (l.length != r.length) {
            return l.length - r.length;
        }
        for (int i = 0; i < l.length; ++i) {
            if (l[i] != r[i]) {
                return l[i] - r[i];
            }
        }
        return 0;
    };

    private static boolean equalsByteArrayList(List<byte[]> signatures,
            List<byte[]> requestSignatures) {
        if (signatures.size() != requestSignatures.size()) {
            return false;
        }
        for (int i = 0; i < signatures.size(); ++i) {
            if (!Arrays.equals(signatures.get(i), requestSignatures.get(i))) {
                return false;
            }
        }
        return true;
    }

    private static List<byte[]> convertToByteArrayList(Signature[] signatures) {
        List<byte[]> shaList = new ArrayList<>();
        for (Signature signature : signatures) {
            shaList.add(signature.toByteArray());
        }
        return shaList;
    }

    @RequiresApi(16)
    static class Api16Impl {
        private Api16Impl() {
            // This class is not instantiable.
        }

        @SuppressWarnings("SameParameterValue")
        @DoNotInline
        static Cursor query(ContentResolver contentResolver, Uri uri, String[] projection,
                String selection, String[] selectionArgs, String sortOrder,
                Object cancellationSignal) { // Avoid implicit NewApi cast for CancellationSignal
            return contentResolver.query(uri, projection, selection, selectionArgs, sortOrder,
                    (CancellationSignal) cancellationSignal);
        }
    }
}