/*
* 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 static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_WRONG_CERTIFICATES;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES;
import static androidx.core.provider.FontsContractCompat.FontRequestCallback.RESULT_SUCCESS;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.os.Handler;
import android.os.Process;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.LruCache;
import androidx.collection.SimpleArrayMap;
import androidx.core.content.res.FontResourcesParserCompat;
import androidx.core.graphics.TypefaceCompat;
import androidx.core.provider.FontsContractCompat.FontFamilyResult;
import androidx.core.provider.FontsContractCompat.FontRequestCallback.FontRequestFailReason;
import androidx.core.util.Consumer;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
/**
* Given a {@link FontRequest}, loads the Typeface. Handles the sync/async nature of the calls,
* and also makes use of {@link RequestExecutor#createDefaultExecutor} to load Typeface
* asynchronously.
*/
class FontRequestWorker {
private FontRequestWorker() {}
static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
private static final ExecutorService DEFAULT_EXECUTOR_SERVICE = RequestExecutor
.createDefaultExecutor(
"fonts-androidx",
Process.THREAD_PRIORITY_BACKGROUND,
10000 /* keepAliveTime */
);
/** Package protected to prevent synthetic accessor */
static final Object LOCK = new Object();
/** Package protected to prevent synthetic accessor */
@GuardedBy("LOCK")
static final SimpleArrayMap<String, ArrayList<Consumer<TypefaceResult>>> PENDING_REPLIES =
new SimpleArrayMap<>();
static void resetTypefaceCache() {
sTypefaceCache.evictAll();
}
/**
* Requests a Font to be loaded synchronously.
* - If timeoutInMillis is infinite -> calls in the same thread as callee.
* - If timeoutInMillis is NOT infinite -> calls in a bg thread with the timeout, and waits
* on the bg task.
*
* Before returning the result, callback is called with the request result.
*
* @param context
* @param request FontRequest that defines the font to be loaded.
* @param callback the callback to be called.
* @param style Typeface Style such as {@link Typeface#NORMAL}, {@link Typeface#BOLD}
* {@link Typeface#ITALIC}, {@link Typeface#BOLD_ITALIC}.
* @param timeoutInMillis timeout in milliseconds for the request.
* @return
*/
static Typeface requestFontSync(
@NonNull final Context context,
@NonNull final FontRequest request,
@NonNull final CallbackWithHandler callback,
final int style,
int timeoutInMillis
) {
final String id = createCacheId(request, style);
Typeface cached = sTypefaceCache.get(id);
if (cached != null) {
callback.onTypefaceResult(new TypefaceResult(cached));
return cached;
}
// when timeout is infinite, do not post to bg thread, since it will block other requests
if (timeoutInMillis == FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE) {
// Wait forever. No need to post to the thread.
TypefaceResult typefaceResult = getFontSync(id, context, request, style);
callback.onTypefaceResult(typefaceResult);
return typefaceResult.mTypeface;
}
final Callable<TypefaceResult> fetcher = new Callable<TypefaceResult>() {
@Override
public TypefaceResult call() {
return getFontSync(id, context, request, style);
}
};
try {
TypefaceResult typefaceResult = RequestExecutor.submit(
DEFAULT_EXECUTOR_SERVICE,
fetcher,
timeoutInMillis
);
callback.onTypefaceResult(typefaceResult);
return typefaceResult.mTypeface;
} catch (InterruptedException e) {
callback.onTypefaceResult(new TypefaceResult(FAIL_REASON_FONT_LOAD_ERROR));
return null;
}
}
/**
* Request a Font to be loaded async.
*
* The {@link FontRequest} is executed on executor, and the callback is called on the
* {@link Handler} that is contained in {@link CallbackWithHandler}.
*
*
* @param context
* @param request FontRequest for the font to be loaded.
* @param style Typeface Style such as {@link Typeface#NORMAL}, {@link Typeface#BOLD} ads asd
* {@link Typeface#ITALIC}, {@link Typeface#BOLD_ITALIC}.
* @param executor Executor instance to execute the request. If null is provided
* DEFAULT_EXECUTOR_SERVICE will be used.
* @param callback callback to be called for async FontRequest result.
* {@link CallbackWithHandler} contains the Handler to call the
* callback on.
* @return
*/
static Typeface requestFontAsync(
@NonNull final Context context,
@NonNull final FontRequest request,
final int style,
@Nullable final Executor executor,
@NonNull final CallbackWithHandler callback
) {
final String id = createCacheId(request, style);
Typeface cached = sTypefaceCache.get(id);
if (cached != null) {
callback.onTypefaceResult(new TypefaceResult(cached));
return cached;
}
final Consumer<TypefaceResult> reply = new Consumer<TypefaceResult>() {
@Override
public void accept(TypefaceResult typefaceResult) {
callback.onTypefaceResult(typefaceResult);
}
};
synchronized (LOCK) {
ArrayList<Consumer<TypefaceResult>> pendingReplies = PENDING_REPLIES.get(id);
if (pendingReplies != null) {
// Already requested. Do not request the same provider again and insert the
// reply to the queue instead.
pendingReplies.add(reply);
return null;
}
pendingReplies = new ArrayList<>();
pendingReplies.add(reply);
PENDING_REPLIES.put(id, pendingReplies);
}
final Callable<TypefaceResult> fetcher = new Callable<TypefaceResult>() {
@Override
public TypefaceResult call() {
TypefaceResult typeface = getFontSync(id, context, request, style);
return typeface;
}
};
Executor finalExecutor = executor == null ? DEFAULT_EXECUTOR_SERVICE : executor;
RequestExecutor.execute(finalExecutor, fetcher, new Consumer<TypefaceResult>() {
@Override
public void accept(TypefaceResult typefaceResult) {
final ArrayList<Consumer<TypefaceResult>> replies;
synchronized (LOCK) {
replies = PENDING_REPLIES.get(id);
if (replies == null) {
return; // Nobody requested replies. Do nothing.
}
PENDING_REPLIES.remove(id);
}
for (int i = 0; i < replies.size(); ++i) {
replies.get(i).accept(typefaceResult);
}
}
});
return null;
}
private static String createCacheId(@NonNull FontRequest request, int style) {
return request.getId() + "-" + style;
}
/** Package protected to prevent synthetic accessor */
@NonNull
static TypefaceResult getFontSync(
@NonNull final String cacheId,
@NonNull final Context context,
@NonNull final FontRequest request,
int style
) {
Typeface cached = sTypefaceCache.get(cacheId);
if (cached != null) {
return new TypefaceResult(cached);
}
FontFamilyResult result;
try {
result = FontProvider.getFontFamilyResult(context, request, null);
} catch (PackageManager.NameNotFoundException e) {
return new TypefaceResult(FAIL_REASON_PROVIDER_NOT_FOUND);
}
int fontFamilyResultStatus = getFontFamilyResultStatus(result);
if (fontFamilyResultStatus != RESULT_SUCCESS) {
return new TypefaceResult(fontFamilyResultStatus);
}
final Typeface typeface = TypefaceCompat.createFromFontInfo(
context, null /* CancellationSignal */, result.getFonts(), style);
if (typeface != null) {
sTypefaceCache.put(cacheId, typeface);
return new TypefaceResult(typeface);
} else {
return new TypefaceResult(FAIL_REASON_FONT_LOAD_ERROR);
}
}
@SuppressLint("WrongConstant")
@FontRequestFailReason
private static int getFontFamilyResultStatus(@NonNull FontFamilyResult fontFamilyResult) {
if (fontFamilyResult.getStatusCode() != FontFamilyResult.STATUS_OK) {
switch (fontFamilyResult.getStatusCode()) {
case STATUS_WRONG_CERTIFICATES:
return FAIL_REASON_WRONG_CERTIFICATES;
default:
return FAIL_REASON_FONT_LOAD_ERROR;
}
} else {
final FontsContractCompat.FontInfo[] fonts = fontFamilyResult.getFonts();
if (fonts == null || fonts.length == 0) {
return FAIL_REASON_FONT_NOT_FOUND;
}
for (final FontsContractCompat.FontInfo font : fonts) {
// We proceed if all font entry is ready to use. Otherwise report the first
// error.
final int resultCode = font.getResultCode();
if (resultCode != FontsContractCompat.Columns.RESULT_CODE_OK) {
// Negative values are reserved for internal errors. Fallback to load
// error.
return resultCode < 0 ? FAIL_REASON_FONT_LOAD_ERROR : resultCode;
}
}
return RESULT_SUCCESS;
}
}
static final class TypefaceResult {
final Typeface mTypeface;
@FontRequestFailReason final int mResult;
TypefaceResult(@FontRequestFailReason int result) {
mTypeface = null;
mResult = result;
}
@SuppressLint("WrongConstant")
TypefaceResult(@NonNull Typeface typeface) {
mTypeface = typeface;
mResult = RESULT_SUCCESS;
}
@SuppressLint("WrongConstant")
boolean isSuccess() {
return mResult == RESULT_SUCCESS;
}
}
}