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

Commit 1e08e96e authored by Seigo Nonaka's avatar Seigo Nonaka Committed by Android (Google) Code Review
Browse files

Merge "Support Locale Fallback Family Customization" into main

parents eb7e841e e7455a81
Loading
Loading
Loading
Loading
+147 −2
Original line number Original line 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.FontFamily.Builder.VariableFontFamilyType;
import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontStyle;
import android.graphics.fonts.FontVariationAxis;
import android.graphics.fonts.FontVariationAxis;
import android.icu.util.ULocale;
import android.os.Build;
import android.os.Build;
import android.os.LocaleList;
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcel;
@@ -39,6 +40,7 @@ import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Collections;
import java.util.List;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Objects;




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


@@ -71,10 +74,12 @@ public final class FontConfig implements Parcelable {
     */
     */
    public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases,
    public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases,
            @NonNull List<NamedFamilyList> namedFamilyLists,
            @NonNull List<NamedFamilyList> namedFamilyLists,
            @NonNull List<Customization.LocaleFallback> localeFallbackCustomizations,
            long lastModifiedTimeMillis, @IntRange(from = 0) int configVersion) {
            long lastModifiedTimeMillis, @IntRange(from = 0) int configVersion) {
        mFamilies = families;
        mFamilies = families;
        mAliases = aliases;
        mAliases = aliases;
        mNamedFamilyLists = namedFamilyLists;
        mNamedFamilyLists = namedFamilyLists;
        mLocaleFallbackCustomizations = localeFallbackCustomizations;
        mLastModifiedTimeMillis = lastModifiedTimeMillis;
        mLastModifiedTimeMillis = lastModifiedTimeMillis;
        mConfigVersion = configVersion;
        mConfigVersion = configVersion;
    }
    }
@@ -84,7 +89,8 @@ public final class FontConfig implements Parcelable {
     */
     */
    public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases,
    public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases,
            long lastModifiedTimeMillis, @IntRange(from = 0) int configVersion) {
            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;
        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.
     * Returns the last modified time in milliseconds.
     *
     *
@@ -169,7 +187,9 @@ public final class FontConfig implements Parcelable {
            source.readTypedList(familyLists, NamedFamilyList.CREATOR);
            source.readTypedList(familyLists, NamedFamilyList.CREATOR);
            long lastModifiedDate = source.readLong();
            long lastModifiedDate = source.readLong();
            int configVersion = source.readInt();
            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
        @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 Original line 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 Original line Diff line number Diff line
@@ -64,6 +64,7 @@ android_test {
        "servicestests-utils",
        "servicestests-utils",
        "device-time-shell-utils",
        "device-time-shell-utils",
        "testables",
        "testables",
        "com.android.text.flags-aconfig-java",
    ],
    ],


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


package android.graphics;
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.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotNull;
@@ -30,6 +32,9 @@ import android.graphics.fonts.FontFamily;
import android.graphics.fonts.SystemFonts;
import android.graphics.fonts.SystemFonts;
import android.graphics.text.PositionedGlyphs;
import android.graphics.text.PositionedGlyphs;
import android.graphics.text.TextRunShaper;
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.text.FontConfig;
import android.util.ArrayMap;
import android.util.ArrayMap;


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


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


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

    @Before
    @Before
    public void setUp() {
    public void setUp() {
        final AssetManager am =
        final AssetManager am =
@@ -877,6 +887,130 @@ public class TypefaceSystemFallbackTest {
        assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f);
        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)
    @Test(expected = IllegalArgumentException.class)
    public void testBuildSystemFallback__Customization_new_named_family_no_name_exception() {
    public void testBuildSystemFallback__Customization_new_named_family_no_name_exception() {
        final String oemXml = "<?xml version='1.0' encoding='UTF-8'?>"
        final String oemXml = "<?xml version='1.0' encoding='UTF-8'?>"
@@ -902,7 +1036,6 @@ public class TypefaceSystemFallbackTest {
        readFontCustomization(oemXml);
        readFontCustomization(oemXml);
    }
    }



    @Test
    @Test
    public void testBuildSystemFallback_UpdatableFont() {
    public void testBuildSystemFallback_UpdatableFont() {
        final String xml = "<?xml version='1.0' encoding='UTF-8'?>"
        final String xml = "<?xml version='1.0' encoding='UTF-8'?>"
+3 −1
Original line number Original line 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);
                configVersion);
    }
    }


Loading