Add helper class to read/write updatable font dir.
Bug: 173619554
Test: atest FrameworksCoreTests:TypefaceSystemFallbackTest
Test: atest FrameworksServicesTests:UpdatableFontDirTest
Change-Id: I950ed71a14d536eaeba5ab754a6c56a637b97217
diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
index 05ff218..465ea17 100644
--- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
+++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java
@@ -48,7 +48,9 @@
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 @@
} 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);
}
diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java
index 73fff72..af100c9 100644
--- a/graphics/java/android/graphics/FontListParser.java
+++ b/graphics/java/android/graphics/FontListParser.java
@@ -31,6 +31,7 @@
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 @@
* 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 @@
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 @@
* 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 @@
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 @@
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 @@
}
}
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)
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
index 16a53c2..93b1fcc 100644
--- a/graphics/java/android/graphics/fonts/SystemFonts.java
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -92,7 +92,7 @@
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 @@
}
/**
- * @see #buildSystemFallback(String, String, String, FontCustomizationParser.Result, Map)
+ * @see #buildSystemFallback(String, String, Map, FontCustomizationParser.Result, Map)
* @hide
*/
@VisibleForTesting
@@ -236,7 +236,7 @@
@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 @@
* @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 @@
@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 @@
/** @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;
}
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
index 521ce69..633c0c4 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
@@ -20,15 +20,28 @@
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 @@
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 @@
}
}
- @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 @@
@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 @@
}
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;
+ }
+ }
}
diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
new file mode 100644
index 0000000..7306471
--- /dev/null
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -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;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
new file mode 100644
index 0000000..0cf0af3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -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());
+ }
+ }
+}