Support variable font family

A variable font family sets the requested weight and italic style
axes before passing down to the rendering pipeline.
Also, add more accessors to the PositionedGlyph APIs for knowing
fake bold/italic information as well as wght/ital overrides.

Bug: 281769620
Test: minikin_tests
Test: atest CtsTextTestCases:android.text.cts.VariableFamilyTest

Change-Id: I4a4770bf185a1c21113a293fe3d831573411ec26
diff --git a/core/api/current.txt b/core/api/current.txt
index 1d3e275..fd86709 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -17433,6 +17433,7 @@
     ctor public FontFamily.Builder(@NonNull android.graphics.fonts.Font);
     method @NonNull public android.graphics.fonts.FontFamily.Builder addFont(@NonNull android.graphics.fonts.Font);
     method @NonNull public android.graphics.fonts.FontFamily build();
+    method @Nullable public android.graphics.fonts.FontFamily buildVariableFamily();
   }
 
   public final class FontStyle {
@@ -17609,13 +17610,18 @@
     method public float getAdvance();
     method public float getAscent();
     method public float getDescent();
+    method public boolean getFakeBold(@IntRange(from=0) int);
+    method public boolean getFakeItalic(@IntRange(from=0) int);
     method @NonNull public android.graphics.fonts.Font getFont(@IntRange(from=0) int);
     method @IntRange(from=0) public int getGlyphId(@IntRange(from=0) int);
     method public float getGlyphX(@IntRange(from=0) int);
     method public float getGlyphY(@IntRange(from=0) int);
+    method public float getItalicOverride(@IntRange(from=0) int);
     method public float getOffsetX();
     method public float getOffsetY();
+    method public float getWeightOverride(@IntRange(from=0) int);
     method @IntRange(from=0) public int glyphCount();
+    field public static final float NO_OVERRIDE = 1.4E-45f;
   }
 
   public class TextRunShaper {
diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java
index bf79b1b..7cca7f1 100644
--- a/graphics/java/android/graphics/fonts/FontFamily.java
+++ b/graphics/java/android/graphics/fonts/FontFamily.java
@@ -30,6 +30,7 @@
 import libcore.util.NativeAllocationRegistry;
 
 import java.util.ArrayList;
+import java.util.Set;
 
 /**
  * A font family class can be used for creating Typeface.
@@ -58,6 +59,7 @@
  *
  */
 public final class FontFamily {
+
     private static final String TAG = "FontFamily";
 
     /**
@@ -73,6 +75,7 @@
         // initial capacity.
         private final SparseIntArray mStyles = new SparseIntArray(4);
 
+
         /**
          * Constructs a builder.
          *
@@ -110,23 +113,63 @@
         }
 
         /**
+         * Build a variable font family that automatically adjust the `wght` and `ital` axes value
+         * for the requested weight/italic style values.
+         *
+         * To build a variable font family, added fonts must meet one of following conditions.
+         *
+         * If two font files are added, both font files must support `wght` axis and one font must
+         * support {@link FontStyle#FONT_SLANT_UPRIGHT} and another font must support
+         * {@link FontStyle#FONT_SLANT_ITALIC}. If the requested weight value is lower than minimum
+         * value of the supported `wght` axis, the minimum supported `wght` value is used. If the
+         * requested weight value is larger than maximum value of the supported `wght` axis, the
+         * maximum supported `wght` value is used. The weight values of the fonts are ignored.
+         *
+         * If one font file is added, that font must support the `wght` axis. If that font support
+         * `ital` axis, that `ital` value is set to 1 when the italic style is requested. If that
+         * font doesn't support `ital` axis, synthetic italic may be used. If the requested
+         * weight value is lower than minimum value of the supported `wght` axis, the minimum
+         * supported `wght` value is used. If the requested weight value is larger than maximum
+         * value of the supported `wght`axis, the maximum supported `wght` value is used. The weight
+         * value of the font is ignored.
+         *
+         * If none of the above conditions are met, this function return {@code null}.
+         *
+         * @return A variable font family. null if a variable font cannot be built from the given
+         *         fonts.
+         */
+        public @Nullable FontFamily buildVariableFamily() {
+            int variableFamilyType = analyzeAndResolveVariableType(mFonts);
+            if (variableFamilyType == VARIABLE_FONT_FAMILY_TYPE_UNKNOWN) {
+                return null;
+            }
+            return build("", FontConfig.FontFamily.VARIANT_DEFAULT,
+                    true /* isCustomFallback */,
+                    false /* isDefaultFallback */,
+                    variableFamilyType);
+        }
+
+        /**
          * Build the font family
          * @return a font family
          */
         public @NonNull FontFamily build() {
-            return build("", FontConfig.FontFamily.VARIANT_DEFAULT, true /* isCustomFallback */,
-                    false /* isDefaultFallback */);
+            return build("", FontConfig.FontFamily.VARIANT_DEFAULT,
+                    true /* isCustomFallback */,
+                    false /* isDefaultFallback */,
+                    VARIABLE_FONT_FAMILY_TYPE_NONE);
         }
 
         /** @hide */
         public @NonNull FontFamily build(@NonNull String langTags, int variant,
-                boolean isCustomFallback, boolean isDefaultFallback) {
+                boolean isCustomFallback, boolean isDefaultFallback, int variableFamilyType) {
+
             final long builderPtr = nInitBuilder();
             for (int i = 0; i < mFonts.size(); ++i) {
                 nAddFont(builderPtr, mFonts.get(i).getNativePtr());
             }
             final long ptr = nBuild(builderPtr, langTags, variant, isCustomFallback,
-                    isDefaultFallback);
+                    isDefaultFallback, variableFamilyType);
             final FontFamily family = new FontFamily(ptr);
             sFamilyRegistory.registerNativeAllocation(family, ptr);
             return family;
@@ -136,11 +179,94 @@
             return font.getStyle().getWeight() | (font.getStyle().getSlant()  << 16);
         }
 
+        /**
+         * @see #buildVariableFamily()
+         * @hide
+         */
+        public static final int VARIABLE_FONT_FAMILY_TYPE_UNKNOWN = -1;
+
+        /**
+         * @see #buildVariableFamily()
+         * @hide
+         */
+        public static final int VARIABLE_FONT_FAMILY_TYPE_NONE = 0;
+        /**
+         * @see #buildVariableFamily()
+         * @hide
+         */
+        public static final int VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY = 1;
+        /**
+         * @see #buildVariableFamily()
+         * @hide
+         */
+        public static final int VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL = 2;
+        /**
+         * @see #buildVariableFamily()
+         * @hide
+         */
+        public static final int VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT = 3;
+
+        /**
+         * The registered italic axis used for adjusting requested style.
+         * https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_ital
+         */
+        private static final int TAG_ital = 0x6974616C;  // i(0x69), t(0x74), a(0x61), l(0x6c)
+
+        /**
+         * The registered weight axis used for adjusting requested style.
+         * https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_wght
+         */
+        private static final int TAG_wght = 0x77676874;  // w(0x77), g(0x67), h(0x68), t(0x74)
+
+        private static int analyzeAndResolveVariableType(ArrayList<Font> fonts) {
+            if (fonts.size() > 2) {
+                return VARIABLE_FONT_FAMILY_TYPE_UNKNOWN;
+            }
+
+            if (fonts.size() == 1) {
+                Font font = fonts.get(0);
+                Set<Integer> supportedAxes =
+                        FontFileUtil.getSupportedAxes(font.getBuffer(), font.getTtcIndex());
+                if (supportedAxes.contains(TAG_wght)) {
+                    if (supportedAxes.contains(TAG_ital)) {
+                        return VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ITAL;
+                    } else {
+                        return VARIABLE_FONT_FAMILY_TYPE_SINGLE_FONT_WGHT_ONLY;
+                    }
+                } else {
+                    return VARIABLE_FONT_FAMILY_TYPE_UNKNOWN;
+                }
+            } else {
+                for (int i = 0; i < fonts.size(); ++i) {
+                    Font font = fonts.get(i);
+                    Set<Integer> supportedAxes =
+                            FontFileUtil.getSupportedAxes(font.getBuffer(), font.getTtcIndex());
+                    if (!supportedAxes.contains(TAG_wght)) {
+                        return VARIABLE_FONT_FAMILY_TYPE_UNKNOWN;
+                    }
+                }
+                boolean italic1 = fonts.get(0).getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC;
+                boolean italic2 = fonts.get(1).getStyle().getSlant() == FontStyle.FONT_SLANT_ITALIC;
+
+                if (italic1 == italic2) {
+                    return VARIABLE_FONT_FAMILY_TYPE_UNKNOWN;
+                } else {
+                    if (italic1) {
+                        // Swap fonts to make the first font upright, second font italic.
+                        Font firstFont = fonts.get(0);
+                        fonts.set(0, fonts.get(1));
+                        fonts.set(1, firstFont);
+                    }
+                    return VARIABLE_FONT_FAMILY_TYPE_TWO_FONTS_WGHT;
+                }
+            }
+        }
+
         private static native long nInitBuilder();
         @CriticalNative
         private static native void nAddFont(long builderPtr, long fontPtr);
         private static native long nBuild(long builderPtr, String langTags, int variant,
-                boolean isCustomFallback, boolean isDefaultFallback);
+                boolean isCustomFallback, boolean isDefaultFallback, int variableFamilyType);
         @CriticalNative
         private static native long nGetReleaseNativeFamily();
     }
