/*
* 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_UNEXPECTED_DATA_PROVIDED;
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 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.content.res.ResourcesCompat;
import androidx.core.graphics.TypefaceCompat;
import androidx.core.provider.FontRequestThreadPool.ReplyCallback;
import java.util.ArrayList;
import java.util.concurrent.Callable;
class FontRequestWorker {
private FontRequestWorker() {}
static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
private static final FontRequestThreadPool BACKGROUND_THREAD = new FontRequestThreadPool(
"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<ReplyCallback<TypefaceResult>>> PENDING_REPLIES =
new SimpleArrayMap<>();
static void resetTypefaceCache() {
sTypefaceCache.evictAll();
}
/**
* Internal method of requestFont for avoiding holding strong refernece of Context.
*/
@SuppressWarnings("deprecation")
static void requestFontInternal(
final @NonNull Context appContext,
final @NonNull FontRequest request,
final @NonNull FontsContractCompat.FontRequestCallback callback,
final @NonNull Handler handler
) {
final Handler callerHandler = CalleeHandler.create();
handler.post(new Runnable() {
@Override
public void run() {
// TODO: Cache the result.
FontsContractCompat.FontFamilyResult result;
try {
result = FontProvider.getFontFamilyResult(appContext, request, null);
} catch (PackageManager.NameNotFoundException e) {
notifyFailed(callerHandler, callback, FAIL_REASON_PROVIDER_NOT_FOUND);
return;
}
if (result.getStatusCode() != FontsContractCompat.FontFamilyResult.STATUS_OK) {
switch (result.getStatusCode()) {
case STATUS_WRONG_CERTIFICATES:
notifyFailed(callerHandler, callback, FAIL_REASON_WRONG_CERTIFICATES);
return;
case STATUS_UNEXPECTED_DATA_PROVIDED:
notifyFailed(callerHandler, callback, FAIL_REASON_FONT_LOAD_ERROR);
return;
default:
// fetchFont returns unexpected status type. Fallback to load error.
notifyFailed(callerHandler, callback, FAIL_REASON_FONT_LOAD_ERROR);
return;
}
}
final FontsContractCompat.FontInfo[] fonts = result.getFonts();
if (fonts == null || fonts.length == 0) {
notifyFailed(callerHandler, callback, FAIL_REASON_FONT_NOT_FOUND);
return;
}
for (final FontsContractCompat.FontInfo font : fonts) {
if (font.getResultCode() != FontsContractCompat.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.
notifyFailed(callerHandler, callback, FAIL_REASON_FONT_LOAD_ERROR);
} else {
notifyFailed(callerHandler, callback, resultCode);
}
return;
}
}
final Typeface typeface = TypefaceCompat.createFromFontInfo(appContext,
null /* cancellationSignal */,
fonts,
Typeface.NORMAL
);
if (typeface == null) {
// Something went wrong during reading font files. This happens if the given
// font file is an unsupported font type.
notifyFailed(callerHandler, callback, FAIL_REASON_FONT_LOAD_ERROR);
return;
}
notifyRetrieved(callerHandler, callback, typeface);
}
});
}
static void notifyFailed(
@NonNull final Handler callerThreadHandler,
@NonNull final FontsContractCompat.FontRequestCallback callback,
final int code
) {
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRequestFailed(code);
}
});
}
static void notifyRetrieved(
@NonNull final Handler callerThreadHandler,
@NonNull final FontsContractCompat.FontRequestCallback callback,
@NonNull final Typeface typeface
) {
callerThreadHandler.post(new Runnable() {
@Override
public void run() {
callback.onTypefaceRetrieved(typeface);
}
});
}
static Typeface getTypeface(
@NonNull final Context context,
@NonNull final FontRequest request,
@Nullable final ResourcesCompat.FontCallback fontCallback,
@Nullable final Handler handler, boolean isBlockingFetch, int timeout,
final int style) {
final String id = request.getId() + "-" + 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 == FontsContractCompat.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() {
TypefaceResult typeface = getFontInternal(context, request, style);
if (typeface.mTypeface != null) {
sTypefaceCache.put(id, typeface.mTypeface);
}
return typeface;
}
};
if (isBlockingFetch) {
try {
return BACKGROUND_THREAD.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(
FAIL_REASON_FONT_NOT_FOUND, handler);
} else if (typeface.mResult
== FontsContractCompat.FontFamilyResult.STATUS_OK) {
fontCallback.callbackSuccessAsync(typeface.mTypeface, handler);
} else {
fontCallback.callbackFailAsync(typeface.mResult, handler);
}
}
};
synchronized (LOCK) {
ArrayList<ReplyCallback<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.
if (reply != null) {
pendingReplies.add(reply);
}
return null;
}
if (reply != null) {
pendingReplies = new ArrayList<>();
pendingReplies.add(reply);
PENDING_REPLIES.put(id, pendingReplies);
}
}
BACKGROUND_THREAD.postAndReply(fetcher, new ReplyCallback<TypefaceResult>() {
@Override
public void onReply(final TypefaceResult typeface) {
final ArrayList<ReplyCallback<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).onReply(typeface);
}
}
});
return null;
}
}
/** Package protected to prevent synthetic accessor */
@SuppressLint("WrongConstant")
@NonNull
static TypefaceResult getFontInternal(
@NonNull final Context context,
@NonNull final FontRequest request,
int style) {
FontsContractCompat.FontFamilyResult result;
try {
result = FontProvider.getFontFamilyResult(context, request, null);
} catch (PackageManager.NameNotFoundException e) {
return new TypefaceResult(null, FAIL_REASON_PROVIDER_NOT_FOUND);
}
if (result.getStatusCode() == FontsContractCompat.FontFamilyResult.STATUS_OK) {
final Typeface typeface = TypefaceCompat.createFromFontInfo(
context, null /* CancellationSignal */, result.getFonts(), style);
return new TypefaceResult(typeface, typeface != null
? FontsContractCompat.FontRequestCallback.RESULT_SUCCESS
: FAIL_REASON_FONT_LOAD_ERROR);
}
int resultCode = result.getStatusCode() == STATUS_WRONG_CERTIFICATES
? FAIL_REASON_WRONG_CERTIFICATES
: FAIL_REASON_FONT_LOAD_ERROR;
return new TypefaceResult(null, resultCode);
}
private static final class TypefaceResult {
final Typeface mTypeface;
@FontsContractCompat.FontRequestCallback.FontRequestFailReason final int mResult;
TypefaceResult(@Nullable Typeface typeface,
@FontsContractCompat.FontRequestCallback.FontRequestFailReason int result) {
mTypeface = typeface;
mResult = result;
}
}
}