Implement support for downloadable font requests in xml. Given the
xml fonts feature in O, this adds support to not only declare
local font files as font resources, but also Downloadable fonts
from a fonts provider.
A provider returns a font family (of one or more files) given a
query, so the new attributes are added to the font-family tag.
Additionally, add support to pre-declare downloadable font resources
in the Android Manifest. These will then be fetched at app startup
time so they are available to use from the Typeface cache asap.
When retrieving downloadable fonts via resources, the cache is
checked to see if the font is already there and is used, otherwise
a request is sent to the provider and the default font is returned
as we need a result synchronously.
To do this, the developer declares an additional fonts xml resource
file with the list of fonts to preload and links it in the manifest
with a meta-data tag.
E.g.:
res/font/mydownloadedfont.xml
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
android:fontProviderAuthority="com.example.test.fontprovider"
android:fontProviderQuery="myrequestedfont">
</font-family>
res/font/preloaded_fonts.xml
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
<font android:font="@font/mydownloadedfont" />
</font-family>
and in the AndroidManifest.xml
<meta-data android:name="preloaded_fonts"
android:resource="@font/preloaded_fonts" />
Bug:
34660500,
34658116
Test: WIP, need to add more
Change-Id: I1d92555e115e241bf23b59e6f5c6cca6c7361de7
field public static final int font = 16844082; // 0x1010532
field public static final int fontFamily = 16843692; // 0x10103ac
field public static final int fontFeatureSettings = 16843959; // 0x10104b7
+ field public static final int fontProviderAuthority = 16844114; // 0x1010552
+ field public static final int fontProviderQuery = 16844115; // 0x1010553
field public static final int fontStyle = 16844081; // 0x1010531
field public static final int fontWeight = 16844083; // 0x1010533
field public static final int footerDividersEnabled = 16843311; // 0x101022f
field public static final int font = 16844082; // 0x1010532
field public static final int fontFamily = 16843692; // 0x10103ac
field public static final int fontFeatureSettings = 16843959; // 0x10104b7
+ field public static final int fontProviderAuthority = 16844114; // 0x1010552
+ field public static final int fontProviderQuery = 16844115; // 0x1010553
field public static final int fontStyle = 16844081; // 0x1010531
field public static final int fontWeight = 16844083; // 0x1010533
field public static final int footerDividersEnabled = 16843311; // 0x101022f
field public static final int font = 16844082; // 0x1010532
field public static final int fontFamily = 16843692; // 0x10103ac
field public static final int fontFeatureSettings = 16843959; // 0x10104b7
+ field public static final int fontProviderAuthority = 16844114; // 0x1010552
+ field public static final int fontProviderQuery = 16844115; // 0x1010553
field public static final int fontStyle = 16844081; // 0x1010531
field public static final int fontWeight = 16844083; // 0x1010533
field public static final int footerDividersEnabled = 16843311; // 0x101022f
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
+
+ // Preload fonts resources
+ try {
+ final ApplicationInfo info =
+ sPackageManager.getApplicationInfo(
+ data.appInfo.packageName,
+ PackageManager.GET_META_DATA /*flags*/,
+ UserHandle.myUserId());
+ if (info.metaData != null) {
+ final int preloadedFontsResource = info.metaData.getInt(
+ ApplicationInfo.METADATA_PRELOADED_FONTS, 0);
+ if (preloadedFontsResource != 0) {
+ data.info.mResources.preloadFonts(preloadedFontsResource);
+ }
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/*package*/ final void finishInstrumentation(int resultCode, Bundle results) {
public int privateFlags;
/**
+ * @hide
+ */
+ public static final String METADATA_PRELOADED_FONTS = "preloaded_fonts";
+
+ /**
* The required smallest screen width the application can run on. If 0,
* nothing has been specified. Comes from
* {@link android.R.styleable#AndroidManifestSupportsScreens_requiresSmallestWidthDp
private static FontConfig.Family readFamily(XmlPullParser parser, Resources resources)
throws XmlPullParserException, IOException {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ TypedArray array = resources.obtainAttributes(attrs, R.styleable.FontFamily);
+ String authority = array.getString(R.styleable.FontFamily_fontProviderAuthority);
+ String query = array.getString(R.styleable.FontFamily_fontProviderQuery);
+ array.recycle();
+ if (authority != null && query != null) {
+ while (parser.next() != XmlPullParser.END_TAG) {
+ skip(parser);
+ }
+ return new FontConfig.Family(authority, query);
+ }
List<FontConfig.Font> fonts = new ArrayList<>();
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
int weight = array.getInt(R.styleable.FontFamilyFont_fontWeight, NORMAL_WEIGHT);
boolean isItalic = ITALIC == array.getInt(R.styleable.FontFamilyFont_fontStyle, 0);
String filename = array.getString(R.styleable.FontFamilyFont_font);
+ int resourceId = array.getResourceId(R.styleable.FontFamilyFont_font, 0);
array.recycle();
while (parser.next() != XmlPullParser.END_TAG) {
skip(parser);
}
- return new FontConfig.Font(filename, 0, null, weight, isItalic);
+ return new FontConfig.Font(filename, 0, null, weight, isItalic, resourceId);
}
private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
}
/**
+ * @hide
+ */
+ public void preloadFonts(@FontRes int id) {
+ final TypedValue value = obtainTempTypedValue();
+ try {
+ final ResourcesImpl impl = mResourcesImpl;
+ impl.getValue(id, value, true);
+ impl.preloadFonts(this, value, id);
+ } finally {
+ releaseTempTypedValue(value);
+ }
+ }
+
+ /**
* Returns the character sequence necessary for grammatically correct pluralization
* of the given resource ID for the given quantity.
* Note that the character sequence is selected based solely on grammatical necessity,
import android.content.pm.ActivityInfo.Config;
import android.content.res.Configuration.NativeConfig;
import android.content.res.Resources.NotFoundException;
+import android.graphics.FontFamily;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
/**
}
/**
+ * @hide
+ */
+ public void preloadFonts(Resources wrapper, TypedValue value, int id) {
+ if (value.string == null) {
+ throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
+ + Integer.toHexString(id) + ") is not a Font: " + value);
+ }
+
+ final String file = value.string.toString();
+
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
+ try {
+ final XmlResourceParser rp = loadXmlResourceParser(
+ file, id, value.assetCookie, "font");
+ final FontConfig config = FontResourcesParser.parse(rp, wrapper);
+ final List<FontConfig.Family> families = config.getFamilies();
+ if (families == null || families.isEmpty()) {
+ return;
+ }
+ for (int j = 0; j < families.size(); j++) {
+ final FontConfig.Family family = families.get(j);
+ final List<FontConfig.Font> fonts = family.getFonts();
+ for (int i = 0; i < fonts.size(); i++) {
+ int resourceId = fonts.get(i).getResourceId();
+ wrapper.getFont(resourceId);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Failed to parse xml resource " + file, e);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to read xml resource " + file, e);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
* Given the value and id, we can get the XML filename as in value.data, based on that, we
* first try to load CSL from the cache. If not found, try to get from the constant state.
* Last, parse the XML and generate the CSL.
private final int mWeight;
private final boolean mIsItalic;
private ParcelFileDescriptor mFd;
+ private final int mResourceId;
- public Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic) {
+ /**
+ * @hide
+ */
+ public Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic,
+ int resourceId) {
mFontName = fontName;
mTtcIndex = ttcIndex;
mAxes = axes;
mWeight = weight;
mIsItalic = isItalic;
mFd = null;
+ mResourceId = resourceId;
+ }
+
+ public Font(String fontName, int ttcIndex, List<Axis> axes, int weight, boolean isItalic) {
+ this(fontName, ttcIndex, axes, weight, isItalic, 0);
}
public Font(Font origin) {
e.printStackTrace();
}
}
+ mResourceId = origin.mResourceId;
}
/**
/**
* @hide
*/
+ public int getResourceId() {
+ return mResourceId;
+ }
+
+ /**
+ * @hide
+ */
public Font(Parcel in) {
mFontName = in.readString();
mTtcIndex = in.readInt();
} else {
mFd = null;
}
+ mResourceId = in.readInt();
}
@Override
if (mFd != null) {
mFd.writeToParcel(out, flag);
}
+ out.writeInt(mResourceId);
}
@Override
private final List<Font> mFonts;
private final String mLanguage;
private final String mVariant;
+ private final String mProviderAuthority;
+ private final String mQuery;
public Family(String name, List<Font> fonts, String language, String variant) {
- this.mName = name;
- this.mFonts = fonts;
- this.mLanguage = language;
- this.mVariant = variant;
+ mName = name;
+ mFonts = fonts;
+ mLanguage = language;
+ mVariant = variant;
+ mProviderAuthority = null;
+ mQuery = null;
+ }
+
+ /**
+ * @hide
+ */
+ public Family(String providerAuthority, String query) {
+ mName = null;
+ mFonts = null;
+ mLanguage = null;
+ mVariant = null;
+ mProviderAuthority = providerAuthority;
+ mQuery = query;
}
public Family(Family origin) {
- this.mName = origin.mName;
- this.mLanguage = origin.mLanguage;
- this.mVariant = origin.mVariant;
- this.mFonts = new ArrayList<>();
+ mName = origin.mName;
+ mLanguage = origin.mLanguage;
+ mVariant = origin.mVariant;
+ mFonts = new ArrayList<>();
for (int i = 0; i < origin.mFonts.size(); i++) {
mFonts.add(new Font(origin.mFonts.get(i)));
}
+ mProviderAuthority = origin.mProviderAuthority;
+ mQuery = origin.mQuery;
}
/**
/**
* @hide
*/
+ public String getProviderAuthority() {
+ return mProviderAuthority;
+ }
+
+ /**
+ * @hide
+ */
+ public String getQuery() {
+ return mQuery;
+ }
+
+ /**
+ * @hide
+ */
public Family(Parcel in) {
mName = in.readString();
final int size = in.readInt();
}
mLanguage = in.readString();
mVariant = in.readString();
+ if (in.readInt() == 1) {
+ mProviderAuthority = in.readString();
+ } else {
+ mProviderAuthority = null;
+ }
+ if (in.readInt() == 1) {
+ mQuery = in.readString();
+ } else {
+ mQuery = null;
+ }
}
@Override
}
out.writeString(mLanguage);
out.writeString(mVariant);
+ out.writeInt(mProviderAuthority == null ? 0 : 1);
+ if (mProviderAuthority != null) {
+ out.writeString(mProviderAuthority);
+ }
+ out.writeInt(mQuery == null ? 0 : 1);
+ if (mQuery != null) {
+ out.writeString(mQuery);
+ }
}
@Override
<attr name="fontWeight" format="integer" />
</declare-styleable>
+ <!-- Attributes that are read when parsing a <fontfamily> tag, -->
+ <declare-styleable name="FontFamily">
+ <attr name="fontProviderAuthority" format="string" />
+ <attr name="fontProviderQuery" format="string" />
+ </declare-styleable>
+
<!-- @hide -->
<declare-styleable name="RecyclerView">
<attr name="layoutManager" format="string" />
<public name="canCaptureFingerprintGestures" />
<public name="alphabeticModifiers" />
<public name="numericModifiers" />
+ <public name="fontProviderAuthority" />
+ <public name="fontProviderQuery" />
</public-group>
<public-group type="style" first-id="0x010302e0">
<public type="attr" name="primaryContentAlpha" />
<public type="attr" name="secondaryContentAlpha" />
+
<!-- ===============================================================
DO NOT ADD UN-GROUPED ITEMS HERE
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<font-family xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fontProviderAuthority="com.example.test.fontprovider"
+ android:fontProviderQuery="MyRequestedFont">
+</font-family>
\ No newline at end of file
*/
package android.content.res;
+import static junit.framework.Assert.assertNull;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
assertEquals(true, font4.isItalic());
assertEquals("res/font/samplefont4.ttf", font4.getFontName());
}
+
+ @Test
+ public void testParseDownloadableFont() throws IOException, XmlPullParserException {
+ XmlResourceParser parser = mResources.getXml(R.font.samplexmldownloadedfont);
+
+ FontConfig result = FontResourcesParser.parse(parser, mResources);
+
+ assertNotNull(result);
+ List<FontConfig.Family> families = result.getFamilies();
+ assertEquals(1, families.size());
+ FontConfig.Family family = families.get(0);
+ assertEquals("com.example.test.fontprovider", family.getProviderAuthority());
+ assertEquals("MyRequestedFont", family.getQuery());
+ assertNull(family.getFonts());
+ }
}
@Nullable
public static Typeface createFromResources(FontConfig config, AssetManager mgr, String path) {
if (sFallbackFonts != null) {
- synchronized (sDynamicTypefaceCache) {
- final String key = createAssetUid(mgr, path);
- Typeface typeface = sDynamicTypefaceCache.get(key);
- if (typeface != null) return typeface;
+ Typeface typeface = findFromCache(mgr, path);
+ if (typeface != null) return typeface;
- List<FontConfig.Family> families = config.getFamilies();
- if (families == null || families.isEmpty()) {
- throw new RuntimeException("Font resource contained no fonts.");
- }
- if (families.size() > 1) {
- throw new RuntimeException("Font resource contained more than one family.");
+ List<FontConfig.Family> families = config.getFamilies();
+ if (families == null || families.isEmpty()) {
+ throw new RuntimeException(
+ "Font resource " + path + " contained no font families.");
+ }
+ if (families.size() > 1) {
+ throw new RuntimeException(
+ "Font resource " + path + " contained more than one family.");
+ }
+ FontConfig.Family family = families.get(0);
+ if (family.getProviderAuthority() != null && family.getQuery() != null) {
+ // Downloadable font
+ typeface = findFromCache(
+ family.getProviderAuthority(), family.getQuery());
+ if (typeface != null) {
+ return typeface;
}
- FontConfig.Family family = families.get(0);
+ // Downloaded font and it wasn't cached, request it again and return a
+ // default font instead (nothing we can do now).
+ create(new FontRequest(family.getProviderAuthority(), family.getQuery()),
+ NO_OP_REQUEST_CALLBACK);
+ return DEFAULT;
+ }
- FontFamily fontFamily = new FontFamily();
- List<FontConfig.Font> fonts = family.getFonts();
- for (int i = 0; i < fonts.size(); i++) {
- FontConfig.Font font = fonts.get(i);
- // TODO: Use style and weight info
- if (!fontFamily.addFontFromAssetManager(mgr, font.getFontName(),
- 0 /* resourceCookie */, false /* isAsset */)) {
- return null;
- }
+ FontFamily fontFamily = new FontFamily();
+ List<FontConfig.Font> fonts = family.getFonts();
+ if (fonts == null || fonts.isEmpty()) {
+ throw new RuntimeException("Font resource " + path + " contained no fonts.");
+ }
+ for (int i = 0; i < fonts.size(); i++) {
+ FontConfig.Font font = fonts.get(i);
+ // TODO: Use style and weight info
+ if (!fontFamily.addFontFromAssetManager(mgr, font.getFontName(),
+ 0 /* resourceCookie */, false /* isAsset */)) {
+ return null;
}
- fontFamily.freeze();
- FontFamily[] familyChain = { fontFamily };
- typeface = createFromFamiliesWithDefault(familyChain);
+ }
+ fontFamily.freeze();
+ FontFamily[] familyChain = { fontFamily };
+ typeface = createFromFamiliesWithDefault(familyChain);
+ synchronized (sDynamicTypefaceCache) {
+ final String key = createAssetUid(mgr, path);
sDynamicTypefaceCache.put(key, typeface);
- return typeface;
}
+ return typeface;
}
return null;
}
void onTypefaceRequestFailed(@FontRequestFailReason int reason);
}
+ private static final FontRequestCallback NO_OP_REQUEST_CALLBACK = new FontRequestCallback() {
+ @Override
+ public void onTypefaceRetrieved(Typeface typeface) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {
+ // Do nothing.
+ }
+ };
+
/**
* Create a typeface object given a family name, and option style information.
* If null is passed for the name, then the "default" font will be chosen.