blob: 2429c2b137a4f5546958787ef06c42c1b3a03698 [file] [log] [blame]
/*
* 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.provider;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
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.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.provider.BaseColumns;
import androidx.annotation.GuardedBy;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.collection.LruCache;
import androidx.collection.SimpleArrayMap;
import androidx.core.content.res.FontResourcesParserCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.TypefaceCompat;
import androidx.core.graphics.TypefaceCompatUtil;
import androidx.core.provider.SelfDestructiveThread.ReplyCallback;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* Utility class to deal with Font ContentProviders.
*/
public class FontsContractCompat {
private static final String TAG = "FontsContractCompat";
private FontsContractCompat() { }
/**
* Defines the constants used in a response from a Font Provider. The cursor returned from the
* query should have the ID column populated with the content uri ID for the resulting font.
* This should point to a real file or shared memory, as the client will mmap the given file
* descriptor. Pipes, sockets and other non-mmap-able file descriptors will fail to load in the
* client application.
*/
public static final class Columns implements BaseColumns {
/**
* Constant used to request data from a font provider. The cursor returned from the query
* may populate this column with a long for the font file ID. The client will request a file
* descriptor to "file/FILE_ID" with this ID immediately under the top-level content URI. If
* not present, the client will request a file descriptor to the top-level URI with the
* given base font ID. Note that several results may return the same file ID, e.g. for TTC
* files with different indices.
*/
public static final String FILE_ID = "file_id";
/**
* Constant used to request data from a font provider. The cursor returned from the query
* should have this column populated with an int for the ttc index for the resulting font.
*/
public static final String TTC_INDEX = android.provider.FontsContract.Columns.TTC_INDEX;
/**
* Constant used to request data from a font provider. The cursor returned from the query
* may populate this column with the font variation settings String information for the
* font.
*/
public static final String VARIATION_SETTINGS =
android.provider.FontsContract.Columns.VARIATION_SETTINGS;
/**
* Constant used to request data from a font provider. The cursor returned from the query
* should have this column populated with the int weight for the resulting font. This value
* should be between 100 and 900. The most common values are 400 for regular weight and 700
* for bold weight.
*/
public static final String WEIGHT = android.provider.FontsContract.Columns.WEIGHT;
/**
* Constant used to request data from a font provider. The cursor returned from the query
* should have this column populated with the int italic for the resulting font. This should
* be 0 for regular style and 1 for italic.
*/
public static final String ITALIC = android.provider.FontsContract.Columns.ITALIC;
/**
* Constant used to request data from a font provider. The cursor returned from the query
* should have this column populated to indicate the result status of the
* query. This will be checked before any other data in the cursor. Possible values are
* {@link #RESULT_CODE_OK}, {@link #RESULT_CODE_FONT_NOT_FOUND},
* {@link #RESULT_CODE_MALFORMED_QUERY} and {@link #RESULT_CODE_FONT_UNAVAILABLE}. If not
* present, {@link #RESULT_CODE_OK} will be assumed.
*/
public static final String RESULT_CODE = android.provider.FontsContract.Columns.RESULT_CODE;
/**
* Constant used to represent a result was retrieved successfully. The given fonts will be
* attempted to retrieve immediately via
* {@link android.content.ContentProvider#openFile(Uri, String)}. See {@link #RESULT_CODE}.
*/
public static final int RESULT_CODE_OK =
android.provider.FontsContract.Columns.RESULT_CODE_OK;
/**
* Constant used to represent a result was not found. See {@link #RESULT_CODE}.
*/
public static final int RESULT_CODE_FONT_NOT_FOUND =
android.provider.FontsContract.Columns.RESULT_CODE_FONT_NOT_FOUND;
/**
* Constant used to represent a result was found, but cannot be provided at this moment. Use
* this to indicate, for example, that a font needs to be fetched from the network. See
* {@link #RESULT_CODE}.
*/
public static final int RESULT_CODE_FONT_UNAVAILABLE =
android.provider.FontsContract.Columns.RESULT_CODE_FONT_UNAVAILABLE;
/**
* Constant used to represent that the query was not in a supported format by the provider.
* See {@link #RESULT_CODE}.
*/
public static final int RESULT_CODE_MALFORMED_QUERY =
android.provider.FontsContract.Columns.RESULT_CODE_MALFORMED_QUERY;
}
/**
* Constant used to identify the List of {@link ParcelFileDescriptor} item in the Bundle
* returned to the ResultReceiver in getFont.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public static final String PARCEL_FONT_RESULTS = "font_results";
// Error codes internal to the system, which can not come from a provider. To keep the number
// space open for new provider codes, these should all be negative numbers.
/** @hide */
@RestrictTo(LIBRARY_GROUP)
/* package */ static final int RESULT_CODE_PROVIDER_NOT_FOUND = -1;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
/* package */ static final int RESULT_CODE_WRONG_CERTIFICATES = -2;
// Note -3 is used by FontRequestCallback to indicate the font failed to load.
static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
private static final int BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS = 10000;
private static final SelfDestructiveThread sBackgroundThread =
new SelfDestructiveThread("fonts", Process.THREAD_PRIORITY_BACKGROUND,
BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS);
@NonNull
static TypefaceResult getFontInternal(final Context context, final FontRequest request,
int style) {
FontFamilyResult result;
try {
result = fetchFonts(context, null /* CancellationSignal */, request);
} catch (PackageManager.NameNotFoundException e) {
return new TypefaceResult(null, FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND);
}
if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
final Typeface typeface = TypefaceCompat.createFromFontInfo(
context, null /* CancellationSignal */, result.getFonts(), style);
return new TypefaceResult(typeface, typeface != null
? FontRequestCallback.RESULT_OK
: FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
}
int resultCode = result.getStatusCode() == FontFamilyResult.STATUS_WRONG_CERTIFICATES
? FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES
: FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR;
return new TypefaceResult(null, resultCode);
}
static final Object sLock = new Object();
@GuardedBy("sLock")
static final SimpleArrayMap<String, ArrayList<ReplyCallback<TypefaceResult>>>
sPendingReplies = new SimpleArrayMap<>();
private static final class TypefaceResult {
final Typeface mTypeface;
@FontRequestCallback.FontRequestFailReason final int mResult;
TypefaceResult(@Nullable Typeface typeface,
@FontRequestCallback.FontRequestFailReason int result) {
mTypeface = typeface;
mResult = result;
}
}
/**
* Used for tests, should not be used otherwise.
* @hide
**/
@RestrictTo(LIBRARY_GROUP)
public static void resetCache() {
sTypefaceCache.evictAll();
}
/** @hide */
@RestrictTo(LIBRARY_GROUP)
public static Typeface getFontSync(final Context context, final FontRequest request,
final @Nullable ResourcesCompat.FontCallback fontCallback,
final @Nullable Handler handler, boolean isBlockingFetch, int timeout,
final int style) {
final String id = request.getIdentifier() + "-" + style;
Typeface cached = sTypefaceCache.get(id);
if (cached != null) {
if (fontCallback != null) {
fontCallback.onFontRetrieved(cached);
}
return cached;
}
if (isBlockingFetch && timeout == FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE) {
// Wait forever. No need to post to the thread.
TypefaceResult typefaceResult = getFontInternal(context, request, style);
if (fontCallback != null) {
if (typefaceResult.mResult == FontFamilyResult.STATUS_OK) {
fontCallback.callbackSuccessAsync(typefaceResult.mTypeface, handler);
} else {
fontCallback.callbackFailAsync(typefaceResult.mResult, handler);
}
}
return typefaceResult.mTypeface;
}
final Callable<TypefaceResult> fetcher = new Callable<TypefaceResult>() {
@Override
public TypefaceResult call() throws Exception {
TypefaceResult typeface = getFontInternal(context, request, style);
if (typeface.mTypeface != null) {
sTypefaceCache.put(id, typeface.mTypeface);
}
return typeface;
}
};
if (isBlockingFetch) {
try {
return sBackgroundThread.postAndWait(fetcher, timeout).mTypeface;
} catch (InterruptedException e) {
return null;
}
} else {
final ReplyCallback<TypefaceResult> reply = fontCallback == null ? null
: new ReplyCallback<TypefaceResult>() {
@Override
public void onReply(final TypefaceResult typeface) {
if (typeface == null) {
fontCallback.callbackFailAsync(
FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND, handler);
} else if (typeface.mResult == FontFamilyResult.STATUS_OK) {
fontCallback.callbackSuccessAsync(typeface.mTypeface, handler);
} else {
fontCallback.callbackFailAsync(typeface.mResult, handler);
}
}
};
synchronized (sLock) {
if (sPendingReplies.containsKey(id)) {
// Already requested. Do not request the same provider again and insert the
// reply to the queue instead.
if (reply != null) {
sPendingReplies.get(id).add(reply);
}
return null;
}
if (reply != null) {
ArrayList<ReplyCallback<TypefaceResult>> pendingReplies = new ArrayList<>();
pendingReplies.add(reply);
sPendingReplies.put(id, pendingReplies);
}
}
sBackgroundThread.postAndReply(fetcher, new ReplyCallback<TypefaceResult>() {
@Override
public void onReply(final TypefaceResult typeface) {
final ArrayList<ReplyCallback<TypefaceResult>> replies;
synchronized (sLock) {
replies = sPendingReplies.get(id);
if (replies == null) {
return; // Nobody requested replies. Do nothing.
}
sPendingReplies.remove(id);
}
for (int i = 0; i < replies.size(); ++i) {
replies.get(i).onReply(typeface);
}
}
});
return null;
}
}
/**
* Object represent a font entry in the family returned from {@link #fetchFonts}.
*/
public static class FontInfo {
private final Uri mUri;
private final int mTtcIndex;
private final int mWeight;
private final boolean mItalic;
private final int mResultCode;
/**
* Creates a Font with all the information needed about a provided font.
* @param uri A URI associated to the font file.
* @param ttcIndex If providing a TTC_INDEX file, the index to point to. Otherwise, 0.
* @param weight An integer that indicates the font weight.
* @param italic A boolean that indicates the font is italic style or not.
* @param resultCode A boolean that indicates the font contents is ready.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public FontInfo(@NonNull Uri uri, @IntRange(from = 0) int ttcIndex,
@IntRange(from = 1, to = 1000) int weight,
boolean italic, int resultCode) {
mUri = Preconditions.checkNotNull(uri);
mTtcIndex = ttcIndex;
mWeight = weight;
mItalic = italic;
mResultCode = resultCode;
}
/**
* Returns a URI associated to this record.
*/
public @NonNull Uri getUri() {
return mUri;
}
/**
* Returns the index to be used to access this font when accessing a TTC file.
*/
public @IntRange(from = 0) int getTtcIndex() {
return mTtcIndex;
}
/**
* Returns the weight value for this font.
*/
public @IntRange(from = 1, to = 1000) int getWeight() {
return mWeight;
}
/**
* Returns whether this font is italic.
*/
public boolean isItalic() {
return mItalic;
}
/**
* Returns result code.
*
* {@link FontsContractCompat.Columns#RESULT_CODE}
*/
public int getResultCode() {
return mResultCode;
}
}
/**
* Object returned from {@link #fetchFonts}.
*/
public static class FontFamilyResult {
/**
* Constant represents that the font was successfully retrieved. Note that when this value
* is set and {@link #getFonts} returns an empty array, it means there were no fonts
* matching the given query.
*/
public static final int STATUS_OK = 0;
/**
* Constant represents that the given certificate was not matched with the provider's
* signature. {@link #getFonts} returns null if this status was set.
*/
public static final int STATUS_WRONG_CERTIFICATES = 1;
/**
* Constant represents that the provider returns unexpected data. {@link #getFonts} returns
* null if this status was set. For example, this value is set when the font provider
* gives invalid format of variation settings.
*/
public static final int STATUS_UNEXPECTED_DATA_PROVIDED = 2;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED})
@Retention(RetentionPolicy.SOURCE)
@interface FontResultStatus {}
private final @FontResultStatus int mStatusCode;
private final FontInfo[] mFonts;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
mStatusCode = statusCode;
mFonts = fonts;
}
public @FontResultStatus int getStatusCode() {
return mStatusCode;
}
public FontInfo[] getFonts() {
return mFonts;
}
}
/**
* Interface used to receive asynchronously fetched typefaces.
*/
public static class FontRequestCallback {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
public static final int RESULT_OK = Columns.RESULT_CODE_OK;
/**
* Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
* provider was not found on the device.
*/
public static final int FAIL_REASON_PROVIDER_NOT_FOUND = RESULT_CODE_PROVIDER_NOT_FOUND;
/**
* Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
* provider must be authenticated and the given certificates do not match its signature.
*/
public static final int FAIL_REASON_WRONG_CERTIFICATES = RESULT_CODE_WRONG_CERTIFICATES;
/**
* Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
* returned by the provider was not loaded properly.
*/
public static final int FAIL_REASON_FONT_LOAD_ERROR = -3;
/**
* Constant that signals that the font was not loaded due to security issues. This usually
* means the font was attempted to load on a restricted context.
*/
public static final int FAIL_REASON_SECURITY_VIOLATION = -4;
/**
* Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
* provider did not return any results for the given query.
*/
public static final int FAIL_REASON_FONT_NOT_FOUND = Columns.RESULT_CODE_FONT_NOT_FOUND;
/**
* Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
* provider found the queried font, but it is currently unavailable.
*/
public static final int FAIL_REASON_FONT_UNAVAILABLE = Columns.RESULT_CODE_FONT_UNAVAILABLE;
/**
* Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
* query was not supported by the provider.
*/
public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR,
FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE,
FAIL_REASON_MALFORMED_QUERY, FAIL_REASON_WRONG_CERTIFICATES,
FAIL_REASON_SECURITY_VIOLATION, RESULT_OK })
@Retention(RetentionPolicy.SOURCE)
public @interface FontRequestFailReason {}
public FontRequestCallback() {}
/**
* Called then a Typeface request done via {@link #requestFont(Context, FontRequest,
* FontRequestCallback, Handler)} is complete. Note that this method will not be called if
* {@link #onTypefaceRequestFailed(int)} is called instead.
* @param typeface The Typeface object retrieved.
*/
public void onTypefaceRetrieved(Typeface typeface) {}
/**
* Called when a Typeface request done via {@link #requestFont(Context, FontRequest,
* FontRequestCallback, Handler)} fails.
* @param reason May be one of {@link #FAIL_REASON_PROVIDER_NOT_FOUND},
* {@link #FAIL_REASON_FONT_NOT_FOUND},
* {@link #FAIL_REASON_FONT_LOAD_ERROR},
* {@link #FAIL_REASON_FONT_UNAVAILABLE},
* {@link #FAIL_REASON_MALFORMED_QUERY} or
* {@link #FAIL_REASON_WRONG_CERTIFICATES}, or a provider defined positive
* code number.
*/
public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {}
}
/**
* Create a typeface object given a font request. The font will be asynchronously fetched,
* therefore the result is delivered to the given callback. See {@link FontRequest}.
* Only one of the methods in callback will be invoked, depending on whether the request
* succeeds or fails. These calls will happen on the caller thread.
* @param context A context to be used for fetching from font provider.
* @param request A {@link FontRequest} object that identifies the provider and query for the
* request. May not be null.
* @param callback A callback that will be triggered when results are obtained. May not be null.
* @param handler A handler to be processed the font fetching.
*/
public static void requestFont(final @NonNull Context context,
final @NonNull FontRequest request, final @NonNull FontRequestCallback callback,
final @NonNull Handler handler) {
final Handler callerThreadHandler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
// TODO: Cache the result.
FontFamilyResult result;
try {
result = fetchFonts(context, null /* cancellation signal */, request);
} catch (PackageManager.NameNotFoundException e) {
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(
FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND);
}
});
return;
}
if (result.getStatusCode() != FontFamilyResult.STATUS_OK) {
switch (result.getStatusCode()) {
case FontFamilyResult.STATUS_WRONG_CERTIFICATES:
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(
FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES);
}
});
return;
case FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED:
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
}
});
return;
default:
// fetchFont returns unexpected status type. Fallback to load error.
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
}
});
return;
}
}
final FontInfo[] fonts = result.getFonts();
if (fonts == null || fonts.length == 0) {
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(
FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND);
}
});
return;
}
for (final FontInfo font : fonts) {
if (font.getResultCode() != Columns.RESULT_CODE_OK) {
// We proceed if all font entry is ready to use. Otherwise report the first
// error.
final int resultCode = font.getResultCode();
if (resultCode < 0) {
// Negative values are reserved for internal errors. Fallback to load
// error.
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
}
});
} else {
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(resultCode);
}
});
}
return;
}
}
final Typeface typeface = buildTypeface(context, null /* cancellation signal */,
fonts);
if (typeface == null) {
// Something went wrong during reading font files. This happens if the given
// font file is an unsupported font type.
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
}
});
return;
}
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRetrieved(typeface);
}
});
}
});
}
/**
* Build a Typeface from an array of {@link FontInfo}
*
* Results that are marked as not ready will be skipped.
*
* @param context A {@link Context} that will be used to fetch the font contents.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
* the operation is canceled, then {@link
* android.os.OperationCanceledException} will be thrown.
* @param fonts An array of {@link FontInfo} to be used to create a Typeface.
* @return A Typeface object. Returns null if typeface creation fails.
*/
@Nullable
public static Typeface buildTypeface(@NonNull Context context,
@Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts) {
return TypefaceCompat.createFromFontInfo(context, cancellationSignal, fonts,
Typeface.NORMAL);
}
/**
* A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}.
*
* Skip if the file contents is not ready to be read.
*
* @param context A {@link Context} to be used for resolving content URI in
* {@link FontInfo}.
* @param fonts An array of {@link FontInfo}.
* @return A map from {@link Uri} to {@link ByteBuffer}.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@RequiresApi(19)
public static Map<Uri, ByteBuffer> prepareFontData(Context context, FontInfo[] fonts,
CancellationSignal cancellationSignal) {
final HashMap<Uri, ByteBuffer> out = new HashMap<>();
for (FontInfo font : fonts) {
if (font.getResultCode() != Columns.RESULT_CODE_OK) {
continue;
}
final Uri uri = font.getUri();
if (out.containsKey(uri)) {
continue;
}
ByteBuffer buffer = TypefaceCompatUtil.mmap(context, cancellationSignal, uri);
out.put(uri, buffer);
}
return Collections.unmodifiableMap(out);
}
/**
* Fetch fonts given a font request.
*
* @param context A {@link Context} to be used for fetching fonts.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
* the operation is canceled, then {@link
* android.os.OperationCanceledException} will be thrown when the
* query is executed.
* @param request A {@link FontRequest} object that identifies the provider and query for the
* request.
*
* @return {@link FontFamilyResult}
*
* @throws PackageManager.NameNotFoundException If requested package or authority was not found
* in the system.
*/
@NonNull
public static FontFamilyResult fetchFonts(@NonNull Context context,
@Nullable CancellationSignal cancellationSignal, @NonNull FontRequest request)
throws PackageManager.NameNotFoundException {
ProviderInfo providerInfo = getProvider(
context.getPackageManager(), request, context.getResources());
if (providerInfo == null) {
return new FontFamilyResult(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);
}
FontInfo[] fonts = getFontFromProvider(
context, request, providerInfo.authority, cancellationSignal);
return new FontFamilyResult(FontFamilyResult.STATUS_OK, fonts);
}
/** @hide */
@VisibleForTesting
@RestrictTo(LIBRARY_GROUP)
public static @Nullable 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;
}
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 = new Comparator<byte[]>() {
@Override
public int compare(byte[] l, byte[] 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[]> shas = new ArrayList<>();
for (int i = 0; i < signatures.length; ++i) {
shas.add(signatures[i].toByteArray());
}
return shas;
}
@VisibleForTesting
@NonNull
static FontInfo[] getFontFromProvider(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 {
if (Build.VERSION.SDK_INT > 16) {
cursor = context.getContentResolver().query(uri, new String[] {
Columns._ID, Columns.FILE_ID, Columns.TTC_INDEX,
Columns.VARIATION_SETTINGS, Columns.WEIGHT, Columns.ITALIC,
Columns.RESULT_CODE },
"query = ?", new String[] { request.getQuery() }, null, cancellationSignal);
} else {
// No cancellation signal.
cursor = context.getContentResolver().query(uri, new String[] {
Columns._ID, Columns.FILE_ID, Columns.TTC_INDEX,
Columns.VARIATION_SETTINGS, Columns.WEIGHT, Columns.ITALIC,
Columns.RESULT_CODE },
"query = ?", new String[] { request.getQuery() }, null);
}
if (cursor != null && cursor.getCount() > 0) {
final int resultCodeColumnIndex = cursor.getColumnIndex(Columns.RESULT_CODE);
result = new ArrayList<>();
final int idColumnIndex = cursor.getColumnIndex(Columns._ID);
final int fileIdColumnIndex = cursor.getColumnIndex(Columns.FILE_ID);
final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX);
final int weightColumnIndex = cursor.getColumnIndex(Columns.WEIGHT);
final int italicColumnIndex = cursor.getColumnIndex(Columns.ITALIC);
while (cursor.moveToNext()) {
int resultCode = resultCodeColumnIndex != -1
? cursor.getInt(resultCodeColumnIndex) : 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(new FontInfo(fileUri, ttcIndex, weight, italic, resultCode));
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return result.toArray(new FontInfo[0]);
}
}