diff --git a/graphics/java/android/graphics/fonts/FontFileUtil.java b/graphics/java/android/graphics/fonts/FontFileUtil.java
index 917eef2..ff38282 100644
--- a/graphics/java/android/graphics/fonts/FontFileUtil.java
+++ b/graphics/java/android/graphics/fonts/FontFileUtil.java
@@ -19,11 +19,14 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.util.ArraySet;
 
 import dalvik.annotation.optimization.FastNative;
 
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Collections;
+import java.util.Set;
 
 /**
  * Provides a utility for font file operations.
@@ -62,6 +65,7 @@
     private static final int SFNT_VERSION_OTTO = 0x4F54544F;
     private static final int TTC_TAG = 0x74746366;
     private static final int OS2_TABLE_TAG = 0x4F532F32;
+    private static final int FVAR_TABLE_TAG = 0x66766172;
 
     private static final int ANALYZE_ERROR = 0xFFFFFFFF;
 
@@ -200,6 +204,73 @@
         }
     }
 
+    private static int getUInt16(ByteBuffer buffer, int offset) {
+        return ((int) buffer.getShort(offset)) & 0xFFFF;
+    }
+
+    /**
+     * Returns supported axes of font
+     *
+     * @param buffer A buffer of the entire font file.
+     * @param index A font index in case of font collection. Must be 0 otherwise.
+     * @return set of supported axes tag. Returns empty set on error.
+     */
+    public static Set<Integer> getSupportedAxes(@NonNull ByteBuffer buffer, int index) {
+        ByteOrder originalOrder = buffer.order();
+        buffer.order(ByteOrder.BIG_ENDIAN);
+        try {
+            int fontFileOffset = 0;
+            int magicNumber = buffer.getInt(0);
+            if (magicNumber == TTC_TAG) {
+                // TTC file.
+                if (index >= buffer.getInt(8 /* offset to number of fonts in TTC */)) {
+                    return Collections.EMPTY_SET;
+                }
+                fontFileOffset = buffer.getInt(
+                        12 /* offset to array of offsets of font files */ + 4 * index);
+            }
+            int sfntVersion = buffer.getInt(fontFileOffset);
+
+            if (sfntVersion != SFNT_VERSION_1 && sfntVersion != SFNT_VERSION_OTTO) {
+                return Collections.EMPTY_SET;
+            }
+
+            int numTables = buffer.getShort(fontFileOffset + 4 /* offset to number of tables */);
+            int fvarTableOffset = -1;
+            for (int i = 0; i < numTables; ++i) {
+                int tableOffset = fontFileOffset + 12 /* size of offset table */
+                        + i * 16 /* size of table record */;
+                if (buffer.getInt(tableOffset) == FVAR_TABLE_TAG) {
+                    fvarTableOffset = buffer.getInt(tableOffset + 8 /* offset to the table */);
+                    break;
+                }
+            }
+
+            if (fvarTableOffset == -1) {
+                // Couldn't find OS/2 table. use regular style
+                return Collections.EMPTY_SET;
+            }
+
+            if (buffer.getShort(fvarTableOffset) != 1
+                    || buffer.getShort(fvarTableOffset + 2) != 0) {
+                return Collections.EMPTY_SET;
+            }
+
+            int axesArrayOffset = getUInt16(buffer, fvarTableOffset + 4);
+            int axisCount = getUInt16(buffer, fvarTableOffset + 8);
+            int axisSize = getUInt16(buffer, fvarTableOffset + 10);
+
+            ArraySet<Integer> axes = new ArraySet<>();
+            for (int i = 0; i < axisCount; ++i) {
+                axes.add(buffer.getInt(fvarTableOffset + axesArrayOffset + axisSize * i));
+            }
+
+            return axes;
+        } finally {
+            buffer.order(originalOrder);
+        }
+    }
+
     @FastNative
     private static native long nGetFontRevision(@NonNull ByteBuffer buffer,
             @IntRange(from = 0) int index);
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
index 8fe28ae..3fea65f 100644
--- a/graphics/java/android/graphics/fonts/SystemFonts.java
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -194,7 +194,7 @@
             }
         }
         return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */,
