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

Commit 0ce28292 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add helper class to read/write updatable font dir."

parents 199d469f 77c95526
Loading
Loading
Loading
Loading
+7 −1
Original line number Original line Diff line number Diff line
@@ -48,7 +48,9 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Locale;
import java.util.Locale;
import java.util.Map;


@SmallTest
@SmallTest
@RunWith(AndroidJUnit4.class)
@RunWith(AndroidJUnit4.class)
@@ -145,9 +147,13 @@ public class TypefaceSystemFallbackTest {
        } catch (IOException e) {
        } catch (IOException e) {
            throw new RuntimeException(e);
            throw new RuntimeException(e);
        }
        }
        Map<String, File> updatableFontMap = new HashMap<>();
        for (File file : new File(TEST_UPDATABLE_FONT_DIR).listFiles()) {
            updatableFontMap.put(file.getName(), file);
        }


        final FontConfig.Alias[] aliases = SystemFonts.buildSystemFallback(TEST_FONTS_XML,
        final FontConfig.Alias[] aliases = SystemFonts.buildSystemFallback(TEST_FONTS_XML,
                TEST_FONT_DIR, TEST_UPDATABLE_FONT_DIR, oemCustomization, fallbackMap);
                TEST_FONT_DIR, updatableFontMap, oemCustomization, fallbackMap);
        Typeface.initSystemDefaultTypefaces(fontMap, fallbackMap, aliases);
        Typeface.initSystemDefaultTypefaces(fontMap, fallbackMap, aliases);
    }
    }


+20 −15
Original line number Original line Diff line number Diff line
@@ -31,6 +31,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.Pattern;


