Cache the Typeface based on the FontRequest.

Bug: 37471729
Test: FontsContractE2ETest
Change-Id: Ifb1bd2b50077471404f5f1dffc01e7697d7042e6
This commit is contained in:
Seigo Nonaka
2017-04-19 12:16:39 -07:00
parent ab7a2879eb
commit daa8dfc690
7 changed files with 595 additions and 1 deletions

View File

@@ -44,6 +44,7 @@ import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.ResultReceiver;
import android.util.Log;
import android.util.LruCache;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -181,6 +182,8 @@ public class FontsContract {
@GuardedBy("mLock")
private HandlerThread mThread;
private static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
/** @hide */
public FontsContract(Context context) {
mContext = context.getApplicationContext();
@@ -476,6 +479,11 @@ public class FontsContract {
* therefore the result is delivered to the given callback. See {@link FontRequest}.
* Only one of the methods in callback will be invoked, depending on whether the request
* succeeds or fails. These calls will happen on the caller thread.
*
* Note that the result Typeface may be cached internally and the same instance will be returned
* the next time you call this method with the same request. If you want to bypass this cache,
* use {@link #fetchFonts} and {@link #buildTypeface} instead.
*
* @param context A context to be used for fetching from font provider.
* @param request A {@link FontRequest} object that identifies the provider and query for the
* request. May not be null.
@@ -486,8 +494,13 @@ public class FontsContract {
@NonNull FontRequestCallback callback, @NonNull Handler handler) {
final Handler callerThreadHandler = new Handler();
final Typeface cachedTypeface = sTypefaceCache.get(request.getIdentifier());
if (cachedTypeface != null) {
callerThreadHandler.post(() -> callback.onTypefaceRetrieved(cachedTypeface));
return;
}
handler.post(() -> {
// TODO: Cache the result.
FontFamilyResult result;
try {
result = fetchFonts(context, null /* cancellation signal */, request);
@@ -497,6 +510,13 @@ public class FontsContract {
return;
}
// Same request might be dispatched during fetchFonts. Check the cache again.
final Typeface anotherCachedTypeface = sTypefaceCache.get(request.getIdentifier());
if (anotherCachedTypeface != null) {
callerThreadHandler.post(() -> callback.onTypefaceRetrieved(anotherCachedTypeface));
return;
}
if (result.getStatusCode() != FontFamilyResult.STATUS_OK) {
switch (result.getStatusCode()) {
case FontFamilyResult.STATUS_WRONG_CERTIFICATES:
@@ -547,6 +567,7 @@ public class FontsContract {
return;
}
sTypefaceCache.put(request.getIdentifier(), typeface);
callerThreadHandler.post(() -> callback.onTypefaceRetrieved(typeface));
});
}

View File

@@ -1266,6 +1266,11 @@
<meta-data android:name="com.android.frameworks.coretests.reference" android:resource="@xml/metadata" />
</provider>
<provider android:name="android.provider.MockFontProvider"
android:authorities="android.provider.fonts.font"
android:exported="false"
android:multiprocess="true" />
<!-- Application components used for content tests -->
<provider android:name="android.content.MemoryFileProvider"
android:authorities="android.content.MemoryFileProvider"

Binary file not shown.

View File

@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (C) 2017 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.
-->
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="3.0">
<GlyphOrder>
<GlyphID id="0" name=".notdef"/>
<GlyphID id="1" name="a"/>
</GlyphOrder>
<head>
<tableVersion value="1.0"/>
<fontRevision value="1.0"/>
<checkSumAdjustment value="0x640cdb2f"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
<created value="Fri Mar 17 07:26:00 2017"/>
<macStyle value="00000000 00000000"/>
<lowestRecPPEM value="7"/>
<fontDirectionHint value="2"/>
<glyphDataFormat value="0"/>
</head>
<hhea>
<tableVersion value="1.0"/>
<ascent value="1000"/>
<descent value="-200"/>
<lineGap value="0"/>
<caretSlopeRise value="1"/>
<caretSlopeRun value="0"/>
<caretOffset value="0"/>
<reserved0 value="0"/>
<reserved1 value="0"/>
<reserved2 value="0"/>
<reserved3 value="0"/>
<metricDataFormat value="0"/>
</hhea>
<maxp>
<tableVersion value="0x10000"/>
<maxZones value="0"/>
<maxTwilightPoints value="0"/>
<maxStorage value="0"/>
<maxFunctionDefs value="0"/>
<maxInstructionDefs value="0"/>
<maxStackElements value="0"/>
<maxSizeOfInstructions value="0"/>
<maxComponentElements value="0"/>
</maxp>
<OS_2>
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
will be recalculated by the compiler -->
<version value="3"/>
<xAvgCharWidth value="594"/>
<usWeightClass value="400"/>
<usWidthClass value="5"/>
<fsType value="00000000 00001000"/>
<ySubscriptXSize value="650"/>
<ySubscriptYSize value="600"/>
<ySubscriptXOffset value="0"/>
<ySubscriptYOffset value="75"/>
<ySuperscriptXSize value="650"/>
<ySuperscriptYSize value="600"/>
<ySuperscriptXOffset value="0"/>
<ySuperscriptYOffset value="350"/>
<yStrikeoutSize value="50"/>
<yStrikeoutPosition value="300"/>
<sFamilyClass value="0"/>
<panose>
<bFamilyType value="0"/>
<bSerifStyle value="0"/>
<bWeight value="5"/>
<bProportion value="0"/>
<bContrast value="0"/>
<bStrokeVariation value="0"/>
<bArmStyle value="0"/>
<bLetterForm value="0"/>
<bMidline value="0"/>
<bXHeight value="0"/>
</panose>
<ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
<ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
<achVendID value="UKWN"/>
<fsSelection value="00000000 01000000"/>
<usFirstCharIndex value="32"/>
<usLastCharIndex value="122"/>
<sTypoAscender value="800"/>
<sTypoDescender value="-200"/>
<sTypoLineGap value="200"/>
<usWinAscent value="1000"/>
<usWinDescent value="200"/>
<ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
<ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
<sxHeight value="500"/>
<sCapHeight value="700"/>
<usDefaultChar value="0"/>
<usBreakChar value="32"/>
<usMaxContext value="0"/>
</OS_2>
<hmtx>
<mtx name=".notdef" width="500" lsb="93"/>
<mtx name="a" width="500" lsb="93"/>
</hmtx>
<cmap>
<tableVersion version="0"/>
<cmap_format_4 platformID="3" platEncID="10" language="0">
<map code="0x0061" name="a" />
</cmap_format_4>
</cmap>
<loca>
<!-- The 'loca' table will be calculated by the compiler -->
</loca>
<glyf>
<TTGlyph name=".notdef" xMin="0" yMin="0" xMax="0" yMax="0" />
<TTGlyph name="a" xMin="0" yMin="0" xMax="0" yMax="0" />
</glyf>
<name>
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x0" unicode="True">
Sample Font
</namerecord>
<namerecord nameID="2" platformID="1" platEncID="0" langID="0x0" unicode="True">
Regular
</namerecord>
<namerecord nameID="4" platformID="1" platEncID="0" langID="0x0" unicode="True">
Sample Font
</namerecord>
<namerecord nameID="6" platformID="1" platEncID="0" langID="0x0" unicode="True">
SampleFont-Regular
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
Sample Font
</namerecord>
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
Sample Font
</namerecord>
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
SampleFont-Regular
</namerecord>
</name>
<post>
<formatType value="3.0"/>
<italicAngle value="0.0"/>
<underlinePosition value="-75"/>
<underlineThickness value="50"/>
<isFixedPitch value="0"/>
<minMemType42 value="0"/>
<maxMemType42 value="0"/>
<minMemType1 value="0"/>
<maxMemType1 value="0"/>
</post>
</ttFont>

View File

@@ -0,0 +1,175 @@
/*
* Copyright (C) 2017 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 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 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.graphics.Typeface;
import android.graphics.fonts.FontRequest;
import android.provider.FontsContract;
import android.provider.FontsContract.FontFamilyResult;
import android.provider.FontsContract.FontInfo;
import android.provider.FontsContract.Columns;
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 org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class FontsContractE2ETest {
private static final String AUTHORITY = "android.provider.fonts.font";
private static final String PACKAGE = "com.android.frameworks.coretests";
// Signature to be used for authentication to access content provider.
// In this test case, the content provider and consumer live in the same package, self package's
// signature works.
private static List<List<byte[]>> SIGNATURE;
static {
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
try {
PackageManager manager = context.getPackageManager();
PackageInfo info = manager.getPackageInfo(
context.getPackageName(), PackageManager.GET_SIGNATURES);
ArrayList<byte[]> out = new ArrayList<>();
for (Signature sig : info.signatures) {
out.add(sig.toByteArray());
}
SIGNATURE = new ArrayList<>();
SIGNATURE.add(out);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}
@Before
public void setUp() {
MockFontProvider.prepareFontFiles(
InstrumentationRegistry.getInstrumentation().getTargetContext());
}
@After
public void tearDown() {
MockFontProvider.cleanUpFontFiles(
InstrumentationRegistry.getInstrumentation().getTargetContext());
}
private static class TestCallback extends FontsContract.FontRequestCallback {
private Typeface mTypeface;
private int mSuccessCallCount;
private int mFailedCallCount;
public void onTypefaceRetrieved(Typeface typeface) {
mTypeface = typeface;
mSuccessCallCount++;
}
public void onTypefaceRequestFailed(int reason) {
mFailedCallCount++;
}
public Typeface getTypeface() {
return mTypeface;
}
public int getSuccessCallCount() {
return mSuccessCallCount;
}
public int getFailedCallCount() {
return mFailedCallCount;
}
}
@Test
public void typefaceCacheTest() throws NameNotFoundException {
Instrumentation inst = InstrumentationRegistry.getInstrumentation();
Context ctx = inst.getTargetContext();
final TestCallback callback = new TestCallback();
inst.runOnMainSync(() -> {
FontRequest request = new FontRequest(
AUTHORITY, PACKAGE, "singleFontFamily", SIGNATURE);
FontsContract.requestFont(ctx, request, callback, new Handler());
});
inst.waitForIdleSync();
assertEquals(1, callback.getSuccessCallCount());
assertEquals(0, callback.getFailedCallCount());
assertNotNull(callback.getTypeface());
final TestCallback callback2 = new TestCallback();
inst.runOnMainSync(() -> {
FontRequest request = new FontRequest(
AUTHORITY, PACKAGE, "singleFontFamily", SIGNATURE);
FontsContract.requestFont(ctx, request, callback2, new Handler());
});
inst.waitForIdleSync();
assertEquals(1, callback2.getSuccessCallCount());
assertEquals(0, callback2.getFailedCallCount());
assertSame(callback.getTypeface(), callback2.getTypeface());
final TestCallback callback3 = new TestCallback();
inst.runOnMainSync(() -> {
FontRequest request = new FontRequest(
AUTHORITY, PACKAGE, "singleFontFamily2", SIGNATURE);
FontsContract.requestFont(ctx, request, callback3, new Handler());
});
inst.waitForIdleSync();
assertEquals(1, callback3.getSuccessCallCount());
assertEquals(0, callback3.getFailedCallCount());
assertNotSame(callback.getTypeface(), callback3.getTypeface());
}
@Test
public void typefaceNotCacheTest() throws NameNotFoundException {
Instrumentation inst = InstrumentationRegistry.getInstrumentation();
Context ctx = inst.getTargetContext();
FontRequest request = new FontRequest(
AUTHORITY, PACKAGE, "singleFontFamily", SIGNATURE);
FontFamilyResult result = FontsContract.fetchFonts(
ctx, null /* cancellation signal */, request);
assertEquals(FontFamilyResult.STATUS_OK, result.getStatusCode());
Typeface typeface = FontsContract.buildTypeface(
ctx, null /* cancellation signal */, result.getFonts());
FontFamilyResult result2 = FontsContract.fetchFonts(
ctx, null /* cancellation signal */, request);
assertEquals(FontFamilyResult.STATUS_OK, result2.getStatusCode());
Typeface typeface2 = FontsContract.buildTypeface(
ctx, null /* cancellation signal */, result2.getFonts());
// Neighter fetchFonts nor buildTypeface should cache the Typeface.
assertNotSame(typeface, typeface2);
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2017 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 android.provider;
import static android.provider.FontsContract.Columns;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.fonts.FontVariationAxis;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
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.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.nio.file.StandardCopyOption;
public class MockFontProvider extends ContentProvider {
final static String AUTHORITY = "android.provider.fonts.font";
final static String[] FONT_FILES = {
"samplefont1.ttf",
};
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,
int resultCode) {
mId = id;
mFileId = fileId;
mTtcIndex = ttcIndex;
mVarSettings = varSettings;
mWeight = weight;
mItalic = italic;
mResultCode = resultCode;
}
public int getId() {
return mId;
}
public int getTtcIndex() {
return mTtcIndex;
}
public String getVarSettings() {
return mVarSettings;
}
public int getWeight() {
return mWeight;
}
public int getItalic() {
return mItalic;
}
public int getResultCode() {
return mResultCode;
}
public int getFileId() {
return mFileId;
}
private int mId;
private int mFileId;
private int mTtcIndex;
private String mVarSettings;
private int mWeight;
private int mItalic;
private int mResultCode;
};
private static Map<String, Font[]> QUERY_MAP;
static {
HashMap<String, Font[]> map = new HashMap<>();
int id = 0;
map.put("singleFontFamily", new Font[] {
new Font(id++, SAMPLE_FONT_FILE_0_ID, 0, null, 400, 0, Columns.RESULT_CODE_OK),
});
map.put("singleFontFamily2", new Font[] {
new Font(id++, SAMPLE_FONT_FILE_0_ID, 0, null, 700, 0, Columns.RESULT_CODE_OK),
});
QUERY_MAP = Collections.unmodifiableMap(map);
}
private static Cursor buildCursor(Font[] in) {
MatrixCursor cursor = new MatrixCursor(new String[] {
Columns._ID, Columns.TTC_INDEX, Columns.VARIATION_SETTINGS, Columns.WEIGHT,
Columns.ITALIC, Columns.RESULT_CODE, Columns.FILE_ID});
for (Font font : in) {
MatrixCursor.RowBuilder builder = cursor.newRow();
builder.add(Columns._ID, font.getId());
builder.add(Columns.FILE_ID, font.getFileId());
builder.add(Columns.TTC_INDEX, font.getTtcIndex());
builder.add(Columns.VARIATION_SETTINGS, font.getVarSettings());
builder.add(Columns.WEIGHT, font.getWeight());
builder.add(Columns.ITALIC, font.getItalic());
builder.add(Columns.RESULT_CODE, font.getResultCode());
}
return cursor;
}
public MockFontProvider() {
}
public static void prepareFontFiles(Context context) {
final AssetManager mgr = context.getAssets();
for (String file : FONT_FILES) {
try (InputStream is = mgr.open("fonts/" + file)) {
Files.copy(is, getCopiedFile(context, file).toPath(),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void cleanUpFontFiles(Context context) {
for (String file : FONT_FILES) {
getCopiedFile(context, file).delete();
}
}
public static File getCopiedFile(Context context, String path) {
return new File(context.getFilesDir(), path);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) {
final int id = (int)ContentUris.parseId(uri);
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");
}
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
return "vnd.android.cursor.dir/vnd.android.provider.font";
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
return buildCursor(QUERY_MAP.get(selectionArgs[0]));
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("insert is not supported.");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("delete is not supported.");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("update is not supported.");
}
}

View File

@@ -35,6 +35,9 @@ public final class FontRequest implements Parcelable {
private final String mQuery;
private final List<List<byte[]>> mCertificates;
// Used for key of the cache.
private final String mIdentifier;
/**
* @param providerAuthority The authority of the Font Provider to be used for the request. This
* should be a system installed app.
@@ -49,6 +52,8 @@ public final class FontRequest implements Parcelable {
mQuery = Preconditions.checkNotNull(query);
mProviderPackage = Preconditions.checkNotNull(providerPackage);
mCertificates = Collections.emptyList();
mIdentifier = new StringBuilder(mProviderAuthority).append("-").append(mProviderPackage)
.append("-").append(mQuery).toString();
}
/**
@@ -68,6 +73,8 @@ public final class FontRequest implements Parcelable {
mProviderPackage = Preconditions.checkNotNull(providerPackage);
mQuery = Preconditions.checkNotNull(query);
mCertificates = Preconditions.checkNotNull(certificates);
mIdentifier = new StringBuilder(mProviderAuthority).append("-").append(mProviderPackage)
.append("-").append(mQuery).toString();
}
/**
@@ -102,6 +109,11 @@ public final class FontRequest implements Parcelable {
return mCertificates;
}
/** @hide */
public String getIdentifier() {
return mIdentifier;
}
@Override
public int describeContents() {
return 0;
@@ -121,6 +133,8 @@ public final class FontRequest implements Parcelable {
mQuery = in.readString();
mCertificates = new ArrayList<>();
in.readList(mCertificates, null);
mIdentifier = new StringBuilder(mProviderAuthority).append("-").append(mProviderPackage)
.append("-").append(mQuery).toString();
}
public static final Parcelable.Creator<FontRequest> CREATOR =