Loading core/java/android/provider/FontsContract.java +68 −16 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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. Loading @@ -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) { Loading @@ -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) { Loading @@ -353,16 +358,51 @@ 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(); } } } /** * Interface used to receive asynchronously fetched typefaces. Loading Loading @@ -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) Loading Loading @@ -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(); } Loading Loading @@ -651,14 +697,20 @@ public class FontsContract { ByteBuffer buffer = null; try (final ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal); final FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) { 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 } // TODO: try other approach?, e.g. read all contents instead of mmap. Loading core/tests/coretests/src/android/provider/FontsContractE2ETest.java +37 −9 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); } } core/tests/coretests/src/android/provider/MockFontProvider.java +73 −9 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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<>(); Loading @@ -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); } Loading Loading @@ -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; } } Loading @@ -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 Loading graphics/java/android/graphics/Typeface.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; } Loading Loading
core/java/android/provider/FontsContract.java +68 −16 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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. Loading @@ -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) { Loading @@ -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) { Loading @@ -353,16 +358,51 @@ 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(); } } } /** * Interface used to receive asynchronously fetched typefaces. Loading Loading @@ -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) Loading Loading @@ -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(); } Loading Loading @@ -651,14 +697,20 @@ public class FontsContract { ByteBuffer buffer = null; try (final ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal); final FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) { 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 } // TODO: try other approach?, e.g. read all contents instead of mmap. Loading
core/tests/coretests/src/android/provider/FontsContractE2ETest.java +37 −9 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); } }
core/tests/coretests/src/android/provider/MockFontProvider.java +73 −9 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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<>(); Loading @@ -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); } Loading Loading @@ -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; } } Loading @@ -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 Loading
graphics/java/android/graphics/Typeface.java +1 −1 Original line number Diff line number Diff line Loading @@ -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; } Loading