Merge "Load font file from remote provider synchronously." into oc-dev

This commit is contained in:
Seigo Nonaka
2017-04-26 17:49:53 +00:00
committed by Android (Google) Code Review
4 changed files with 179 additions and 35 deletions

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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;
}