Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e7455a81 authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Support Locale Fallback Family Customization

Bug: 303327287
Test: atest TypefaceSystemFallbackTest
Test: atest CtsTextTestCases CtsGraphcisTestCases
Change-Id: I50754faf91d6d7222366f26a640f4dd4c1b156b0
parent 73a14015
Loading
Loading
Loading
Loading
+147 −2
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.fonts.FontFamily.Builder.VariableFontFamilyType;
import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontVariationAxis;
import android.icu.util.ULocale;
import android.os.Build;
import android.os.LocaleList;
import android.os.Parcel;
@@ -39,6 +40,7 @@ import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;


@@ -58,6 +60,7 @@ public final class FontConfig implements Parcelable {
    private final @NonNull List<FontFamily> mFamilies;
    private final @NonNull List<Alias> mAliases;
    private final @NonNull List<NamedFamilyList> mNamedFamilyLists;
    private final @NonNull List<Customization.LocaleFallback> mLocaleFallbackCustomizations;
    private final long mLastModifiedTimeMillis;
    private final int mConfigVersion;

@@ -71,10 +74,12 @@ public final class FontConfig implements Parcelable {
     */
    public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases,
            @NonNull List<NamedFamilyList> namedFamilyLists,
            @NonNull List<Customization.LocaleFallback> localeFallbackCustomizations,
            long lastModifiedTimeMillis, @IntRange(from = 0) int configVersion) {
        mFamilies = families;
        mAliases = aliases;
        mNamedFamilyLists = namedFamilyLists;
        mLocaleFallbackCustomizations = localeFallbackCustomizations;
        mLastModifiedTimeMillis = lastModifiedTimeMillis;
        mConfigVersion = configVersion;
    }
@@ -84,7 +89,8 @@ public final class FontConfig implements Parcelable {
     */
    public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases,
            long lastModifiedTimeMillis, @IntRange(from = 0) int configVersion) {
        this(families, aliases, Collections.emptyList(), lastModifiedTimeMillis, configVersion);
        this(families, aliases, Collections.emptyList(), Collections.emptyList(),
                lastModifiedTimeMillis, configVersion);
    }


@@ -112,6 +118,18 @@ public final class FontConfig implements Parcelable {
        return mNamedFamilyLists;
    }

    /**
     * Returns a locale fallback customizations.
     *
     * This field is used for creating the system fallback in the system server. This field is
     * always empty in the application process.
     *
     * @hide
     */
    public @NonNull List<Customization.LocaleFallback> getLocaleFallbackCustomizations() {
        return mLocaleFallbackCustomizations;
    }

    /**
     * Returns the last modified time in milliseconds.
     *
@@ -169,7 +187,9 @@ public final class FontConfig implements Parcelable {
            source.readTypedList(familyLists, NamedFamilyList.CREATOR);
            long lastModifiedDate = source.readLong();
            int configVersion = source.readInt();
            return new FontConfig(families, aliases, familyLists, lastModifiedDate, configVersion);
            return new FontConfig(families, aliases, familyLists,
                    Collections.emptyList(),  // Don't need to pass customization to API caller.
                    lastModifiedDate, configVersion);
        }

        @Override
@@ -813,4 +833,129 @@ public final class FontConfig implements Parcelable {
                    + '}';
        }
    }

    /** @hide */
    public static class Customization {
        private Customization() {}  // Singleton

        /**
         * A class that represents customization of locale fallback
         *
         * This class represents a vendor customization of new-locale-family.
         *
         * <pre>
         * <family customizationType="new-locale-family" operation="prepend" lang="ja-JP">
         *     <font weight="400" style="normal">MyAlternativeFont.ttf
         *         <axis tag="wght" stylevalue="400"/>
         *     </font>
         * </family>
         * </pre>
         *
         * The operation can be one of prepend, replace or append. The operation prepend means that
         * the new font family is inserted just before the original font family. The original font
         * family is still in the fallback. The operation replace means that the original font
         * family is replaced with new font family. The original font family is removed from the
         * fallback. The operation append means that the new font family is inserted just after the
         * original font family. The original font family is still in the fallback.
         *
         * The lang attribute is a BCP47 compliant language tag. The font fallback mainly uses ISO
         * 15924 script code for matching. If the script code is missing, most likely script code
         * will be used.
         */
        public static class LocaleFallback {
            private final Locale mLocale;
            private final int mOperation;
            private final FontFamily mFamily;
            private final String mScript;

            public static final int OPERATION_PREPEND = 0;
            public static final int OPERATION_APPEND = 1;
            public static final int OPERATION_REPLACE = 2;

            /** @hide */
            @Retention(SOURCE)
            @IntDef(prefix = { "OPERATION_" }, value = {
                    OPERATION_PREPEND,
                    OPERATION_APPEND,
                    OPERATION_REPLACE
            })
            public @interface Operation {}


            public LocaleFallback(@NonNull Locale locale, @Operation int operation,
                    @NonNull FontFamily family) {
                mLocale = locale;
                mOperation = operation;
                mFamily = family;
                mScript = resolveScript(locale);
            }

            /**
             * A customization target locale.
             * @return a locale
             */
            public @NonNull Locale getLocale() {
                return mLocale;
            }

            /**
             * An operation to be applied to the original font family.
             *
             * The operation can be one of {@link #OPERATION_PREPEND}, {@link #OPERATION_REPLACE} or
             * {@link #OPERATION_APPEND}.
             *
             * The operation prepend ({@link #OPERATION_PREPEND}) means that the new font family is
             * inserted just before the original font family. The original font family is still in
             * the fallback.
             *
             * The operation replace ({@link #OPERATION_REPLACE}) means that the original font
             * family is replaced with new font family. The original font family is removed from the
             * fallback.
             *
             * The operation append ({@link #OPERATION_APPEND}) means that the new font family is
             * inserted just after the original font family. The original font family is still in
             * the fallback.
             *
             * @return an operation.
             */
            public @Operation int getOperation() {
                return mOperation;
            }

            /**
             * Returns a family to be inserted or replaced to the fallback.
             *
             * @return a family
             */
            public @NonNull FontFamily getFamily() {
                return mFamily;
            }

            /**
             * Returns a script of the locale. If the script is missing in the given locale, the
             * most likely locale is returned.
             */
            public @NonNull String getScript() {
                return mScript;
            }

            @Override
            public String toString() {
                return "LocaleFallback{"
                        + "mLocale=" + mLocale
                        + ", mOperation=" + mOperation
                        + ", mFamily=" + mFamily
                        + '}';
            }
        }
    }

    /** @hide */
    public static String resolveScript(Locale locale) {
        String script = locale.getScript();
        if (script != null && !script.isEmpty()) {
            return script;
        }
        return ULocale.addLikelySubtags(ULocale.forLocale(locale)).getScript();
    }
}
+8 −0
Original line number Diff line number Diff line
package: "com.android.text.flags"