-                isDefaultFallback);
+                isDefaultFallback, FontFamily.Builder.VARIABLE_FONT_FAMILY_TYPE_NONE);
     }
 
     private static void appendNamedFamilyList(@NonNull FontConfig.NamedFamilyList namedFamilyList,
diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java
index 8d20e9c..49e9d0c 100644
--- a/graphics/java/android/graphics/text/PositionedGlyphs.java
+++ b/graphics/java/android/graphics/text/PositionedGlyphs.java
@@ -165,6 +165,68 @@
     }
 
     /**
+     * Returns true if the fake bold option used for drawing, otherwise false.
+     *
+     * @param index the glyph index
+     * @return true if the fake bold option is on, otherwise off.
+     */
+    public boolean getFakeBold(@IntRange(from = 0) int index) {
+        Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
+        return nGetFakeBold(mLayoutPtr, index);
+    }
+
+    /**
+     * Returns true if the fake italic option used for drawing, otherwise false.
+     *
+     * @param index the glyph index
+     * @return true if the fake italic option is on, otherwise off.
+     */
+    public boolean getFakeItalic(@IntRange(from = 0) int index) {
+        Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
+        return nGetFakeItalic(mLayoutPtr, index);
+    }
+
+    /**
+     * A special value returned by {@link #getWeightOverride(int)} and
+     * {@link #getItalicOverride(int)} that indicates no font variation setting is overridden.
+     */
+    public static final float NO_OVERRIDE = Float.MIN_VALUE;
+
+    /**
+     * Returns overridden weight value if the font is variable font and `wght` value is overridden
+     * for drawing. Otherwise returns {@link #NO_OVERRIDE}.
+     *
+     * @param index the glyph index
+     * @return overridden weight value or {@link #NO_OVERRIDE}.
+     */
+    public float getWeightOverride(@IntRange(from = 0) int index) {
+        Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
+        float value = nGetWeightOverride(mLayoutPtr, index);
+        if (value == -1) {
+            return NO_OVERRIDE;
+        } else {
+            return value;
+        }
+    }
+
+    /**
+     * Returns overridden italic value if the font is variable font and `ital` value is overridden
+     * for drawing. Otherwise returns {@link #NO_OVERRIDE}.
+     *
+     * @param index the glyph index
+     * @return overridden weight value or {@link #NO_OVERRIDE}.
+     */
+    public float getItalicOverride(@IntRange(from = 0) int index) {
+        Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index");
+        float value = nGetItalicOverride(mLayoutPtr, index);
+        if (value == -1) {
+            return NO_OVERRIDE;
+        } else {
+            return value;
+        }
+    }
+
+    /**
      * Create single style layout from native result.
      *
      * @hide
@@ -210,6 +272,14 @@
     private static native long nGetFont(long minikinLayout, int i);
     @CriticalNative
     private static native long nReleaseFunc();
+    @CriticalNative
+    private static native boolean nGetFakeBold(long minikinLayout, int i);
+    @CriticalNative
+    private static native boolean nGetFakeItalic(long minikinLayout, int i);
+    @CriticalNative
+    private static native float nGetWeightOverride(long minikinLayout, int i);
+    @CriticalNative
+    private static native float nGetItalicOverride(long minikinLayout, int i);
 
     @Override
     public boolean equals(Object o) {
diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp
index 69774cc..af1668f 100644
--- a/libs/hwui/jni/FontFamily.cpp
+++ b/libs/hwui/jni/FontFamily.cpp
@@ -89,7 +89,8 @@
     }
     std::shared_ptr<minikin::FontFamily> family = minikin::FontFamily::create(
             builder->langId, builder->variant, std::move(builder->fonts),
-            true /* isCustomFallback */, false /* isDefaultFallback */);
+            true /* isCustomFallback */, false /* isDefaultFallback */,
+            minikin::VariationFamilyType::None);
     if (family->getCoverage().length() == 0) {
         return 0;
     }
