From f91ea1b201492911f40672a30fd1f2b5fe4372a6 Mon Sep 17 00:00:00 2001 From: Kohsuke Yatoh Date: Mon, 8 Feb 2021 16:11:39 -0800 Subject: [PATCH] Add FontListParser#writeFamily(). Bug: 179103383 Test: atest FrameworksCoreTests:FontListParserTest Change-Id: I992acd13dc5dc084a00ee9f3832c4d7111ecd4e4 --- core/java/android/text/FontConfig.java | 114 +++++++- .../android/graphics/FontListParserTest.java | 245 ++++++++++++++++++ .../java/android/graphics/FontListParser.java | 107 +++++++- 3 files changed, 450 insertions(+), 16 deletions(-) create mode 100644 core/tests/coretests/src/android/graphics/FontListParserTest.java diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java index 2de755857b70e..aef185c776339 100644 --- a/core/java/android/text/FontConfig.java +++ b/core/java/android/text/FontConfig.java @@ -36,6 +36,7 @@ import java.io.File; import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** @@ -155,6 +156,32 @@ public final class FontConfig implements Parcelable { } }; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FontConfig that = (FontConfig) o; + return mLastModifiedTimeMillis == that.mLastModifiedTimeMillis + && mConfigVersion == that.mConfigVersion + && Objects.equals(mFamilies, that.mFamilies) + && Objects.equals(mAliases, that.mAliases); + } + + @Override + public int hashCode() { + return Objects.hash(mFamilies, mAliases, mLastModifiedTimeMillis, mConfigVersion); + } + + @Override + public String toString() { + return "FontConfig{" + + "mFamilies=" + mFamilies + + ", mAliases=" + mAliases + + ", mLastModifiedTimeMillis=" + mLastModifiedTimeMillis + + ", mConfigVersion=" + mConfigVersion + + '}'; + } + /** * Represents single font entry in system font configuration. * @@ -317,6 +344,37 @@ public final class FontConfig implements Parcelable { public boolean isItalic() { return getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Font font = (Font) o; + return mIndex == font.mIndex + && Objects.equals(mFile, font.mFile) + && Objects.equals(mOriginalFile, font.mOriginalFile) + && Objects.equals(mStyle, font.mStyle) + && Objects.equals(mFontVariationSettings, font.mFontVariationSettings) + && Objects.equals(mFontFamilyName, font.mFontFamilyName); + } + + @Override + public int hashCode() { + return Objects.hash(mFile, mOriginalFile, mStyle, mIndex, mFontVariationSettings, + mFontFamilyName); + } + + @Override + public String toString() { + return "Font{" + + "mFile=" + mFile + + ", mOriginalFile=" + mOriginalFile + + ", mStyle=" + mStyle + + ", mIndex=" + mIndex + + ", mFontVariationSettings='" + mFontVariationSettings + '\'' + + ", mFontFamilyName='" + mFontFamilyName + '\'' + + '}'; + } } /** @@ -398,6 +456,30 @@ public final class FontConfig implements Parcelable { return new Alias[size]; } }; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Alias alias = (Alias) o; + return mWeight == alias.mWeight + && Objects.equals(mName, alias.mName) + && Objects.equals(mOriginal, alias.mOriginal); + } + + @Override + public int hashCode() { + return Objects.hash(mName, mOriginal, mWeight); + } + + @Override + public String toString() { + return "Alias{" + + "mName='" + mName + '\'' + + ", mOriginal='" + mOriginal + '\'' + + ", mWeight=" + mWeight + + '}'; + } } /** @@ -413,7 +495,7 @@ public final class FontConfig implements Parcelable { public static final class FontFamily implements Parcelable { private final @NonNull List mFonts; private final @Nullable String mName; - private final @Nullable LocaleList mLocaleList; + private final @NonNull LocaleList mLocaleList; private final @Variant int mVariant; /** @hide */ @@ -454,7 +536,7 @@ public final class FontConfig implements Parcelable { * @hide Only system server can create this instance and passed via IPC. */ public FontFamily(@NonNull List fonts, @Nullable String name, - @Nullable LocaleList localeList, @Variant int variant) { + @NonNull LocaleList localeList, @Variant int variant) { mFonts = fonts; mName = name; mLocaleList = localeList; @@ -493,8 +575,6 @@ public final class FontConfig implements Parcelable { * * The locale list will be used for deciding which font family should be used in fallback * list. - * - * @return non-null if a locale list is available. Otherwise null. */ public @NonNull LocaleList getLocaleList() { return mLocaleList; @@ -559,5 +639,31 @@ public final class FontConfig implements Parcelable { public @NonNull String getLanguages() { return mLocaleList.toLanguageTags(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FontFamily that = (FontFamily) o; + return mVariant == that.mVariant + && Objects.equals(mFonts, that.mFonts) + && Objects.equals(mName, that.mName) + && Objects.equals(mLocaleList, that.mLocaleList); + } + + @Override + public int hashCode() { + return Objects.hash(mFonts, mName, mLocaleList, mVariant); + } + + @Override + public String toString() { + return "FontFamily{" + + "mFonts=" + mFonts + + ", mName='" + mName + '\'' + + ", mLocaleList=" + mLocaleList + + ", mVariant=" + mVariant + + '}'; + } } } diff --git a/core/tests/coretests/src/android/graphics/FontListParserTest.java b/core/tests/coretests/src/android/graphics/FontListParserTest.java new file mode 100644 index 0000000000000..eae41e37e5f38 --- /dev/null +++ b/core/tests/coretests/src/android/graphics/FontListParserTest.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2021 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.graphics; + +import static android.graphics.fonts.FontStyle.FONT_SLANT_ITALIC; +import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT; +import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL; +import static android.text.FontConfig.FontFamily.VARIANT_COMPACT; +import static android.text.FontConfig.FontFamily.VARIANT_DEFAULT; +import static android.text.FontConfig.FontFamily.VARIANT_ELEGANT; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.graphics.fonts.FontStyle; +import android.os.LocaleList; +import android.text.FontConfig; +import android.util.TypedXmlSerializer; +import android.util.Xml; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class FontListParserTest { + + @Test + public void named() throws Exception { + String xml = "" + + "" + + " test.ttf" + + ""; + FontConfig.FontFamily expected = new FontConfig.FontFamily( + Arrays.asList( + new FontConfig.Font(new File("test.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "", null)), + "sans-serif", LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT); + + FontConfig.FontFamily family = readFamily(xml); + assertThat(family).isEqualTo(expected); + + String serialized = writeFamily(family); + assertWithMessage("serialized = " + serialized) + .that(readFamily(serialized)).isEqualTo(expected); + } + + @Test + public void fallback() throws Exception { + String xml = "" + + "" + + " test.ttf" + + " test.ttf" + + ""; + FontConfig.FontFamily expected = new FontConfig.FontFamily( + Arrays.asList( + new FontConfig.Font(new File("test.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "", null), + new FontConfig.Font(new File("test.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "", "serif")), + null, LocaleList.forLanguageTags("en"), VARIANT_DEFAULT); + + FontConfig.FontFamily family = readFamily(xml); + assertThat(family).isEqualTo(expected); + + String serialized = writeFamily(family); + assertWithMessage("serialized = " + serialized) + .that(readFamily(serialized)).isEqualTo(expected); + } + + @Test + public void compact() throws Exception { + String xml = "" + + "" + + " test.ttf" + + ""; + FontConfig.FontFamily expected = new FontConfig.FontFamily( + Arrays.asList( + new FontConfig.Font(new File("test.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "", null)), + null, LocaleList.forLanguageTags("en"), VARIANT_COMPACT); + + FontConfig.FontFamily family = readFamily(xml); + assertThat(family).isEqualTo(expected); + + String serialized = writeFamily(family); + assertWithMessage("serialized = " + serialized) + .that(readFamily(serialized)).isEqualTo(expected); + } + + @Test + public void elegant() throws Exception { + String xml = "" + + "" + + " test.ttf" + + ""; + FontConfig.FontFamily expected = new FontConfig.FontFamily( + Arrays.asList( + new FontConfig.Font(new File("test.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "", null)), + null, LocaleList.forLanguageTags("en"), VARIANT_ELEGANT); + + FontConfig.FontFamily family = readFamily(xml); + assertThat(family).isEqualTo(expected); + + String serialized = writeFamily(family); + assertWithMessage("serialized = " + serialized) + .that(readFamily(serialized)).isEqualTo(expected); + } + + @Test + public void styles() throws Exception { + String xml = "" + + "" + + " normal.ttf" + + " weight.ttf" + + " italic.ttf" + + ""; + FontConfig.FontFamily expected = new FontConfig.FontFamily( + Arrays.asList( + new FontConfig.Font(new File("normal.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "", null), + new FontConfig.Font(new File("weight.ttf"), null, + new FontStyle(100, FONT_SLANT_UPRIGHT), + 0, "", null), + new FontConfig.Font(new File("italic.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_ITALIC), + 0, "", null)), + "sans-serif", LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT); + FontConfig.FontFamily family = readFamily(xml); + assertThat(family).isEqualTo(expected); + + String serialized = writeFamily(family); + assertWithMessage("serialized = " + serialized) + .that(readFamily(serialized)).isEqualTo(expected); + } + + @Test + public void variable() throws Exception { + String xml = "" + + "" + + " test-VF.ttf" + + " " + + " " + + " " + + " test-VF.ttf" + + " " + + " " + + " " + + ""; + FontConfig.FontFamily expected = new FontConfig.FontFamily( + Arrays.asList( + new FontConfig.Font(new File("test-VF.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "'wdth' 100.0,'wght' 200.0", null), + new FontConfig.Font(new File("test-VF.ttf"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "'wdth' 400.0,'wght' 700.0", null)), + "sans-serif", LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT); + FontConfig.FontFamily family = readFamily(xml); + assertThat(family).isEqualTo(expected); + + String serialized = writeFamily(family); + assertWithMessage("serialized = " + serialized) + .that(readFamily(serialized)).isEqualTo(expected); + } + + @Test + public void ttc() throws Exception { + String xml = "" + + "" + + " test.ttc" + + " test.ttc" + + ""; + FontConfig.FontFamily expected = new FontConfig.FontFamily( + Arrays.asList( + new FontConfig.Font(new File("test.ttc"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 0, "", null), + new FontConfig.Font(new File("test.ttc"), null, + new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT), + 1, "", null)), + "sans-serif", LocaleList.getEmptyLocaleList(), VARIANT_DEFAULT); + FontConfig.FontFamily family = readFamily(xml); + assertThat(family).isEqualTo(expected); + + String serialized = writeFamily(family); + assertWithMessage("serialized = " + serialized) + .that(readFamily(serialized)).isEqualTo(expected); + } + + private FontConfig.FontFamily readFamily(String xml) + throws IOException, XmlPullParserException { + StandardCharsets.UTF_8.name(); + ByteArrayInputStream buffer = new ByteArrayInputStream( + xml.getBytes(StandardCharsets.UTF_8)); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(buffer, "UTF-8"); + parser.nextTag(); + return FontListParser.readFamily(parser, "", null); + } + + private String writeFamily(FontConfig.FontFamily family) throws IOException { + TypedXmlSerializer out = Xml.newFastSerializer(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + out.setOutput(buffer, "UTF-8"); + out.startTag(null, "family"); + FontListParser.writeFamily(out, family); + out.endTag(null, "family"); + out.endDocument(); + return buffer.toString("UTF-8"); + } +} diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index bb795cd99e6e1..63df0db99764d 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -25,6 +25,8 @@ import android.graphics.fonts.FontVariationAxis; import android.os.Build; import android.os.LocaleList; import android.text.FontConfig; +import android.text.TextUtils; +import android.util.TypedXmlSerializer; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; @@ -45,6 +47,27 @@ import java.util.regex.Pattern; */ public class FontListParser { + // XML constants for FontFamily. + private static final String ATTR_NAME = "name"; + private static final String ATTR_LANG = "lang"; + private static final String ATTR_VARIANT = "variant"; + private static final String TAG_FONT = "font"; + private static final String VARIANT_COMPACT = "compact"; + private static final String VARIANT_ELEGANT = "elegant"; + + // XML constants for Font. + public static final String ATTR_INDEX = "index"; + public static final String ATTR_WEIGHT = "weight"; + public static final String ATTR_STYLE = "style"; + public static final String ATTR_FALLBACK_FOR = "fallbackFor"; + public static final String STYLE_ITALIC = "italic"; + public static final String STYLE_NORMAL = "normal"; + public static final String TAG_AXIS = "axis"; + + // XML constants for FontVariationAxis. + public static final String ATTR_TAG = "tag"; + public static final String ATTR_STYLEVALUE = "stylevalue"; + /* Parse fallback list (no names) */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static FontConfig parse(InputStream in) throws XmlPullParserException, IOException { @@ -148,7 +171,7 @@ public class FontListParser { while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() != XmlPullParser.START_TAG) continue; final String tag = parser.getName(); - if (tag.equals("font")) { + if (tag.equals(TAG_FONT)) { fonts.add(readFont(parser, fontDir, updatableFontMap)); } else { skip(parser); @@ -156,15 +179,41 @@ public class FontListParser { } int intVariant = FontConfig.FontFamily.VARIANT_DEFAULT; if (variant != null) { - if (variant.equals("compact")) { + if (variant.equals(VARIANT_COMPACT)) { intVariant = FontConfig.FontFamily.VARIANT_COMPACT; - } else if (variant.equals("elegant")) { + } else if (variant.equals(VARIANT_ELEGANT)) { intVariant = FontConfig.FontFamily.VARIANT_ELEGANT; } } return new FontConfig.FontFamily(fonts, name, LocaleList.forLanguageTags(lang), intVariant); } + /** + * Write a family tag representing {@code fontFamily}. The tag should be started by the caller. + */ + public static void writeFamily(TypedXmlSerializer out, FontConfig.FontFamily fontFamily) + throws IOException { + if (!TextUtils.isEmpty(fontFamily.getName())) { + out.attribute(null, ATTR_NAME, fontFamily.getName()); + } + if (!fontFamily.getLocaleList().isEmpty()) { + out.attribute(null, ATTR_LANG, fontFamily.getLocaleList().toLanguageTags()); + } + switch (fontFamily.getVariant()) { + case FontConfig.FontFamily.VARIANT_COMPACT: + out.attribute(null, ATTR_VARIANT, VARIANT_COMPACT); + break; + case FontConfig.FontFamily.VARIANT_ELEGANT: + out.attribute(null, ATTR_VARIANT, VARIANT_ELEGANT); + break; + } + for (FontConfig.Font font : fontFamily.getFontList()) { + out.startTag(null, TAG_FONT); + writeFont(out, font); + out.endTag(null, TAG_FONT); + } + } + /** Matches leading and trailing XML whitespace. */ private static final Pattern FILENAME_WHITESPACE_PATTERN = Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$"); @@ -175,13 +224,13 @@ public class FontListParser { @Nullable Map updatableFontMap) throws XmlPullParserException, IOException { - String indexStr = parser.getAttributeValue(null, "index"); + String indexStr = parser.getAttributeValue(null, ATTR_INDEX); int index = indexStr == null ? 0 : Integer.parseInt(indexStr); List axes = new ArrayList<>(); - String weightStr = parser.getAttributeValue(null, "weight"); - int weight = weightStr == null ? 400 : Integer.parseInt(weightStr); - boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style")); - String fallbackFor = parser.getAttributeValue(null, "fallbackFor"); + String weightStr = parser.getAttributeValue(null, ATTR_WEIGHT); + int weight = weightStr == null ? FontStyle.FONT_WEIGHT_NORMAL : Integer.parseInt(weightStr); + boolean isItalic = STYLE_ITALIC.equals(parser.getAttributeValue(null, ATTR_STYLE)); + String fallbackFor = parser.getAttributeValue(null, ATTR_FALLBACK_FOR); StringBuilder filename = new StringBuilder(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.TEXT) { @@ -189,7 +238,7 @@ public class FontListParser { } if (parser.getEventType() != XmlPullParser.START_TAG) continue; String tag = parser.getName(); - if (tag.equals("axis")) { + if (tag.equals(TAG_AXIS)) { axes.add(readAxis(parser)); } else { skip(parser); @@ -237,14 +286,48 @@ public class FontListParser { return null; } + private static void writeFont(TypedXmlSerializer out, FontConfig.Font font) + throws IOException { + if (font.getTtcIndex() != 0) { + out.attributeInt(null, ATTR_INDEX, font.getTtcIndex()); + } + if (font.getStyle().getWeight() != FontStyle.FONT_WEIGHT_NORMAL) { + out.attributeInt(null, ATTR_WEIGHT, font.getStyle().getWeight()); + } + if (font.getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC) { + out.attribute(null, ATTR_STYLE, STYLE_ITALIC); + } else { + out.attribute(null, ATTR_STYLE, STYLE_NORMAL); + } + if (!TextUtils.isEmpty(font.getFontFamilyName())) { + out.attribute(null, ATTR_FALLBACK_FOR, font.getFontFamilyName()); + } + out.text(font.getFile().getName()); + FontVariationAxis[] axes = + FontVariationAxis.fromFontVariationSettings(font.getFontVariationSettings()); + if (axes != null) { + for (FontVariationAxis axis : axes) { + out.startTag(null, TAG_AXIS); + writeAxis(out, axis); + out.endTag(null, TAG_AXIS); + } + } + } + private static FontVariationAxis readAxis(XmlPullParser parser) throws XmlPullParserException, IOException { - String tagStr = parser.getAttributeValue(null, "tag"); - String styleValueStr = parser.getAttributeValue(null, "stylevalue"); + String tagStr = parser.getAttributeValue(null, ATTR_TAG); + String styleValueStr = parser.getAttributeValue(null, ATTR_STYLEVALUE); skip(parser); // axis tag is empty, ignore any contents and consume end tag return new FontVariationAxis(tagStr, Float.parseFloat(styleValueStr)); } + private static void writeAxis(TypedXmlSerializer out, FontVariationAxis axis) + throws IOException { + out.attribute(null, ATTR_TAG, axis.getTag()); + out.attributeFloat(null, ATTR_STYLEVALUE, axis.getStyleValue()); + } + /** * Reads alias elements */ @@ -255,7 +338,7 @@ public class FontListParser { String weightStr = parser.getAttributeValue(null, "weight"); int weight; if (weightStr == null) { - weight = 400; + weight = FontStyle.FONT_WEIGHT_NORMAL; } else { weight = Integer.parseInt(weightStr); }