Merge "Load font file from remote provider synchronously." into oc-dev
am: 1c661d1943
Change-Id: I29b540d331a40ad47ad0561ecefb1269cb76a188
This commit is contained in:
@@ -65,6 +65,12 @@ 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.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Utility class to deal with Font ContentProviders.
|
||||
@@ -303,6 +309,8 @@ public class FontsContract {
|
||||
|
||||
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.
|
||||
@@ -314,14 +322,13 @@ public class FontsContract {
|
||||
sThread.quitSafely();
|
||||
sThread = null;
|
||||
sHandler = null;
|
||||
sInQueueSet = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** @hide */
|
||||
public static Typeface getFontOrWarmUpCache(FontRequest request) {
|
||||
public static Typeface getFontSync(FontRequest request) {
|
||||
final String id = request.getIdentifier();
|
||||
Typeface cachedTypeface = sTypefaceCache.get(id);
|
||||
if (cachedTypeface != null) {
|
||||
@@ -336,16 +343,14 @@ public class FontsContract {
|
||||
sThread = new HandlerThread("fonts", Process.THREAD_PRIORITY_BACKGROUND);
|
||||
sThread.start();
|
||||
sHandler = new Handler(sThread.getLooper());
|
||||
sInQueueSet = new ArraySet<>();
|
||||
}
|
||||
if (sInQueueSet.contains(id)) {
|
||||
return null; // Already requested.
|
||||
}
|
||||
sInQueueSet.add(id);
|
||||
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(() -> {
|
||||
synchronized (sLock) {
|
||||
sInQueueSet.remove(id);
|
||||
}
|
||||
try {
|
||||
FontFamilyResult result = fetchFonts(sContext, null, request);
|
||||
if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
|
||||
@@ -353,15 +358,50 @@ public class FontsContract {
|
||||
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();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -594,6 +634,9 @@ public class FontsContract {
|
||||
}
|
||||
final Map<Uri, ByteBuffer> uriBuffer =
|
||||
prepareFontData(context, fonts, cancellationSignal);
|
||||
if (uriBuffer.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new Typeface.Builder(fonts, uriBuffer)
|
||||
.setFallback(fallbackFontName)
|
||||
.setWeight(weight)
|
||||
@@ -621,6 +664,9 @@ public class FontsContract {
|
||||
}
|
||||
final Map<Uri, ByteBuffer> uriBuffer =
|
||||
prepareFontData(context, fonts, cancellationSignal);
|
||||
if (uriBuffer.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new Typeface.Builder(fonts, uriBuffer).build();
|
||||
}
|
||||
|
||||
@@ -651,11 +697,17 @@ public class FontsContract {
|
||||
|
||||
ByteBuffer buffer = null;
|
||||
try (final ParcelFileDescriptor pfd =
|
||||
resolver.openFileDescriptor(uri, "r", cancellationSignal);
|
||||
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);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -18,27 +18,28 @@ package android.provider;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.pm.Signature;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.fonts.FontRequest;
|
||||
import android.provider.FontsContract;
|
||||
import android.os.Handler;
|
||||
import android.provider.FontsContract.Columns;
|
||||
import android.provider.FontsContract.FontFamilyResult;
|
||||
import android.provider.FontsContract.FontInfo;
|
||||
import android.provider.FontsContract.Columns;
|
||||
import android.provider.FontsContract;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.os.Handler;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -172,4 +173,31 @@ public class FontsContractE2ETest {
|
||||
// Neighter fetchFonts nor buildTypeface should cache the Typeface.
|
||||
assertNotSame(typeface, typeface2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typefaceNullFdTest() throws NameNotFoundException {
|
||||
Instrumentation inst = InstrumentationRegistry.getInstrumentation();
|
||||
Context ctx = inst.getTargetContext();
|
||||
|
||||
FontRequest request = new FontRequest(
|
||||
AUTHORITY, PACKAGE, MockFontProvider.NULL_FD_QUERY, SIGNATURE);
|
||||
FontFamilyResult result = FontsContract.fetchFonts(
|
||||
ctx, null /* cancellation signal */, request);
|
||||
assertNull(FontsContract.buildTypeface(
|
||||
ctx, null /* cancellation signal */, result.getFonts()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFontSyncTest() {
|
||||
FontRequest request = new FontRequest(AUTHORITY, PACKAGE, "singleFontFamily", SIGNATURE);
|
||||
assertNotNull(FontsContract.getFontSync(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFontSyncTest_timeout() {
|
||||
FontRequest request = new FontRequest(
|
||||
AUTHORITY, PACKAGE, MockFontProvider.BLOCKING_QUERY, SIGNATURE);
|
||||
assertNull(FontsContract.getFontSync(request));
|
||||
MockFontProvider.unblock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,26 +29,73 @@ import android.graphics.fonts.FontVariationAxis;
|
||||
import android.net.Uri;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.ArraySet;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
|
||||
public class MockFontProvider extends ContentProvider {
|
||||
final static String AUTHORITY = "android.provider.fonts.font";
|
||||
|
||||
private static final long BLOCKING_TIMEOUT_MS = 10000; // 10 sec
|
||||
private static final Lock sLock = new ReentrantLock();
|
||||
private static final Condition sCond = sLock.newCondition();
|
||||
@GuardedBy("sLock")
|
||||
private static boolean sSignaled;
|
||||
|
||||
private static void blockUntilSignal() {
|
||||
long remaining = TimeUnit.MILLISECONDS.toNanos(BLOCKING_TIMEOUT_MS);
|
||||
sLock.lock();
|
||||
try {
|
||||
sSignaled = false;
|
||||
while (!sSignaled) {
|
||||
try {
|
||||
remaining = sCond.awaitNanos(remaining);
|
||||
} catch (InterruptedException e) {
|
||||
// do nothing.
|
||||
}
|
||||
if (sSignaled) {
|
||||
return;
|
||||
}
|
||||
if (remaining <= 0) {
|
||||
// Timed out
|
||||
throw new RuntimeException("Timeout during waiting");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static void unblock() {
|
||||
sLock.lock();
|
||||
try {
|
||||
sSignaled = true;
|
||||
sCond.signal();
|
||||
} finally {
|
||||
sLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
final static String[] FONT_FILES = {
|
||||
"samplefont1.ttf",
|
||||
};
|
||||
private static final int NO_FILE_ID = 255;
|
||||
private static final int SAMPLE_FONT_FILE_0_ID = 0;
|
||||
private static final int SAMPLE_FONT_FILE_1_ID = 1;
|
||||
|
||||
static class Font {
|
||||
public Font(int id, int fileId, int ttcIndex, String varSettings, int weight, int italic,
|
||||
@@ -99,6 +146,9 @@ public class MockFontProvider extends ContentProvider {
|
||||
private int mResultCode;
|
||||
};
|
||||
|
||||
public static final String BLOCKING_QUERY = "queryBlockingQuery";
|
||||
public static final String NULL_FD_QUERY = "nullFdQuery";
|
||||
|
||||
private static Map<String, Font[]> QUERY_MAP;
|
||||
static {
|
||||
HashMap<String, Font[]> map = new HashMap<>();
|
||||
@@ -112,6 +162,14 @@ public class MockFontProvider extends ContentProvider {
|
||||
new Font(id++, SAMPLE_FONT_FILE_0_ID, 0, null, 700, 0, Columns.RESULT_CODE_OK),
|
||||
});
|
||||
|
||||
map.put(BLOCKING_QUERY, new Font[] {
|
||||
new Font(id++, SAMPLE_FONT_FILE_0_ID, 0, null, 700, 0, Columns.RESULT_CODE_OK),
|
||||
});
|
||||
|
||||
map.put(NULL_FD_QUERY, new Font[] {
|
||||
new Font(id++, NO_FILE_ID, 0, null, 700, 0, Columns.RESULT_CODE_OK),
|
||||
});
|
||||
|
||||
QUERY_MAP = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
@@ -160,12 +218,14 @@ public class MockFontProvider extends ContentProvider {
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) {
|
||||
final int id = (int)ContentUris.parseId(uri);
|
||||
if (id == NO_FILE_ID) {
|
||||
return null;
|
||||
}
|
||||
final File targetFile = getCopiedFile(getContext(), FONT_FILES[id]);
|
||||
try {
|
||||
return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to found font file. You might forget call prepareFontFiles in setUp");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +242,11 @@ public class MockFontProvider extends ContentProvider {
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return buildCursor(QUERY_MAP.get(selectionArgs[0]));
|
||||
final String query = selectionArgs[0];
|
||||
if (query.equals(BLOCKING_QUERY)) {
|
||||
blockUntilSignal();
|
||||
}
|
||||
return buildCursor(QUERY_MAP.get(query));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -218,7 +218,7 @@ public class Typeface {
|
||||
// default font instead (nothing we can do now).
|
||||
FontRequest request = new FontRequest(providerEntry.getAuthority(),
|
||||
providerEntry.getPackage(), providerEntry.getQuery(), certs);
|
||||
Typeface typeface = FontsContract.getFontOrWarmUpCache(request);
|
||||
Typeface typeface = FontsContract.getFontSync(request);
|
||||
return typeface == null ? DEFAULT : typeface;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user