diff --git a/libs/hwui/jni/fonts/FontFamily.cpp b/libs/hwui/jni/fonts/FontFamily.cpp
index ee158ee..1e392b1 100644
--- a/libs/hwui/jni/fonts/FontFamily.cpp
+++ b/libs/hwui/jni/fonts/FontFamily.cpp
@@ -60,7 +60,7 @@
 // Regular JNI
 static jlong FontFamily_Builder_build(JNIEnv* env, jobject clazz, jlong builderPtr,
                                       jstring langTags, jint variant, jboolean isCustomFallback,
-                                      jboolean isDefaultFallback) {
+                                      jboolean isDefaultFallback, jint variationFamilyType) {
     std::unique_ptr<NativeFamilyBuilder> builder(toBuilder(builderPtr));
     uint32_t localeId;
     if (langTags == nullptr) {
@@ -71,7 +71,8 @@
     }
     std::shared_ptr<minikin::FontFamily> family = minikin::FontFamily::create(
             localeId, static_cast<minikin::FamilyVariant>(variant), std::move(builder->fonts),
-            isCustomFallback, isDefaultFallback);
+            isCustomFallback, isDefaultFallback,
+            static_cast<minikin::VariationFamilyType>(variationFamilyType));
     if (family->getCoverage().length() == 0) {
         // No coverage means minikin rejected given font for some reasons.
         jniThrowException(env, "java/lang/IllegalArgumentException",
@@ -121,7 +122,7 @@
 static const JNINativeMethod gFontFamilyBuilderMethods[] = {
         {"nInitBuilder", "()J", (void*)FontFamily_Builder_initBuilder},
         {"nAddFont", "(JJ)V", (void*)FontFamily_Builder_addFont},
-        {"nBuild", "(JLjava/lang/String;IZZ)J", (void*)FontFamily_Builder_build},
+        {"nBuild", "(JLjava/lang/String;IZZI)J", (void*)FontFamily_Builder_build},
         {"nGetReleaseNativeFamily", "()J", (void*)FontFamily_Builder_GetReleaseFunc},
 };
 
diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp
index d69a47c..8c377b9 100644
--- a/libs/hwui/jni/text/TextShaper.cpp
+++ b/libs/hwui/jni/text/TextShaper.cpp
@@ -148,6 +148,30 @@
 }
 
 // CriticalNative
+static jboolean TextShaper_Result_getFakeBold(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+    const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+    return layout->layout.getFakery(i).isFakeBold();
+}
+
+// CriticalNative
+static jboolean TextShaper_Result_getFakeItalic(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+    const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+    return layout->layout.getFakery(i).isFakeItalic();
+}
+
+// CriticalNative
+static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+    const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+    return layout->layout.getFakery(i).wghtAdjustment();
+}
+
+// CriticalNative
+static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
+    const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
+    return layout->layout.getFakery(i).italAdjustment();
+}
+
+// CriticalNative
 static jlong TextShaper_Result_getFont(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) {
     const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr);
     std::shared_ptr<minikin::Font> fontRef = layout->layout.getFontRef(i);
