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

This commit is contained in:
TreeHugger Robot
2021-01-16 03:05:06 +00:00
committed by Android (Google) Code Review
6 changed files with 418 additions and 27 deletions

View File

@@ -48,7 +48,9 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -145,9 +147,13 @@ public class TypefaceSystemFallbackTest {
} catch (IOException 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,
TEST_FONT_DIR, TEST_UPDATABLE_FONT_DIR, oemCustomization, fallbackMap);
TEST_FONT_DIR, updatableFontMap, oemCustomization, fallbackMap);
Typeface.initSystemDefaultTypefaces(fontMap, fallbackMap, aliases);
}

View File

@@ -31,6 +31,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
@@ -50,19 +51,21 @@ public class FontListParser {
* Parse the fonts.xml
*/
public static FontConfig parse(InputStream in, String fontDir,
@Nullable String updatableFontDir) throws XmlPullParserException, IOException {
@Nullable Map<String, File> updatableFontMap)
throws XmlPullParserException, IOException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(in, null);
parser.nextTag();
return readFamilies(parser, fontDir, updatableFontDir);
return readFamilies(parser, fontDir, updatableFontMap);
} finally {
in.close();
}
}
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.Alias> aliases = new ArrayList<>();
@@ -71,7 +74,7 @@ public class FontListParser {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
String tag = parser.getName();
if (tag.equals("family")) {
families.add(readFamily(parser, fontDir, updatableFontDir));
families.add(readFamily(parser, fontDir, updatableFontMap));
} else if (tag.equals("alias")) {
aliases.add(readAlias(parser));
} else {
@@ -86,7 +89,8 @@ public class FontListParser {
* Reads a family element
*/
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 lang = parser.getAttributeValue("", "lang");
final String variant = parser.getAttributeValue(null, "variant");
@@ -95,7 +99,7 @@ public class FontListParser {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
final String tag = parser.getName();
if (tag.equals("font")) {
fonts.add(readFont(parser, fontDir, updatableFontDir));
fonts.add(readFont(parser, fontDir, updatableFontMap));
} else {
skip(parser);
}
@@ -117,7 +121,8 @@ public class FontListParser {
Pattern.compile("^[ \\n\\r\\t]+|[ \\n\\r\\t]+$");
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");
int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
List<FontVariationAxis> axes = new ArrayList<FontVariationAxis>();
@@ -139,20 +144,20 @@ public class FontListParser {
}
}
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(
new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor);
}
private static String findFontFile(String fileName, String fontDir,
@Nullable String updatableFontDir) {
if (updatableFontDir != null) {
String updatableFontName = updatableFontDir + fileName;
if (new File(updatableFontName).exists()) {
return updatableFontName;
private static String findFontFile(String name, String fontDir,
@Nullable Map<String, File> updatableFontMap) {
if (updatableFontMap != null) {
File updatedFile = updatableFontMap.get(name);
if (updatedFile != null) {
return updatedFile.getAbsolutePath();
}
}
return fontDir + fileName;
return fontDir + name;
}
private static FontVariationAxis readAxis(XmlPullParser parser)

View File

@@ -92,7 +92,7 @@ public final class SystemFonts {
readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
Map<String, FontFamily[]> map = new ArrayMap<>();
// TODO: use updated fonts
buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", null /* updatableFontDir */,
buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", null /* updatableFontMap */,
oemCustomization, map);
Set<Font> res = new HashSet<>();
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
*/
@VisibleForTesting
@@ -236,7 +236,7 @@ public final class SystemFonts {
@NonNull String fontDir,
@NonNull FontCustomizationParser.Result oemCustomization,
@NonNull Map<String, FontFamily[]> fallbackMap) {
return buildSystemFallback(xmlPath, fontDir, null /* updatableFontDir */,
return buildSystemFallback(xmlPath, fontDir, null /* updatableFontMap */,
oemCustomization, fallbackMap);
}
@@ -246,8 +246,7 @@ public final class SystemFonts {
* @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
* slash('/').
* @param updatableFontDir A full path string to the updatable system font directory. This
* must end with slash('/').
* @param updatableFontMap A map from font file name to updated font file path.
* @param fallbackMap An output system fallback map. Caller must pass empty map.
* @return a list of aliases
* @hide
@@ -255,12 +254,12 @@ public final class SystemFonts {
@VisibleForTesting
public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
@NonNull String fontDir,
@Nullable String updatableFontDir,
@Nullable Map<String, File> updatableFontMap,
@NonNull FontCustomizationParser.Result oemCustomization,
@NonNull Map<String, FontFamily[]> fallbackMap) {
try {
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 FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
@@ -329,12 +328,12 @@ public final class SystemFonts {
/** @hide */
public static Pair<FontConfig.Alias[], Map<String, FontFamily[]>>
initializeSystemFonts(@Nullable String updatableFontDir) {
initializeSystemFonts(@Nullable Map<String, File> updatableFontMap) {
final FontCustomizationParser.Result oemCustomization =
readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
Map<String, FontFamily[]> map = new ArrayMap<>();
FontConfig.Alias[] aliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
updatableFontDir, oemCustomization, map);
updatableFontMap, oemCustomization, map);
synchronized (LOCK) {
sFamilyMap = map;
}

View File

@@ -20,15 +20,28 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
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.system.ErrnoException;
import android.text.FontConfig;
import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
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. */
// TODO(b/173619554): Add API to update fonts.
@@ -36,6 +49,10 @@ public final class 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. */
public static final class Lifecycle extends SystemService {
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
private SharedMemory mSerializedSystemFontMap = null;
private FontManagerService() {
mUpdatableFontDir = ENABLE_FONT_UPDATES
? new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser()) : null;
}
@Nullable
private SharedMemory getSerializedSystemFontMap() {
if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) {
@@ -77,7 +121,19 @@ public final class FontManagerService {
@Nullable
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 {
return Typeface.serializeFontMap(Typeface.getSystemFontMap());
} catch (IOException | ErrnoException e) {
@@ -85,4 +141,19 @@ public final class FontManagerService {
}
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;
}
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,160 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.content.Context;
import android.os.FileUtils;
import android.platform.test.annotations.Presubmit;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class UpdatableFontDirTest {
/**
* A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files,
* this test uses fake font files. A fake font file has its version as its file content.
*/
private static class FakeFontFileParser implements UpdatableFontDir.FontFileParser {
@Override
public long getVersion(File file) throws IOException {
return Long.parseLong(FileUtils.readTextFile(file, 100, ""));
}
}
private File mCacheDir;
private File mUpdatableFontFilesDir;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Before
public void setUp() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
mCacheDir = new File(context.getCacheDir(), "UpdatableFontDirTest");
FileUtils.deleteContentsAndDir(mCacheDir);
mCacheDir.mkdirs();
mUpdatableFontFilesDir = new File(mCacheDir, "updatable_fonts");
mUpdatableFontFilesDir.mkdir();
}
@After
public void tearDown() {
FileUtils.deleteContentsAndDir(mCacheDir);
}
@Test
public void construct() throws Exception {
FakeFontFileParser parser = new FakeFontFileParser();
UpdatableFontDir dirForPreparation = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
installFontFile(dirForPreparation, "foo.ttf", "1");
installFontFile(dirForPreparation, "bar.ttf", "2");
installFontFile(dirForPreparation, "foo.ttf", "3");
installFontFile(dirForPreparation, "bar.ttf", "4");
// Four font dirs are created.
assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
assertThat(dir.getFontFileMap()).containsKey("foo.ttf");
assertThat(parser.getVersion(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(3);
assertThat(dir.getFontFileMap()).containsKey("bar.ttf");
assertThat(parser.getVersion(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4);
// Outdated font dir should be deleted.
assertThat(mUpdatableFontFilesDir.list()).hasLength(2);
}
@Test
public void construct_empty() {
FakeFontFileParser parser = new FakeFontFileParser();
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
assertThat(dir.getFontFileMap()).isEmpty();
}
@Test
public void installFontFile() throws Exception {
FakeFontFileParser parser = new FakeFontFileParser();
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
installFontFile(dir, "test.ttf", "1");
assertThat(dir.getFontFileMap()).containsKey("test.ttf");
assertThat(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(1);
}
@Test
public void installFontFile_upgrade() throws Exception {
FakeFontFileParser parser = new FakeFontFileParser();
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
installFontFile(dir, "test.ttf", "1");
Map<String, File> mapBeforeUpgrade = dir.getFontFileMap();
installFontFile(dir, "test.ttf", "2");
assertThat(dir.getFontFileMap()).containsKey("test.ttf");
assertThat(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2);
assertThat(mapBeforeUpgrade).containsKey("test.ttf");
assertWithMessage("Older fonts should not be deleted until next loadFontFileMap")
.that(parser.getVersion(mapBeforeUpgrade.get("test.ttf"))).isEqualTo(1);
}
@Test
public void installFontFile_downgrade() throws Exception {
FakeFontFileParser parser = new FakeFontFileParser();
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
installFontFile(dir, "test.ttf", "2");
installFontFile(dir, "test.ttf", "1");
assertThat(dir.getFontFileMap()).containsKey("test.ttf");
assertWithMessage("Font should not be downgraded to an older version")
.that(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2);
}
@Test
public void installFontFile_multiple() throws Exception {
FakeFontFileParser parser = new FakeFontFileParser();
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
installFontFile(dir, "foo.ttf", "1");
installFontFile(dir, "bar.ttf", "2");
assertThat(dir.getFontFileMap()).containsKey("foo.ttf");
assertThat(parser.getVersion(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(1);
assertThat(dir.getFontFileMap()).containsKey("bar.ttf");
assertThat(parser.getVersion(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(2);
}
private void installFontFile(UpdatableFontDir dir, String name, String content)
throws IOException {
File file = File.createTempFile(name, "", mCacheDir);
FileUtils.stringToFile(file, content);
try (FileInputStream in = new FileInputStream(file)) {
dir.installFontFile(name, in.getFD());
}
}
}