897 lines
38 KiB
Java
897 lines
38 KiB
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 android.provider;
|
|
|
|
import android.annotation.IntDef;
|
|
import android.annotation.IntRange;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
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.PackageManager.NameNotFoundException;
|
|
import android.content.pm.ProviderInfo;
|
|
import android.content.pm.Signature;
|
|
import android.database.Cursor;
|
|
import android.graphics.Typeface;
|
|
import android.graphics.fonts.Font;
|
|
import android.graphics.fonts.FontFamily;
|
|
import android.graphics.fonts.FontStyle;
|
|
import android.graphics.fonts.FontVariationAxis;
|
|
import android.net.Uri;
|
|
import android.os.CancellationSignal;
|
|
import android.os.Handler;
|
|
import android.os.HandlerThread;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.os.Process;
|
|
import android.util.Log;
|
|
import android.util.LruCache;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.util.Preconditions;
|
|
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.channels.FileChannel;
|
|
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.Set;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.concurrent.locks.Condition;
|
|
import java.util.concurrent.locks.Lock;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
|
|
/**
|
|
* Utility class to deal with Font ContentProviders.
|
|
* @deprecated Use the <a href="{@docRoot}jetpack">Jetpack Core Library</a>
|
|
* {@link androidx.core.provider.FontsContractCompat} for consistent behavior across all
|
|
* devices.
|
|
*/
|
|
@Deprecated
|
|
public class FontsContract {
|
|
private static final String TAG = "FontsContract";
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @deprecated Use the {@link androidx.core.provider.FontsContractCompat.Columns} for consistent
|
|
* behavior across all devices.
|
|
*/
|
|
@Deprecated
|
|
public static final class Columns implements BaseColumns {
|
|
|
|
// Do not instantiate.
|
|
private Columns() {}
|
|
|
|
/**
|
|
* 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 = "font_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 = "font_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 = "font_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 = "font_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} for system
|
|
* defined values. You may also define your own values in the 0x000010000..0xFFFF0000 range.
|
|
* If not present, {@link #RESULT_CODE_OK} will be assumed.
|
|
*/
|
|
public static final String RESULT_CODE = "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 = 0;
|
|
/**
|
|
* Constant used to represent a result was not found. See {@link #RESULT_CODE}.
|
|
*/
|
|
public static final int RESULT_CODE_FONT_NOT_FOUND = 1;
|
|
/**
|
|
* 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 = 2;
|
|
/**
|
|
* 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 = 3;
|
|
}
|
|
|
|
private static final Object sLock = new Object();
|
|
@GuardedBy("sLock")
|
|
private static Handler sHandler;
|
|
@GuardedBy("sLock")
|
|
private static HandlerThread sThread;
|
|
@GuardedBy("sLock")
|
|
private static Set<String> sInQueueSet;
|
|
|
|
private volatile static Context sContext; // set once in setApplicationContextForResources
|
|
|
|
private static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
|
|
|
|
private FontsContract() {
|
|
}
|
|
|
|
/** @hide */
|
|
public static void setApplicationContextForResources(Context context) {
|
|
sContext = context.getApplicationContext();
|
|
}
|
|
|
|
/**
|
|
* Object represent a font entry in the family returned from {@link #fetchFonts}.
|
|
*
|
|
* @deprecated Use the {@link androidx.core.provider.FontsContractCompat.FontInfo} for
|
|
* consistent behavior across all devices
|
|
*/
|
|
@Deprecated
|
|
public static class FontInfo {
|
|
private final Uri mUri;
|
|
private final int mTtcIndex;
|
|
private final FontVariationAxis[] mAxes;
|
|
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 axes If providing a variation font, the settings for it. May be null.
|
|
* @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 */
|
|
public FontInfo(@NonNull Uri uri, @IntRange(from = 0) int ttcIndex,
|
|
@Nullable FontVariationAxis[] axes, @IntRange(from = 1, to = 1000) int weight,
|
|
boolean italic, int resultCode) {
|
|
mUri = Preconditions.checkNotNull(uri);
|
|
mTtcIndex = ttcIndex;
|
|
mAxes = axes;
|
|
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 list of axes associated to this font.
|
|
*/
|
|
public @Nullable FontVariationAxis[] getAxes() {
|
|
return mAxes;
|
|
}
|
|
|
|
/**
|
|
* 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 FontsContract.Columns#RESULT_CODE}
|
|
*/
|
|
public int getResultCode() {
|
|
return mResultCode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Object returned from {@link #fetchFonts}.
|
|
*
|
|
* @deprecated Use the {@link androidx.core.provider.FontsContractCompat.FontFamilyResult} for
|
|
* consistent behavior across all devices
|
|
*/
|
|
@Deprecated
|
|
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;
|
|
|
|
/**
|
|
* Constant represents that the fetching font data was rejected by system. This happens if
|
|
* the passed context is restricted.
|
|
*/
|
|
public static final int STATUS_REJECTED = 3;
|
|
|
|
/** @hide */
|
|
@IntDef(prefix = { "STATUS_" }, value = {
|
|
STATUS_OK,
|
|
STATUS_WRONG_CERTIFICATES,
|
|
STATUS_UNEXPECTED_DATA_PROVIDED
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
@interface FontResultStatus {}
|
|
|
|
private final @FontResultStatus int mStatusCode;
|
|
private final FontInfo[] mFonts;
|
|
|
|
/** @hide */
|
|
public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
|
|
mStatusCode = statusCode;
|
|
mFonts = fonts;
|
|
}
|
|
|
|
public @FontResultStatus int getStatusCode() {
|
|
return mStatusCode;
|
|
}
|
|
|
|
public @NonNull FontInfo[] getFonts() {
|
|
return mFonts;
|
|
}
|
|
}
|
|
|
|
private static final int THREAD_RENEWAL_THRESHOLD_MS = 10000;
|
|
|
|
private static final long SYNC_FONT_FETCH_TIMEOUT_MS = 500;
|
|
|
|
// We use a background thread to post the content resolving work for all requests on. This
|
|
// thread should be quit/stopped after all requests are done.
|
|
// TODO: Factor out to other class. Consider to switch MessageQueue.IdleHandler.
|
|
private static final Runnable sReplaceDispatcherThreadRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
synchronized (sLock) {
|
|
if (sThread != null) {
|
|
sThread.quitSafely();
|
|
sThread = null;
|
|
sHandler = null;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/** @hide */
|
|
public static Typeface getFontSync(FontRequest request) {
|
|
final String id = request.getIdentifier();
|
|
Typeface cachedTypeface = sTypefaceCache.get(id);
|
|
if (cachedTypeface != null) {
|
|
return cachedTypeface;
|
|
}
|
|
|
|
Log.w(TAG, "Platform version of downloadable fonts is deprecated. Please use"
|
|
+ " androidx version instead.");
|
|
|
|
synchronized (sLock) {
|
|
// It is possible that Font is loaded during the thread sleep time
|
|
// re-check the cache to avoid re-loading the font
|
|
cachedTypeface = sTypefaceCache.get(id);
|
|
if (cachedTypeface != null) {
|
|
return cachedTypeface;
|
|
}
|
|
|
|
// Unfortunately the typeface is not available at this time, but requesting from
|
|
// the font provider takes too much time. For now, request the font data to ensure
|
|
// it is in the cache next time and return.
|
|
if (sHandler == null) {
|
|
// Use FOREGROUND priority as this thread will block UI thread.
|
|
sThread = new HandlerThread("fonts", Process.THREAD_PRIORITY_FOREGROUND);
|
|
sThread.start();
|
|
sHandler = new Handler(sThread.getLooper());
|
|
}
|
|
final Lock lock = new ReentrantLock();
|
|
final Condition cond = lock.newCondition();
|
|
final AtomicReference<Typeface> holder = new AtomicReference<>();
|
|
final AtomicBoolean waiting = new AtomicBoolean(true);
|
|
final AtomicBoolean timeout = new AtomicBoolean(false);
|
|
|
|
sHandler.post(() -> {
|
|
try {
|
|
FontFamilyResult result = fetchFonts(sContext, null, request);
|
|
if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
|
|
Typeface typeface = buildTypeface(sContext, null, result.getFonts());
|
|
if (typeface != null) {
|
|
sTypefaceCache.put(id, typeface);
|
|
}
|
|
holder.set(typeface);
|
|
}
|
|
} catch (NameNotFoundException e) {
|
|
// Ignore.
|
|
}
|
|
lock.lock();
|
|
try {
|
|
if (!timeout.get()) {
|
|
waiting.set(false);
|
|
cond.signal();
|
|
}
|
|
} finally {
|
|
lock.unlock();
|
|
}
|
|
});
|
|
sHandler.removeCallbacks(sReplaceDispatcherThreadRunnable);
|
|
sHandler.postDelayed(sReplaceDispatcherThreadRunnable, THREAD_RENEWAL_THRESHOLD_MS);
|
|
|
|
long remaining = TimeUnit.MILLISECONDS.toNanos(SYNC_FONT_FETCH_TIMEOUT_MS);
|
|
lock.lock();
|
|
try {
|
|
if (!waiting.get()) {
|
|
return holder.get();
|
|
}
|
|
for (;;) {
|
|
try {
|
|
remaining = cond.awaitNanos(remaining);
|
|
} catch (InterruptedException e) {
|
|
// do nothing.
|
|
}
|
|
if (!waiting.get()) {
|
|
return holder.get();
|
|
}
|
|
if (remaining <= 0) {
|
|
timeout.set(true);
|
|
Log.w(TAG, "Remote font fetch timed out: " +
|
|
request.getProviderAuthority() + "/" + request.getQuery());
|
|
return null;
|
|
}
|
|
}
|
|
} finally {
|
|
lock.unlock();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface used to receive asynchronously fetched typefaces.
|
|
*
|
|
* @deprecated Use the {@link androidx.core.provider.FontsContractCompat.FontRequestCallback}
|
|
* for consistent behavior across all devices
|
|
*/
|
|
@Deprecated
|
|
public static class FontRequestCallback {
|
|
/**
|
|
* 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 = -1;
|
|
/**
|
|
* 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 = -2;
|
|
/**
|
|
* 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 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 */
|
|
@IntDef(prefix = { "FAIL_" }, value = {
|
|
FAIL_REASON_PROVIDER_NOT_FOUND,
|
|
FAIL_REASON_FONT_LOAD_ERROR,
|
|
FAIL_REASON_FONT_NOT_FOUND,
|
|
FAIL_REASON_FONT_UNAVAILABLE,
|
|
FAIL_REASON_MALFORMED_QUERY
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
@interface FontRequestFailReason {}
|
|
|
|
public FontRequestCallback() {}
|
|
|
|
/**
|
|
* Called then a Typeface request done via {@link #requestFonts} 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 #requestFonts}} fails.
|
|
* @param reason 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} or
|
|
* {@link #FAIL_REASON_MALFORMED_QUERY} if returned by the system. May also be
|
|
* a positive value greater than 0 defined by the font provider as an
|
|
* additional error code. Refer to the provider's documentation for more
|
|
* information on possible returned error codes.
|
|
*/
|
|
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.
|
|
*
|
|
* Note that the result Typeface may be cached internally and the same instance will be returned
|
|
* the next time you call this method with the same request. If you want to bypass this cache,
|
|
* use {@link #fetchFonts} and {@link #buildTypeface} instead.
|
|
*
|
|
* @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 handler A handler to be processed the font fetching.
|
|
* @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 callback A callback that will be triggered when results are obtained. May not be null.
|
|
*/
|
|
public static void requestFonts(@NonNull Context context, @NonNull FontRequest request,
|
|
@NonNull Handler handler, @Nullable CancellationSignal cancellationSignal,
|
|
@NonNull FontRequestCallback callback) {
|
|
|
|
final Handler callerThreadHandler = new Handler();
|
|
final Typeface cachedTypeface = sTypefaceCache.get(request.getIdentifier());
|
|
if (cachedTypeface != null) {
|
|
callerThreadHandler.post(() -> callback.onTypefaceRetrieved(cachedTypeface));
|
|
return;
|
|
}
|
|
|
|
handler.post(() -> {
|
|
FontFamilyResult result;
|
|
try {
|
|
result = fetchFonts(context, cancellationSignal, request);
|
|
} catch (NameNotFoundException e) {
|
|
callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
|
|
FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND));
|
|
return;
|
|
}
|
|
|
|
// Same request might be dispatched during fetchFonts. Check the cache again.
|
|
final Typeface anotherCachedTypeface = sTypefaceCache.get(request.getIdentifier());
|
|
if (anotherCachedTypeface != null) {
|
|
callerThreadHandler.post(() -> callback.onTypefaceRetrieved(anotherCachedTypeface));
|
|
return;
|
|
}
|
|
|
|
if (result.getStatusCode() != FontFamilyResult.STATUS_OK) {
|
|
switch (result.getStatusCode()) {
|
|
case FontFamilyResult.STATUS_WRONG_CERTIFICATES:
|
|
callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
|
|
FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES));
|
|
return;
|
|
case FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED:
|
|
callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
|
|
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
|
|
return;
|
|
default:
|
|
// fetchFont returns unexpected status type. Fallback to load error.
|
|
callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
|
|
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
|
|
return;
|
|
}
|
|
}
|
|
|
|
final FontInfo[] fonts = result.getFonts();
|
|
if (fonts == null || fonts.length == 0) {
|
|
callerThreadHandler.post(() -> 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(() -> callback.onTypefaceRequestFailed(
|
|
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
|
|
} else {
|
|
callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
|
|
resultCode));
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
final Typeface typeface = buildTypeface(context, cancellationSignal, 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(() -> callback.onTypefaceRequestFailed(
|
|
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
|
|
return;
|
|
}
|
|
|
|
sTypefaceCache.put(request.getIdentifier(), typeface);
|
|
callerThreadHandler.post(() -> callback.onTypefaceRetrieved(typeface));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 NameNotFoundException If requested package or authority was not found in system.
|
|
*/
|
|
public static @NonNull FontFamilyResult fetchFonts(
|
|
@NonNull Context context, @Nullable CancellationSignal cancellationSignal,
|
|
@NonNull FontRequest request) throws NameNotFoundException {
|
|
if (context.isRestricted()) {
|
|
// TODO: Should we allow if the peer process is system or myself?
|
|
return new FontFamilyResult(FontFamilyResult.STATUS_REJECTED, null);
|
|
}
|
|
ProviderInfo providerInfo = getProvider(context.getPackageManager(), request);
|
|
if (providerInfo == null) {
|
|
return new FontFamilyResult(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);
|
|
|
|
}
|
|
try {
|
|
FontInfo[] fonts = getFontFromProvider(
|
|
context, request, providerInfo.authority, cancellationSignal);
|
|
return new FontFamilyResult(FontFamilyResult.STATUS_OK, fonts);
|
|
} catch (IllegalArgumentException e) {
|
|
return new FontFamilyResult(FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED, null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public static Typeface buildTypeface(@NonNull Context context,
|
|
@Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts) {
|
|
if (context.isRestricted()) {
|
|
// TODO: Should we allow if the peer process is system or myself?
|
|
return null;
|
|
}
|
|
final Map<Uri, ByteBuffer> uriBuffer =
|
|
prepareFontData(context, fonts, cancellationSignal);
|
|
if (uriBuffer.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
FontFamily.Builder familyBuilder = null;
|
|
for (FontInfo fontInfo : fonts) {
|
|
final ByteBuffer buffer = uriBuffer.get(fontInfo.getUri());
|
|
if (buffer == null) {
|
|
continue;
|
|
}
|
|
try {
|
|
final Font font = new Font.Builder(buffer)
|
|
.setWeight(fontInfo.getWeight())
|
|
.setSlant(fontInfo.isItalic()
|
|
? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT)
|
|
.setTtcIndex(fontInfo.getTtcIndex())
|
|
.setFontVariationSettings(fontInfo.getAxes())
|
|
.build();
|
|
if (familyBuilder == null) {
|
|
familyBuilder = new FontFamily.Builder(font);
|
|
} else {
|
|
familyBuilder.addFont(font);
|
|
}
|
|
} catch (IllegalArgumentException e) {
|
|
// To be a compatible behavior with API28 or before, catch IllegalArgumentExcetpion
|
|
// thrown by native code and returns null.
|
|
return null;
|
|
} catch (IOException e) {
|
|
continue;
|
|
}
|
|
}
|
|
if (familyBuilder == null) {
|
|
return null;
|
|
}
|
|
|
|
final FontFamily family = familyBuilder.build();
|
|
|
|
final FontStyle normal = new FontStyle(FontStyle.FONT_WEIGHT_NORMAL,
|
|
FontStyle.FONT_SLANT_UPRIGHT);
|
|
Font bestFont = family.getFont(0);
|
|
int bestScore = normal.getMatchScore(bestFont.getStyle());
|
|
for (int i = 1; i < family.getSize(); ++i) {
|
|
final Font candidate = family.getFont(i);
|
|
final int score = normal.getMatchScore(candidate.getStyle());
|
|
if (score < bestScore) {
|
|
bestFont = candidate;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
return new Typeface.CustomFallbackBuilder(family).setStyle(bestFont.getStyle()).build();
|
|
}
|
|
|
|
/**
|
|
* 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}.
|
|
*/
|
|
private static Map<Uri, ByteBuffer> prepareFontData(Context context, FontInfo[] fonts,
|
|
CancellationSignal cancellationSignal) {
|
|
final HashMap<Uri, ByteBuffer> out = new HashMap<>();
|
|
final ContentResolver resolver = context.getContentResolver();
|
|
|
|
for (FontInfo font : fonts) {
|
|
if (font.getResultCode() != Columns.RESULT_CODE_OK) {
|
|
continue;
|
|
}
|
|
|
|
final Uri uri = font.getUri();
|
|
if (out.containsKey(uri)) {
|
|
continue;
|
|
}
|
|
|
|
ByteBuffer buffer = null;
|
|
try (final ParcelFileDescriptor pfd =
|
|
resolver.openFileDescriptor(uri, "r", cancellationSignal)) {
|
|
if (pfd != null) {
|
|
try (final FileInputStream fis =
|
|
new FileInputStream(pfd.getFileDescriptor())) {
|
|
final FileChannel fileChannel = fis.getChannel();
|
|
final long size = fileChannel.size();
|
|
buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
|
|
} catch (IOException e) {
|
|
// ignore
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
// ignore
|
|
}
|
|
|
|
// TODO: try other approach?, e.g. read all contents instead of mmap.
|
|
|
|
out.put(uri, buffer);
|
|
}
|
|
return Collections.unmodifiableMap(out);
|
|
}
|
|
|
|
/** @hide */
|
|
@VisibleForTesting
|
|
public static @Nullable ProviderInfo getProvider(
|
|
PackageManager packageManager, FontRequest request) throws NameNotFoundException {
|
|
String providerAuthority = request.getProviderAuthority();
|
|
ProviderInfo info = packageManager.resolveContentProvider(providerAuthority, 0);
|
|
if (info == null) {
|
|
throw new NameNotFoundException("No package found for authority: " + providerAuthority);
|
|
}
|
|
|
|
if (!info.packageName.equals(request.getProviderPackage())) {
|
|
throw new NameNotFoundException("Found content provider " + providerAuthority
|
|
+ ", but package was not " + request.getProviderPackage());
|
|
}
|
|
// Trust system apps without signature checks
|
|
if (info.applicationInfo.isSystemApp()) {
|
|
return info;
|
|
}
|
|
|
|
List<byte[]> signatures;
|
|
PackageInfo packageInfo = packageManager.getPackageInfo(info.packageName,
|
|
PackageManager.GET_SIGNATURES);
|
|
signatures = convertToByteArrayList(packageInfo.signatures);
|
|
Collections.sort(signatures, sByteArrayComparator);
|
|
|
|
List<List<byte[]>> requestCertificatesList = request.getCertificates();
|
|
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 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[]> shas = new ArrayList<>();
|
|
for (int i = 0; i < signatures.length; ++i) {
|
|
shas.add(signatures[i].toByteArray());
|
|
}
|
|
return shas;
|
|
}
|
|
|
|
/** @hide */
|
|
@VisibleForTesting
|
|
public static @NonNull 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();
|
|
try (Cursor 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);) {
|
|
// TODO: Should we restrict the amount of fonts that can be returned?
|
|
// TODO: Write documentation explaining that all results should be from the same family.
|
|
if (cursor != null && cursor.getCount() > 0) {
|
|
final int resultCodeColumnIndex = cursor.getColumnIndex(Columns.RESULT_CODE);
|
|
result = new ArrayList<>();
|
|
final int idColumnIndex = cursor.getColumnIndexOrThrow(Columns._ID);
|
|
final int fileIdColumnIndex = cursor.getColumnIndex(Columns.FILE_ID);
|
|
final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX);
|
|
final int vsColumnIndex = cursor.getColumnIndex(Columns.VARIATION_SETTINGS);
|
|
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;
|
|
final String variationSettings = vsColumnIndex != -1
|
|
? cursor.getString(vsColumnIndex) : null;
|
|
|
|
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;
|
|
boolean italic;
|
|
if (weightColumnIndex != -1 && italicColumnIndex != -1) {
|
|
weight = cursor.getInt(weightColumnIndex);
|
|
italic = cursor.getInt(italicColumnIndex) == 1;
|
|
} else {
|
|
weight = Typeface.Builder.NORMAL_WEIGHT;
|
|
italic = false;
|
|
}
|
|
FontVariationAxis[] axes =
|
|
FontVariationAxis.fromFontVariationSettings(variationSettings);
|
|
result.add(new FontInfo(fileUri, ttcIndex, axes, weight, italic, resultCode));
|
|
}
|
|
}
|
|
}
|
|
return result.toArray(new FontInfo[0]);
|
|
}
|
|
}
|