@@ -185,15 +209,19 @@
 };
 
 static const JNINativeMethod gResultMethods[] = {
-    { "nGetGlyphCount", "(J)I", (void*)TextShaper_Result_getGlyphCount },
-    { "nGetTotalAdvance", "(J)F", (void*)TextShaper_Result_getTotalAdvance },
-    { "nGetAscent", "(J)F", (void*)TextShaper_Result_getAscent },
-    { "nGetDescent", "(J)F", (void*)TextShaper_Result_getDescent },
-    { "nGetGlyphId", "(JI)I", (void*)TextShaper_Result_getGlyphId },
-    { "nGetX", "(JI)F", (void*)TextShaper_Result_getX },
-    { "nGetY", "(JI)F", (void*)TextShaper_Result_getY },
-    { "nGetFont", "(JI)J", (void*)TextShaper_Result_getFont },
-    { "nReleaseFunc", "()J", (void*)TextShaper_Result_nReleaseFunc },
+        {"nGetGlyphCount", "(J)I", (void*)TextShaper_Result_getGlyphCount},
+        {"nGetTotalAdvance", "(J)F", (void*)TextShaper_Result_getTotalAdvance},
+        {"nGetAscent", "(J)F", (void*)TextShaper_Result_getAscent},
+        {"nGetDescent", "(J)F", (void*)TextShaper_Result_getDescent},
+        {"nGetGlyphId", "(JI)I", (void*)TextShaper_Result_getGlyphId},
+        {"nGetX", "(JI)F", (void*)TextShaper_Result_getX},
+        {"nGetY", "(JI)F", (void*)TextShaper_Result_getY},
+        {"nGetFont", "(JI)J", (void*)TextShaper_Result_getFont},
+        {"nGetFakeBold", "(JI)Z", (void*)TextShaper_Result_getFakeBold},
+        {"nGetFakeItalic", "(JI)Z", (void*)TextShaper_Result_getFakeItalic},
+        {"nGetWeightOverride", "(JI)F", (void*)TextShaper_Result_getWeightOverride},
+        {"nGetItalicOverride", "(JI)F", (void*)TextShaper_Result_getItalicOverride},
+        {"nReleaseFunc", "()J", (void*)TextShaper_Result_nReleaseFunc},
 };
 
 int register_android_graphics_text_TextShaper(JNIEnv* env) {