/**
/**
@@ -50,19 +51,21 @@ public class FontListParser {
     * Parse the fonts.xml
     * Parse the fonts.xml
     */
     */
    public static FontConfig parse(InputStream in, String fontDir,
    public static FontConfig parse(InputStream in, String fontDir,
            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
            @Nullable Map<String, File> updatableFontMap)
            throws XmlPullParserException, IOException {
        try {
        try {
            XmlPullParser parser = Xml.newPullParser();
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(in, null);
            parser.setInput(in, null);
            parser.nextTag();
            parser.nextTag();
            return readFamilies(parser, fontDir, updatableFontDir);
            return readFamilies(parser, fontDir, updatableFontMap);
        } finally {
        } finally {
            in.close();
            in.close();
        }
        }
    }
    }


    private static FontConfig readFamilies(XmlPullParser parser, String fontDir,
    private static FontConfig readFamilies(XmlPullParser parser, String fontDir,
            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
            @Nullable Map<String, File> updatableFontMap)
            throws XmlPullParserException, IOException {
        List<FontConfig.Family> families = new ArrayList<>();
        List<FontConfig.Family> families = new ArrayList<>();
        List<FontConfig.Alias> aliases = new ArrayList<>();
        List<FontConfig.Alias> aliases = new ArrayList<>();


@@ -71,7 +74,7 @@ public class FontListParser {
            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
            String tag = parser.getName();
            String tag = parser.getName();
            if (tag.equals("family")) {
            if (tag.equals("family")) {
                families.add(readFamily(parser, fontDir, updatableFontDir));
                families.add(readFamily(parser, fontDir, updatableFontMap));
            } else if (tag.equals("alias")) {
            } else if (tag.equals("alias")) {
                aliases.add(readAlias(parser));
                aliases.add(readAlias(parser));
            } else {
            } else {
@@ -86,7 +89,8 @@ public class FontListParser {
     * Reads a family element
     * Reads a family element
     */
     */
    public static FontConfig.Family readFamily(XmlPullParser parser, String fontDir,
    public static FontConfig.Family readFamily(XmlPullParser parser, String fontDir,
            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
            @Nullable Map<String, File> updatableFontMap)
            throws XmlPullParserException, IOException {
        final String name = parser.getAttributeValue(null, "name");
        final String name = parser.getAttributeValue(null, "name");
        final String lang = parser.getAttributeValue("", "lang");
        final String lang = parser.getAttributeValue("", "lang");
        final String variant = parser.getAttributeValue(null, "variant");
        final String variant = parser.getAttributeValue(null, "variant");
@@ -95,7 +99,7 @@ public class FontListParser {
            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
            if (parser.getEventType() != XmlPullParser.START_TAG) continue;
            final String tag = parser.getName();
            final String tag = parser.getName();
            if (tag.equals("font")) {
            if (tag.equals("font")) {
                fonts.add(readFont(parser, fontDir, updatableFontDir));
                fonts.add(readFont(parser, fontDir, updatableFontMap));
            } else {
            } else {
                skip(parser);
                skip(parser);
            }
            }
@@ -117,7 +121,8 @@ public class FontListParser {
            Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
            Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");


    private static FontConfig.Font readFont(XmlPullParser parser, String fontDir,
    private static FontConfig.Font readFont(XmlPullParser parser, String fontDir,
            @Nullable String updatableFontDir) throws XmlPullParserException, IOException {
            @Nullable Map<String, File> updatableFontMap)
            throws XmlPullParserException, IOException {
        String indexStr = parser.getAttributeValue(null, "index");
        String indexStr = parser.getAttributeValue(null, "index");
        int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
        int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
        List<FontVariationAxis> axes = new ArrayList<FontVariationAxis>();
        List<FontVariationAxis> axes = new ArrayList<FontVariationAxis>();
@@ -139,20 +144,20 @@ public class FontListParser {
            }
            }
        }
        }
        String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
        String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll("");
        String fontName = findFontFile(sanitizedName, fontDir, updatableFontDir);
        String fontName = findFontFile(sanitizedName, fontDir, updatableFontMap);
        return new FontConfig.Font(fontName, index, axes.toArray(
        return new FontConfig.Font(fontName, index, axes.toArray(
                new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor);
                new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor);
    }
    }


    private static String findFontFile(String fileName, String fontDir,
    private static String findFontFile(String name, String fontDir,
            @Nullable String updatableFontDir) {
            @Nullable Map<String, File> updatableFontMap) {
        if (updatableFontDir != null) {
        if (updatableFontMap != null) {
            String updatableFontName = updatableFontDir + fileName;
            File updatedFile = updatableFontMap.get(name);
            if (new File(updatableFontName).exists()) {
            if (updatedFile != null) {
                return updatableFontName;
                return updatedFile.getAbsolutePath();
            }
            }
        }
        }
        return fontDir + fileName;
        return fontDir + name;
    }
    }


    private static FontVariationAxis readAxis(XmlPullParser parser)
    private static FontVariationAxis readAxis(XmlPullParser parser)
+8 −9
Original line number Original line Diff line number Diff line
@@ -92,7 +92,7 @@ public final class SystemFonts {
                readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
                readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
        Map<String, FontFamily[]> map = new ArrayMap<>();
        Map<String, FontFamily[]> map = new ArrayMap<>();
        // TODO: use updated fonts
        // TODO: use updated fonts
        buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", null /* updatableFontDir */,
        buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", null /* updatableFontMap */,
                oemCustomization, map);
                oemCustomization, map);
        Set<Font> res = new HashSet<>();
        Set<Font> res = new HashSet<>();
        for (FontFamily[] families : map.values()) {
        for (FontFamily[] families : map.values()) {
@@ -228,7 +228,7 @@ public final class SystemFonts {
    }
    }


    /**
    /**
     * @see #buildSystemFallback(String, String, String, FontCustomizationParser.Result, Map)
     * @see #buildSystemFallback(String, String, Map, FontCustomizationParser.Result, Map)
     * @hide
     * @hide
     */
     */
    @VisibleForTesting
    @VisibleForTesting
@@ -236,7 +236,7 @@ public final class SystemFonts {
            @NonNull String fontDir,
            @NonNull String fontDir,
            @NonNull FontCustomizationParser.Result oemCustomization,
            @NonNull FontCustomizationParser.Result oemCustomization,
            @NonNull Map<String, FontFamily[]> fallbackMap) {
            @NonNull Map<String, FontFamily[]> fallbackMap) {
        return buildSystemFallback(xmlPath, fontDir, null /* updatableFontDir */,
        return buildSystemFallback(xmlPath, fontDir, null /* updatableFontMap */,
                oemCustomization, fallbackMap);
                oemCustomization, fallbackMap);
    }
    }


@@ -246,8 +246,7 @@ public final class SystemFonts {
     * @param xmlPath A full path string to the fonts.xml file.
     * @param xmlPath A full path string to the fonts.xml file.
     * @param fontDir A full path string to the system font directory. This must end with
     * @param fontDir A full path string to the system font directory. This must end with
     *                slash('/').
     *                slash('/').
     * @param updatableFontDir A full path string to the updatable system font directory. This
     * @param updatableFontMap A map from font file name to updated font file path.
     *                           must end with slash('/').
     * @param fallbackMap An output system fallback map. Caller must pass empty map.
     * @param fallbackMap An output system fallback map. Caller must pass empty map.
     * @return a list of aliases
     * @return a list of aliases
     * @hide
     * @hide
@@ -255,12 +254,12 @@ public final class SystemFonts {
    @VisibleForTesting
    @VisibleForTesting
    public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
    public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
            @NonNull String fontDir,
            @NonNull String fontDir,
            @Nullable String updatableFontDir,
            @Nullable Map<String, File> updatableFontMap,
            @NonNull FontCustomizationParser.Result oemCustomization,
            @NonNull FontCustomizationParser.Result oemCustomization,
            @NonNull Map<String, FontFamily[]> fallbackMap) {
            @NonNull Map<String, FontFamily[]> fallbackMap) {
        try {
        try {
            final FileInputStream fontsIn = new FileInputStream(xmlPath);
            final FileInputStream fontsIn = new FileInputStream(xmlPath);
            final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir, updatableFontDir);
            final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir, updatableFontMap);


            final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
            final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
            final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
            final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
@@ -329,12 +328,12 @@ public final class SystemFonts {


    /** @hide */
    /** @hide */
    public static Pair<FontConfig.Alias[], Map<String, FontFamily[]>>
    public static Pair<FontConfig.Alias[], Map<String, FontFamily[]>>
            initializeSystemFonts(@Nullable String updatableFontDir) {
            initializeSystemFonts(@Nullable Map<String, File> updatableFontMap) {
        final FontCustomizationParser.Result oemCustomization =
        final FontCustomizationParser.Result oemCustomization =
                readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
                readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
        Map<String, FontFamily[]> map = new ArrayMap<>();
        Map<String, FontFamily[]> map = new ArrayMap<>();
        FontConfig.Alias[] aliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
        FontConfig.Alias[] aliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
                updatableFontDir, oemCustomization, map);
                updatableFontMap, oemCustomization, map);
        synchronized (LOCK) {
        synchronized (LOCK) {
            sFamilyMap = map;
            sFamilyMap = map;
        }
        }
+73 −2
Original line number Original line Diff line number Diff line
@@ -20,15 +20,28 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Context;
import android.graphics.Typeface;
import android.graphics.Typeface;
import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontFileUtil;
import android.graphics.fonts.SystemFonts;
import android.os.SharedMemory;
import android.os.SharedMemory;
import android.system.ErrnoException;
import android.system.ErrnoException;
import android.text.FontConfig;
import android.util.Pair;
import android.util.Slog;
import android.util.Slog;


import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.SystemService;


import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.NioUtils;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;


/** A service for managing system fonts. */
/** A service for managing system fonts. */
// TODO(b/173619554): Add API to update fonts.
// TODO(b/173619554): Add API to update fonts.
@@ -36,6 +49,10 @@ public final class FontManagerService {


    private static final String TAG = "FontManagerService";
    private static final String TAG = "FontManagerService";


    // TODO: make this a DeviceConfig flag.
    private static final boolean ENABLE_FONT_UPDATES = false;
    private static final String FONT_FILES_DIR = "/data/fonts/files";

    /** Class to manage FontManagerService's lifecycle. */
    /** Class to manage FontManagerService's lifecycle. */
    public static final class Lifecycle extends SystemService {
    public static final class Lifecycle extends SystemService {
        private final FontManagerService mService;
        private final FontManagerService mService;
@@ -58,10 +75,37 @@ public final class FontManagerService {
        }
        }
    }
    }


    @GuardedBy("this")
    private static class OtfFontFileParser implements UpdatableFontDir.FontFileParser {
        @Override
        public long getVersion(File file) throws IOException {
            ByteBuffer buffer = mmap(file);
            try {
                return FontFileUtil.getRevision(buffer, 0);
            } finally {
                NioUtils.freeDirectBuffer(buffer);
            }
        }

        private static ByteBuffer mmap(File file) throws IOException {
            try (FileInputStream in = new FileInputStream(file)) {
                FileChannel fileChannel = in.getChannel();
                return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
            }
        }
    }

    @Nullable
    private final UpdatableFontDir mUpdatableFontDir;

    @GuardedBy("FontManagerService.this")
    @Nullable
    @Nullable
    private SharedMemory mSerializedSystemFontMap = null;
    private SharedMemory mSerializedSystemFontMap = null;


    private FontManagerService() {
        mUpdatableFontDir = ENABLE_FONT_UPDATES
                ? new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser()) : null;
    }

    @Nullable
    @Nullable
    private SharedMemory getSerializedSystemFontMap() {
    private SharedMemory getSerializedSystemFontMap() {
        if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) {
        if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) {
@@ -77,7 +121,19 @@ public final class FontManagerService {


    @Nullable
    @Nullable
    private SharedMemory createSerializedSystemFontMapLocked() {
    private SharedMemory createSerializedSystemFontMapLocked() {
        // TODO(b/173619554): use updated fonts.
        if (mUpdatableFontDir != null) {
            HashMap<String, Typeface> systemFontMap = new HashMap<>();
            Map<String, File> fontFileMap = mUpdatableFontDir.getFontFileMap();
            Pair<FontConfig.Alias[], Map<String, FontFamily[]>> pair =
                    SystemFonts.initializeSystemFonts(fontFileMap);
            Typeface.initSystemDefaultTypefaces(systemFontMap, pair.second, pair.first);
            try {
                return Typeface.serializeFontMap(systemFontMap);
            } catch (IOException | ErrnoException e) {
                Slog.w(TAG, "Failed to serialize updatable font map. "
                        + "Retrying with system image fonts.", e);
            }
        }
        try {
        try {
            return Typeface.serializeFontMap(Typeface.getSystemFontMap());
            return Typeface.serializeFontMap(Typeface.getSystemFontMap());
        } catch (IOException | ErrnoException e) {
        } catch (IOException | ErrnoException e) {
@@ -85,4 +141,19 @@ public final class FontManagerService {
        }
        }
        return null;
        return null;
    }
    }

    private boolean installFontFile(String name, FileDescriptor fd) {
        if (mUpdatableFontDir == null) return false;
        synchronized (FontManagerService.this) {
            try {
                mUpdatableFontDir.installFontFile(name, fd);
            } catch (IOException e) {
                Slog.w(TAG, "Failed to install font file: " + name, e);
                return false;
            }
            // Create updated font map in the next getSerializedSystemFontMap() call.
            mSerializedSystemFontMap = null;
            return true;
        }
    }
}
}
+150 −0
Original line number Original line Diff line number Diff line
/*
 * 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 com.android.server.graphics.fonts;

import android.os.FileUtils;
import android.util.Base64;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

final class UpdatableFontDir {

    private static final String TAG = "UpdatableFontDir";
    private static final String RANDOM_DIR_PREFIX = "~~";

    /** Interface to mock font file access in tests. */
    interface FontFileParser {
        long getVersion(File file) throws IOException;
    }

    /** Data class to hold font file path and version. */
    static final class FontFileInfo {
        final File mFile;
        final long mVersion;

        FontFileInfo(File file, long version) {
            mFile = file;
            mVersion = version;
        }
    }

    /**
     * Root directory for storing updated font files. Each font file is stored in a unique random
     * dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}.
     */
    private final File mFilesDir;
    private final FontFileParser mParser;
    @GuardedBy("UpdatableFontDir.this")
    private final Map<String, FontFileInfo> mFontFileInfoMap = new HashMap<>();

    UpdatableFontDir(File filesDir, FontFileParser parser) {
        mFilesDir = filesDir;
        mParser = parser;
        loadFontFileMap();
    }

    private void loadFontFileMap() {
        synchronized (UpdatableFontDir.this) {
            mFontFileInfoMap.clear();
            File[] dirs = mFilesDir.listFiles();
            if (dirs == null) return;
            for (File dir : dirs) {
                if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) continue;
                File[] files = dir.listFiles();
                if (files == null || files.length != 1) continue;
                addFileToMapLocked(files[0], true);
            }
        }
    }

    void installFontFile(String name, FileDescriptor fd) throws IOException {
        // TODO: Validate name.
        synchronized (UpdatableFontDir.this) {
            // TODO: proper error handling
            File newDir = getRandomDir(mFilesDir);
            if (!newDir.mkdir()) {
                throw new IOException("Failed to create a new dir");
            }
            File newFontFile = new File(newDir, name);
            try (FileOutputStream out = new FileOutputStream(newFontFile)) {
                FileUtils.copy(fd, out.getFD());
            }
            addFileToMapLocked(newFontFile, false);
        }
    }

    /**
     * Given {@code parent}, returns {@code parent/~~[randomStr]}.
     * Makes sure that {@code parent/~~[randomStr]} directory doesn't exist.
     * Notice that this method doesn't actually create any directory.
     */
    private static File getRandomDir(File parent) {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[16];
        File dir;
        do {
            random.nextBytes(bytes);
            String dirName = RANDOM_DIR_PREFIX
                    + Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
            dir = new File(parent, dirName);
        } while (dir.exists());
        return dir;
    }

    private void addFileToMapLocked(File file, boolean deleteOldFile) {
        final long version;
        try {
            version = mParser.getVersion(file);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to read font file", e);
            return;
        }
        if (version == -1) {
            Slog.e(TAG, "Invalid font file");
            return;
        }
        FontFileInfo info = mFontFileInfoMap.get(file.getName());
        if (info == null) {
            // TODO: check version of font in /system/fonts and /product/fonts
            mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version));
        } else if (info.mVersion < version) {
            if (deleteOldFile) {
                FileUtils.deleteContentsAndDir(info.mFile.getParentFile());
            }
            mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version));
        }
    }

    Map<String, File> getFontFileMap() {
        Map<String, File> map = new HashMap<>();
        synchronized (UpdatableFontDir.this) {
            for (Map.Entry<String, FontFileInfo> entry : mFontFileInfoMap.entrySet()) {
                map.put(entry.getKey(), entry.getValue().mFile);
            }
        }
        return map;
    }
}
Loading