Loading core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java +7 −1 Original line number Original line Diff line number Diff line Loading @@ -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) Loading Loading @@ -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); } } Loading graphics/java/android/graphics/FontListParser.java +20 −15 Original line number Original line Diff line number Diff line Loading @@ -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; /** /** Loading @@ -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<>(); Loading @@ -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 { Loading @@ -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"); Loading @@ -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); } } Loading @@ -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>(); Loading @@ -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) Loading graphics/java/android/graphics/fonts/SystemFonts.java +8 −9 Original line number Original line Diff line number Diff line Loading @@ -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()) { Loading Loading @@ -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 Loading @@ -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); } } Loading @@ -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 Loading @@ -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(); Loading Loading @@ -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; } } Loading services/core/java/com/android/server/graphics/fonts/FontManagerService.java +73 −2 Original line number Original line Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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) { Loading @@ -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) { Loading @@ -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; } } } } services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java 0 → 100644 +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
core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java +7 −1 Original line number Original line Diff line number Diff line Loading @@ -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) Loading Loading @@ -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); } } Loading
graphics/java/android/graphics/FontListParser.java +20 −15 Original line number Original line Diff line number Diff line Loading @@ -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; /** /** Loading @@ -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<>(); Loading @@ -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 { Loading @@ -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"); Loading @@ -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); } } Loading @@ -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>(); Loading @@ -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) Loading
graphics/java/android/graphics/fonts/SystemFonts.java +8 −9 Original line number Original line Diff line number Diff line Loading @@ -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()) { Loading Loading @@ -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 Loading @@ -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); } } Loading @@ -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 Loading @@ -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(); Loading Loading @@ -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; } } Loading
services/core/java/com/android/server/graphics/fonts/FontManagerService.java +73 −2 Original line number Original line Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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) { Loading @@ -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) { Loading @@ -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; } } } }
services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java 0 → 100644 +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; } }