flag {
  name: "custom_locale_fallback"
  namespace: "text"
  description: "A feature flag that adds custom locale fallback to the vendor customization XML. This enables vendors to add their locale specific fonts, e.g. Japanese font."
  bug: ""
}
+1 −0
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ android_test {
        "servicestests-utils",
        "device-time-shell-utils",
        "testables",
        "com.android.text.flags-aconfig-java",
    ],

    libs: [
+134 −1
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.graphics;

import static com.android.text.flags.Flags.FLAG_CUSTOM_LOCALE_FALLBACK;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -30,6 +32,9 @@ import android.graphics.fonts.FontFamily;
import android.graphics.fonts.SystemFonts;
import android.graphics.text.PositionedGlyphs;
import android.graphics.text.TextRunShaper;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.text.FontConfig;
import android.util.ArrayMap;

@@ -39,6 +44,7 @@ import androidx.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParserException;
@@ -107,6 +113,10 @@ public class TypefaceSystemFallbackTest {
        GLYPH_2EM_WIDTH = paint.measureText("a");
    }

    @Rule
    public final CheckFlagsRule mCheckFlagsRule =
            DeviceFlagsValueProvider.createCheckFlagsRule();

    @Before
    public void setUp() {
        final AssetManager am =
@@ -877,6 +887,130 @@ public class TypefaceSystemFallbackTest {
        assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f);
    }

    private static void assertA3emFontIsUsed(Typeface typeface) {
        final Paint paint = new Paint();
        assertNotNull(typeface);
        paint.setTypeface(typeface);
        assertTrue("a3em font must be used", GLYPH_3EM_WIDTH == paint.measureText("a")
                && GLYPH_1EM_WIDTH == paint.measureText("b")
                && GLYPH_1EM_WIDTH == paint.measureText("c"));
    }

    private static void assertB3emFontIsUsed(Typeface typeface) {
        final Paint paint = new Paint();
        assertNotNull(typeface);
        paint.setTypeface(typeface);
        assertTrue("b3em font must be used", GLYPH_1EM_WIDTH == paint.measureText("a")
                && GLYPH_3EM_WIDTH == paint.measureText("b")
                && GLYPH_1EM_WIDTH == paint.measureText("c"));
    }

    private static String getBaseXml(String font, String lang) {
        final String xml = "<?xml version='1.0' encoding='UTF-8'?>"
                + "<familyset>"
                + "  <family>"
                + "    <font weight='400' style='normal'>no_coverage.ttf</font>"
                + "  </family>"
                + "  <family name='named-family'>"
                + "    <font weight='400' style='normal'>no_coverage.ttf</font>"
                + "  </family>"
                + "  <family lang='%s'>"
                + "    <font weight='400' style='normal'>%s</font>"
                + "  </family>"
                + "</familyset>";
        return String.format(xml, lang, font);
    }

    private static String getCustomizationXml(String font, String op, String lang) {
        final String xml = "<?xml version='1.0' encoding='UTF-8'?>"
                + "<fonts-modification version='1'>"
                + "  <family customizationType='new-locale-family' operation='%s' lang='%s'>"
                + "    <font weight='400' style='normal' fallbackFor='named-family'>%s</font>"
                + "  </family>"
                + "</fonts-modification>";
        return String.format(xml, op, lang, font);
    }

    @RequiresFlagsEnabled(FLAG_CUSTOM_LOCALE_FALLBACK)
    @Test
    public void testBuildSystemFallback__Customization_locale_prepend() {
        final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();

        buildSystemFallback(
                getBaseXml("a3em.ttf", "ja-JP"),
                getCustomizationXml("b3em.ttf", "prepend", "ja-JP"),
                fontMap, fallbackMap);
        Typeface typeface = fontMap.get("named-family");

        // operation "prepend" places font before the original font, thus b3em is used.
        assertB3emFontIsUsed(typeface);
    }

    @RequiresFlagsEnabled(FLAG_CUSTOM_LOCALE_FALLBACK)
    @Test
    public void testBuildSystemFallback__Customization_locale_replace() {
        final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();

        buildSystemFallback(
                getBaseXml("a3em.ttf", "ja-JP"),
                getCustomizationXml("b3em.ttf", "replace", "ja-JP"),
                fontMap, fallbackMap);
        Typeface typeface = fontMap.get("named-family");

        // operation "replace" removes the original font, thus b3em font is used.
        assertB3emFontIsUsed(typeface);
    }

    @RequiresFlagsEnabled(FLAG_CUSTOM_LOCALE_FALLBACK)
    @Test
    public void testBuildSystemFallback__Customization_locale_append() {
        final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();

        buildSystemFallback(
                getBaseXml("a3em.ttf", "ja-JP"),
                getCustomizationXml("b3em.ttf", "append", "ja-JP"),
                fontMap, fallbackMap);
        Typeface typeface = fontMap.get("named-family");

        // operation "append" comes next to the original font, so the original "a3em" is used.
        assertA3emFontIsUsed(typeface);
    }

    @RequiresFlagsEnabled(FLAG_CUSTOM_LOCALE_FALLBACK)
    @Test
    public void testBuildSystemFallback__Customization_locale_ScriptMismatch() {
        final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();

        buildSystemFallback(
                getBaseXml("a3em.ttf", "ja-JP"),
                getCustomizationXml("b3em.ttf", "replace", "ko-KR"),
                fontMap, fallbackMap);
        Typeface typeface = fontMap.get("named-family");

        // Since the script doesn't match, the customization is ignored.
        assertA3emFontIsUsed(typeface);
    }

    @RequiresFlagsEnabled(FLAG_CUSTOM_LOCALE_FALLBACK)
    @Test
    public void testBuildSystemFallback__Customization_locale_SubscriptMatch() {
        final ArrayMap<String, Typeface> fontMap = new ArrayMap<>();
        final ArrayMap<String, FontFamily[]> fallbackMap = new ArrayMap<>();

        buildSystemFallback(
                getBaseXml("a3em.ttf", "ja-JP"),
                getCustomizationXml("b3em.ttf", "replace", "ko-Hani-KR"),
                fontMap, fallbackMap);
        Typeface typeface = fontMap.get("named-family");

        // Hani script is supported by Japanese, Jpan.
        assertB3emFontIsUsed(typeface);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testBuildSystemFallback__Customization_new_named_family_no_name_exception() {
        final String oemXml = "<?xml version='1.0' encoding='UTF-8'?>"
@@ -902,7 +1036,6 @@ public class TypefaceSystemFallbackTest {
        readFontCustomization(oemXml);
    }


    @Test
    public void testBuildSystemFallback_UpdatableFont() {
        final String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+3 −1
Original line number Diff line number Diff line
@@ -236,7 +236,9 @@ public class FontListParser {
            }
        }

        return new FontConfig(families, filtered, resultNamedFamilies, lastModifiedDate,
        return new FontConfig(families, filtered, resultNamedFamilies,
                customization.getLocaleFamilyCustomizations(),
                lastModifiedDate,
                configVersion);
    }

Loading