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

Commit d56d2e9a authored by Kohsuke Yatoh's avatar Kohsuke Yatoh
Browse files

Retry "Support loading system font map from SharedMemory."

This is the second try of
commit c7a5b3a9, which was reverted in
commit 735d447d

Changes:
- Use reflection to update static final fields, because some apps and
  tests rely on Typeface.create() to return references to static final
  fields (which is a behavior resulted from the use of internal cache).
- Make setSystemFontMap() thread safe.
- Hold a reference to ByteBuffer so that the memory won't be unmapped.

Bug: 172891184
Test: atest FrameworksCoreTests:TypefaceTest
Test: atest CtsWidgetTestCases:android.widget.cts.TextViewPrecomputedTextTest
Change-Id: I1dea26c08a8585ea10e866c063bef6d682d8b15e
parent 4a15a3ef
Loading
Loading
Loading
Loading
+64 −12
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.fonts.SystemFonts;
import android.os.SharedMemory;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
@@ -35,9 +37,9 @@ import com.android.frameworks.coretests.R;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@RunWith(AndroidJUnit4.class)
@@ -54,6 +56,10 @@ public class TypefaceTest {
        Typeface.create(Typeface.MONOSPACE, 0)
    };

    private static final int[] STYLES = {
        Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC,
    };

    @SmallTest
    @Test
    public void testBasic() throws Exception {
@@ -64,6 +70,16 @@ public class TypefaceTest {
        assertTrue("basic", Typeface.MONOSPACE != null);
    }

    @SmallTest
    @Test
    public void testDefaults() {
        for (int style : STYLES) {
            String msg = "style = " + style;
            assertNotNull(msg, Typeface.defaultFromStyle(style));
            assertEquals(msg, style, Typeface.defaultFromStyle(style).getStyle());
        }
    }

    @SmallTest
    @Test
    public void testUnique() throws Exception {
@@ -178,21 +194,57 @@ public class TypefaceTest {
    @SmallTest
    @Test
    public void testSerialize() throws Exception {
        int size = Typeface.writeTypefaces(null, Arrays.asList(mFaces));
        ByteBuffer buffer = ByteBuffer.allocateDirect(size);
        Typeface.writeTypefaces(buffer, Arrays.asList(mFaces));
        List<Typeface> copiedTypefaces = Typeface.readTypefaces(buffer);
        assertNotNull(copiedTypefaces);
        assertEquals(mFaces.length, copiedTypefaces.size());
        for (int i = 0; i < mFaces.length; i++) {
            Typeface original = mFaces[i];
            Typeface copied = copiedTypefaces.get(i);
        HashMap<String, Typeface> systemFontMap = new HashMap<>();
        Typeface.initSystemDefaultTypefaces(systemFontMap, SystemFonts.getRawSystemFallbackMap(),
                SystemFonts.getAliases());
        SharedMemory sharedMemory = Typeface.serializeFontMap(systemFontMap);
        Map<String, Typeface> copiedFontMap =
                Typeface.deserializeFontMap(sharedMemory.mapReadOnly().order(ByteOrder.BIG_ENDIAN));
        assertEquals(systemFontMap.size(), copiedFontMap.size());
        for (String key : systemFontMap.keySet()) {
            assertTrue(copiedFontMap.containsKey(key));
            Typeface original = systemFontMap.get(key);
            Typeface copied = copiedFontMap.get(key);
            assertEquals(original.getStyle(), copied.getStyle());
            assertEquals(original.getWeight(), copied.getWeight());
            assertEquals(measureText(original, "hello"), measureText(copied, "hello"), 1e-6);
        }
    }

    @SmallTest
    @Test
    public void testSetSystemFontMap() throws Exception {
        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        Resources res = context.getResources();
        Map<String, Typeface> fontMap = Map.of(
                "sans-serif", Typeface.create(res.getFont(R.font.samplefont), Typeface.NORMAL),
                "serif", Typeface.create(res.getFont(R.font.samplefont2), Typeface.NORMAL),
                "monospace", Typeface.create(res.getFont(R.font.samplefont3), Typeface.NORMAL),
                "sample", Typeface.create(res.getFont(R.font.samplefont4), Typeface.NORMAL),
                "sample-italic", Typeface.create(res.getFont(R.font.samplefont4), Typeface.ITALIC));
        Typeface.setSystemFontMap(fontMap);

        // Test public static final fields
        assertEquals(fontMap.get("sans-serif"), Typeface.DEFAULT);
        assertEquals(Typeface.BOLD, Typeface.DEFAULT_BOLD.getStyle());
        assertEquals(fontMap.get("sans-serif"), Typeface.SANS_SERIF);
        assertEquals(fontMap.get("serif"), Typeface.SERIF);
        assertEquals(fontMap.get("monospace"), Typeface.MONOSPACE);

        // Test defaults
        assertEquals(fontMap.get("sans-serif"), Typeface.defaultFromStyle(Typeface.NORMAL));
        for (int style : STYLES) {
            String msg = "style = " + style;
            assertNotNull(msg, Typeface.defaultFromStyle(style));
            assertEquals(msg, style, Typeface.defaultFromStyle(style).getStyle());
        }

        // Test create()
        assertEquals(fontMap.get("sample"), Typeface.create("sample", Typeface.NORMAL));
        assertEquals(
                fontMap.get("sample-italic"), Typeface.create("sample-italic", Typeface.ITALIC));
    }

    private static float measureText(Typeface typeface, String text) {
        Paint paint = new Paint();
        paint.setTypeface(typeface);
+175 −72
Original line number Diff line number Diff line
@@ -34,8 +34,12 @@ import android.graphics.fonts.FontVariationAxis;
import android.graphics.fonts.SystemFonts;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.SharedMemory;
import android.os.Trace;
import android.provider.FontRequest;
import android.provider.FontsContract;
import android.system.ErrnoException;
import android.system.OsConstants;
import android.text.FontConfig;
import android.util.Base64;
import android.util.LongSparseArray;
@@ -50,6 +54,7 @@ import dalvik.annotation.optimization.CriticalNative;

import libcore.util.NativeAllocationRegistry;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -57,6 +62,7 @@ import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -79,19 +85,19 @@ public class Typeface {
            Typeface.class.getClassLoader(), nativeGetReleaseFunc());

    /** The default NORMAL typeface object */
    public static final Typeface DEFAULT;
    public static final Typeface DEFAULT = null;
    /**
     * The default BOLD typeface object. Note: this may be not actually be
     * bold, depending on what fonts are installed. Call getStyle() to know
     * for sure.
     */
    public static final Typeface DEFAULT_BOLD;
    public static final Typeface DEFAULT_BOLD = null;
    /** The NORMAL style of the default sans serif typeface. */
    public static final Typeface SANS_SERIF;
    public static final Typeface SANS_SERIF = null;
    /** The NORMAL style of the default serif typeface. */
    public static final Typeface SERIF;
    public static final Typeface SERIF = null;
    /** The NORMAL style of the default monospace typeface. */
    public static final Typeface MONOSPACE;
    public static final Typeface MONOSPACE = null;

    /**
     * The default {@link Typeface}s for different text styles.
@@ -99,6 +105,7 @@ public class Typeface {
     * It shouldn't be changed for app wide typeface settings. Please use theme and font XML for
     * the same purpose.
     */
    @GuardedBy("SYSTEM_FONT_MAP_LOCK")
    @UnsupportedAppUsage(trackingBug = 123769446)
    static Typeface[] sDefaults;

@@ -125,16 +132,28 @@ public class Typeface {
    private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16);
    private static final Object sDynamicCacheLock = new Object();


    @GuardedBy("SYSTEM_FONT_MAP_LOCK")
    static Typeface sDefaultTypeface;

    // Following two fields are not used but left for hiddenapi private list
    /**
     * sSystemFontMap is read only and unmodifiable.
     * Use public API {@link #create(String, int)} to get the typeface for given familyName.
     */
    @GuardedBy("SYSTEM_FONT_MAP_LOCK")
    @UnsupportedAppUsage(trackingBug = 123769347)
    static final Map<String, Typeface> sSystemFontMap;
    static final Map<String, Typeface> sSystemFontMap = new HashMap<>();

    // DirectByteBuffer object to hold sSystemFontMap's backing memory mapping.
    @GuardedBy("SYSTEM_FONT_MAP_LOCK")
    static ByteBuffer sSystemFontMapBuffer = null;

    // Lock to guard sSystemFontMap and derived default or public typefaces.
    // sStyledCacheLock may be held while this lock is held. Holding them in the reverse order may
    // introduce deadlock.
    private static final Object SYSTEM_FONT_MAP_LOCK = new Object();

    // This field is used but left for hiddenapi private list
    // We cannot support sSystemFallbackMap since we will migrate to public FontFamily API.
    /**
     * @deprecated Use {@link android.graphics.fonts.FontFamily} instead.
@@ -187,9 +206,17 @@ public class Typeface {
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    private static void setDefault(Typeface t) {
        synchronized (SYSTEM_FONT_MAP_LOCK) {
            sDefaultTypeface = t;
            nativeSetDefault(t.native_instance);
        }
    }

    private static Typeface getDefault() {
        synchronized (SYSTEM_FONT_MAP_LOCK) {
            return sDefaultTypeface;
        }
    }

    /** Returns the typeface's weight value */
    public @IntRange(from = 0, to = 1000) int getWeight() {
@@ -833,7 +860,7 @@ public class Typeface {
            style = NORMAL;
        }
        if (family == null) {
            family = sDefaultTypeface;
            family = getDefault();
        }

        // Return early if we're asked for the same face/style
@@ -902,7 +929,7 @@ public class Typeface {
            @IntRange(from = 1, to = 1000) int weight, boolean italic) {
        Preconditions.checkArgumentInRange(weight, 0, 1000, "weight");
        if (family == null) {
            family = sDefaultTypeface;
            family = getDefault();
        }
        return createWeightStyle(family, weight, italic);
    }
@@ -944,8 +971,10 @@ public class Typeface {
     * @return the default typeface that corresponds to the style
     */
    public static Typeface defaultFromStyle(@Style int style) {
        synchronized (SYSTEM_FONT_MAP_LOCK) {
            return sDefaults[style];
        }
    }

    /**
     * Create a new typeface from the specified font data.
@@ -1137,11 +1166,104 @@ public class Typeface {
        }
    }

    static {
        final HashMap<String, Typeface> systemFontMap = new HashMap<>();
        initSystemDefaultTypefaces(systemFontMap, SystemFonts.getRawSystemFallbackMap(),
                SystemFonts.getAliases());
        sSystemFontMap = Collections.unmodifiableMap(systemFontMap);
    /** @hide */
    public static SharedMemory serializeFontMap(Map<String, Typeface> fontMap)
            throws IOException, ErrnoException {
        long[] nativePtrs = new long[fontMap.size()];
        // The name table will not be large, so let's create a byte array in memory.
        ByteArrayOutputStream namesBytes = new ByteArrayOutputStream();
        int i = 0;
        for (Map.Entry<String, Typeface> entry : fontMap.entrySet()) {
            nativePtrs[i++] = entry.getValue().native_instance;
            writeString(namesBytes, entry.getKey());
        }
        int typefacesBytesCount = nativeWriteTypefaces(null, nativePtrs);
        // int (typefacesBytesCount), typefaces, namesBytes
        SharedMemory sharedMemory = SharedMemory.create(
                "fontMap", Integer.BYTES + typefacesBytesCount + namesBytes.size());
        ByteBuffer writableBuffer = sharedMemory.mapReadWrite().order(ByteOrder.BIG_ENDIAN);
        try {
            writableBuffer.putInt(typefacesBytesCount);
            int writtenBytesCount = nativeWriteTypefaces(writableBuffer.slice(), nativePtrs);
            if (writtenBytesCount != typefacesBytesCount) {
                throw new IOException(String.format("Unexpected bytes written: %d, expected: %d",
                        writtenBytesCount, typefacesBytesCount));
            }
            writableBuffer.position(writableBuffer.position() + writtenBytesCount);
            writableBuffer.put(namesBytes.toByteArray());
        } finally {
            SharedMemory.unmap(writableBuffer);
        }
        sharedMemory.setProtect(OsConstants.PROT_READ);
        return sharedMemory;
    }

    // buffer's byte order should be BIG_ENDIAN.
    /** @hide */
    @VisibleForTesting
    public static Map<String, Typeface> deserializeFontMap(ByteBuffer buffer) throws IOException {
        Map<String, Typeface> fontMap = new HashMap<>();
        int typefacesBytesCount = buffer.getInt();
        long[] nativePtrs = nativeReadTypefaces(buffer.slice());
        if (nativePtrs == null) {
            throw new IOException("Could not read typefaces");
        }
        buffer.position(buffer.position() + typefacesBytesCount);
        for (long nativePtr : nativePtrs) {
            String name = readString(buffer);
            fontMap.put(name, new Typeface(nativePtr));
        }
        return fontMap;
    }

    private static String readString(ByteBuffer buffer) {
        int length = buffer.getInt();
        byte[] bytes = new byte[length];
        buffer.get(bytes);
        return new String(bytes);
    }

    private static void writeString(ByteArrayOutputStream bos, String value) throws IOException {
        byte[] bytes = value.getBytes();
        writeInt(bos, bytes.length);
        bos.write(bytes);
    }

    private static void writeInt(ByteArrayOutputStream bos, int value) {
        // Write in the big endian order.
        bos.write((value >> 24) & 0xFF);
        bos.write((value >> 16) & 0xFF);
        bos.write((value >> 8) & 0xFF);
        bos.write(value & 0xFF);
    }

    /**
     * Deserialize font map and set it as system font map. This method should be called at most once
     * per process.
     */
    /** @hide */
    public static void setSystemFontMap(SharedMemory sharedMemory)
            throws IOException, ErrnoException {
        if (sSystemFontMapBuffer != null) {
            throw new UnsupportedOperationException(
                    "Once set, buffer-based system font map cannot be updated");
        }
        Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "setSystemFontMap");
        try {
            sSystemFontMapBuffer = sharedMemory.mapReadOnly().order(ByteOrder.BIG_ENDIAN);
            Map<String, Typeface> systemFontMap = deserializeFontMap(sSystemFontMapBuffer);
            setSystemFontMap(systemFontMap);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
        }
    }

    /** @hide */
    @VisibleForTesting
    public static void setSystemFontMap(Map<String, Typeface> systemFontMap) {
        synchronized (SYSTEM_FONT_MAP_LOCK) {
            sSystemFontMap.clear();
            sSystemFontMap.putAll(systemFontMap);

            // We can't assume DEFAULT_FAMILY available on Roboletric.
            if (sSystemFontMap.containsKey(DEFAULT_FAMILY)) {
@@ -1149,11 +1271,12 @@ public class Typeface {
            }

            // Set up defaults and typefaces exposed in public API
        DEFAULT         = create((String) null, 0);
        DEFAULT_BOLD    = create((String) null, Typeface.BOLD);
        SANS_SERIF      = create("sans-serif", 0);
        SERIF           = create("serif", 0);
        MONOSPACE       = create("monospace", 0);
            // Use sDefaultTypeface here, because create(String, int) uses DEFAULT as fallback.
            nativeForceSetStaticFinalField("DEFAULT", create(sDefaultTypeface, 0));
            nativeForceSetStaticFinalField("DEFAULT_BOLD", create(sDefaultTypeface, Typeface.BOLD));
            nativeForceSetStaticFinalField("SANS_SERIF", create("sans-serif", 0));
            nativeForceSetStaticFinalField("SERIF", create("serif", 0));
            nativeForceSetStaticFinalField("MONOSPACE", create("monospace", 0));

            sDefaults = new Typeface[]{
                DEFAULT,
@@ -1172,6 +1295,14 @@ public class Typeface {
                registerGenericFamilyNative(genericFamily, systemFontMap.get(genericFamily));
            }
        }
    }

    static {
        final HashMap<String, Typeface> systemFontMap = new HashMap<>();
        initSystemDefaultTypefaces(systemFontMap, SystemFonts.getRawSystemFallbackMap(),
                SystemFonts.getAliases());
        setSystemFontMap(systemFontMap);
    }

    @Override
    public boolean equals(Object o) {
@@ -1210,36 +1341,6 @@ public class Typeface {
        return Arrays.binarySearch(mSupportedAxes, axis) >= 0;
    }

    /**
     * Writes Typeface instances to the ByteBuffer and returns the number of bytes written.
     *
     * <p>If {@code buffer} is null, this method returns the number of bytes required to serialize
     * the typefaces, without writing anything.
     * @hide
     */
    public static int writeTypefaces(
            @Nullable ByteBuffer buffer, @NonNull List<Typeface> typefaces) {
        long[] nativePtrs = new long[typefaces.size()];
        for (int i = 0; i < nativePtrs.length; i++) {
            nativePtrs[i] = typefaces.get(i).native_instance;
        }
        return nativeWriteTypefaces(buffer, nativePtrs);
    }

    /**
     * Reads serialized Typeface instances from the ByteBuffer. Returns null on errors.
     * @hide
     */
    public static @Nullable List<Typeface> readTypefaces(@NonNull ByteBuffer buffer) {
        long[] nativePtrs = nativeReadTypefaces(buffer);
        if (nativePtrs == null) return null;
        List<Typeface> typefaces = new ArrayList<>(nativePtrs.length);
        for (long nativePtr : nativePtrs) {
            typefaces.add(new Typeface(nativePtr));
        }
        return typefaces;
    }

    private static native long nativeCreateFromTypeface(long native_instance, int style);
    private static native long nativeCreateFromTypefaceWithExactStyle(
            long native_instance, int weight, boolean italic);
@@ -1270,4 +1371,6 @@ public class Typeface {
            @Nullable ByteBuffer buffer, @NonNull long[] nativePtrs);

    private static native @Nullable long[] nativeReadTypefaces(@NonNull ByteBuffer buffer);

    private static native void nativeForceSetStaticFinalField(String fieldName, Typeface typeface);
}
+16 −0
Original line number Diff line number Diff line
@@ -217,6 +217,20 @@ static jlongArray Typeface_readTypefaces(JNIEnv *env, jobject, jobject buffer) {
    return result;
}


static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring fieldName,
        jobject typeface) {
    ScopedUtfChars fieldNameChars(env, fieldName);
    jfieldID fid =
            env->GetStaticFieldID(cls, fieldNameChars.c_str(), "Landroid/graphics/Typeface;");
    if (fid == 0) {
        jniThrowRuntimeException(env, "Unable to find field");
        return;
    }
    env->SetStaticObjectField(cls, fid, typeface);
}


///////////////////////////////////////////////////////////////////////////////

static const JNINativeMethod gTypefaceMethods[] = {
@@ -237,6 +251,8 @@ static const JNINativeMethod gTypefaceMethods[] = {
          (void*)Typeface_registerGenericFamily },
    { "nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;[J)I", (void*)Typeface_writeTypefaces},
    { "nativeReadTypefaces", "(Ljava/nio/ByteBuffer;)[J", (void*)Typeface_readTypefaces},
    { "nativeForceSetStaticFinalField", "(Ljava/lang/String;Landroid/graphics/Typeface;)V",
          (void*)Typeface_forceSetStaticFinalField },
};

int register_android_graphics_Typeface(JNIEnv* env)