diff options
4 files changed, 179 insertions, 35 deletions
diff --git a/core/java/android/provider/FontsContract.java b/core/java/android/provider/FontsContract.java index 4621763df5a7..f6ad17d24dc2 100644 --- a/core/java/android/provider/FontsContract.java +++ b/core/java/android/provider/FontsContract.java @@ -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 } diff --git a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java index 78c7f4d8716b..51916ffce726 100644 --- a/core/tests/coretests/src/android/provider/FontsContractE2ETest.java +++ b/core/tests/coretests/src/android/provider/FontsContractE2ETest.java @@ -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(); + } } diff --git a/core/tests/coretests/src/android/provider/MockFontProvider.java b/core/tests/coretests/src/android/provider/MockFontProvider.java index 339d5c3f7fa0..ad5b130ca90d 100644 --- a/core/tests/coretests/src/android/provider/MockFontProvider.java +++ b/core/tests/coretests/src/android/provider/MockFontProvider.java @@ -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 diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index aedf6a723861..60b1542671f4 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -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; } |