diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java index 05ff21853131f..465ea172b1c08 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.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 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 73fff7207c453..af100c96f6f59 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -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 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 updatableFontMap) + throws XmlPullParserException, IOException { List families = new ArrayList<>(); List 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 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 updatableFontMap) + throws XmlPullParserException, IOException { String indexStr = parser.getAttributeValue(null, "index"); int index = indexStr == null ? 0 : Integer.parseInt(indexStr); List axes = new ArrayList(); @@ -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 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 16a53c25db08e..93b1fcc3ba1f3 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -92,7 +92,7 @@ public final class SystemFonts { readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/"); Map 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 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 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 updatableFontMap, @NonNull FontCustomizationParser.Result oemCustomization, @NonNull Map 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 bufferCache = new HashMap(); final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies(); @@ -329,12 +328,12 @@ public final class SystemFonts { /** @hide */ public static Pair> - initializeSystemFonts(@Nullable String updatableFontDir) { + initializeSystemFonts(@Nullable Map updatableFontMap) { final FontCustomizationParser.Result oemCustomization = readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/"); Map 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 521ce69735225..633c0c4171926 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.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 systemFontMap = new HashMap<>(); + Map fontFileMap = mUpdatableFontDir.getFontFileMap(); + Pair> 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; + } + } } 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 0000000000000..7306471e68c2b --- /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 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 getFontFileMap() { + Map map = new HashMap<>(); + synchronized (UpdatableFontDir.this) { + for (Map.Entry 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 0000000000000..0cf0af39dc1e5 --- /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 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()); + } + } +}