1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.graphics.fonts; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assert.fail; 23 24 import android.content.Context; 25 import android.graphics.FontListParser; 26 import android.graphics.fonts.FontManager; 27 import android.graphics.fonts.FontStyle; 28 import android.graphics.fonts.FontUpdateRequest; 29 import android.graphics.fonts.SystemFonts; 30 import android.os.FileUtils; 31 import android.os.ParcelFileDescriptor; 32 import android.platform.test.annotations.Presubmit; 33 import android.system.Os; 34 import android.text.FontConfig; 35 import android.util.Xml; 36 37 import androidx.test.InstrumentationRegistry; 38 import androidx.test.filters.SmallTest; 39 import androidx.test.runner.AndroidJUnit4; 40 41 import org.junit.After; 42 import org.junit.Before; 43 import org.junit.Test; 44 import org.junit.runner.RunWith; 45 import org.xmlpull.v1.XmlPullParser; 46 47 import java.io.ByteArrayInputStream; 48 import java.io.File; 49 import java.io.FileInputStream; 50 import java.io.FileOutputStream; 51 import java.io.IOException; 52 import java.io.InputStream; 53 import java.nio.charset.StandardCharsets; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.Collections; 57 import java.util.HashSet; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Set; 61 import java.util.function.Function; 62 import java.util.function.Supplier; 63 import java.util.stream.Collectors; 64 65 @Presubmit 66 @SmallTest 67 @RunWith(AndroidJUnit4.class) 68 public final class UpdatableFontDirTest { 69 70 /** 71 * A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files, 72 * this test uses fake font files. A fake font file has its PostScript naem and revision as the 73 * file content. 74 */ 75 private static class FakeFontFileParser implements UpdatableFontDir.FontFileParser { 76 @Override getPostScriptName(File file)77 public String getPostScriptName(File file) throws IOException { 78 String content = FileUtils.readTextFile(file, 100, ""); 79 return content.split(",")[2]; 80 } 81 82 @Override buildFontFileName(File file)83 public String buildFontFileName(File file) throws IOException { 84 String content = FileUtils.readTextFile(file, 100, ""); 85 return content.split(",")[0]; 86 } 87 88 @Override getRevision(File file)89 public long getRevision(File file) throws IOException { 90 String content = FileUtils.readTextFile(file, 100, ""); 91 return Long.parseLong(content.split(",")[1]); 92 } 93 94 @Override tryToCreateTypeface(File file)95 public void tryToCreateTypeface(File file) throws Throwable { 96 } 97 } 98 99 // FakeFsverityUtil will successfully set up fake fs-verity if the signature is GOOD_SIGNATURE. 100 private static final String GOOD_SIGNATURE = "Good signature"; 101 102 /** A fake FsverityUtil to keep fake verity bit in memory. */ 103 private static class FakeFsverityUtil implements UpdatableFontDir.FsverityUtil { 104 private final Set<String> mHasFsverityPaths = new HashSet<>(); 105 remove(String name)106 public void remove(String name) { 107 mHasFsverityPaths.remove(name); 108 } 109 110 @Override hasFsverity(String path)111 public boolean hasFsverity(String path) { 112 return mHasFsverityPaths.contains(path); 113 } 114 115 @Override setUpFsverity(String path, byte[] pkcs7Signature)116 public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException { 117 String fakeSignature = new String(pkcs7Signature, StandardCharsets.UTF_8); 118 if (GOOD_SIGNATURE.equals(fakeSignature)) { 119 mHasFsverityPaths.add(path); 120 } else { 121 throw new IOException("Failed to set up fake fs-verity"); 122 } 123 } 124 125 @Override rename(File src, File dest)126 public boolean rename(File src, File dest) { 127 if (src.renameTo(dest)) { 128 mHasFsverityPaths.remove(src.getAbsolutePath()); 129 mHasFsverityPaths.add(dest.getAbsolutePath()); 130 return true; 131 } 132 return false; 133 } 134 } 135 136 private static final long CURRENT_TIME = 1234567890L; 137 138 private File mCacheDir; 139 private File mUpdatableFontFilesDir; 140 private File mConfigFile; 141 private List<File> mPreinstalledFontDirs; 142 private final Supplier<Long> mCurrentTimeSupplier = () -> CURRENT_TIME; 143 private final Function<Map<String, File>, FontConfig> mConfigSupplier = 144 (map) -> SystemFonts.getSystemFontConfig(map, 0, 0); 145 private FakeFontFileParser mParser; 146 private FakeFsverityUtil mFakeFsverityUtil; 147 148 @SuppressWarnings("ResultOfMethodCallIgnored") 149 @Before setUp()150 public void setUp() { 151 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 152 mCacheDir = new File(context.getCacheDir(), "UpdatableFontDirTest"); 153 FileUtils.deleteContentsAndDir(mCacheDir); 154 mCacheDir.mkdirs(); 155 mUpdatableFontFilesDir = new File(mCacheDir, "updatable_fonts"); 156 mUpdatableFontFilesDir.mkdir(); 157 mPreinstalledFontDirs = new ArrayList<>(); 158 mPreinstalledFontDirs.add(new File(mCacheDir, "system_fonts")); 159 mPreinstalledFontDirs.add(new File(mCacheDir, "product_fonts")); 160 for (File dir : mPreinstalledFontDirs) { 161 dir.mkdir(); 162 } 163 mConfigFile = new File(mCacheDir, "config.xml"); 164 mParser = new FakeFontFileParser(); 165 mFakeFsverityUtil = new FakeFsverityUtil(); 166 } 167 168 @After tearDown()169 public void tearDown() { 170 FileUtils.deleteContentsAndDir(mCacheDir); 171 } 172 173 @Test construct()174 public void construct() throws Exception { 175 long expectedModifiedDate = CURRENT_TIME / 2; 176 PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); 177 config.lastModifiedMillis = expectedModifiedDate; 178 writeConfig(config, mConfigFile); 179 UpdatableFontDir dirForPreparation = new UpdatableFontDir( 180 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 181 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 182 dirForPreparation.loadFontFileMap(); 183 assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis()) 184 .isEqualTo(expectedModifiedDate); 185 dirForPreparation.update(Arrays.asList( 186 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE), 187 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE), 188 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE), 189 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE), 190 newAddFontFamilyRequest("<family name='foobar'>" 191 + " <font>foo.ttf</font>" 192 + " <font>bar.ttf</font>" 193 + "</family>"))); 194 // Verifies that getLastModifiedTimeMillis() returns the value of currentTimeMillis. 195 assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis()) 196 .isEqualTo(CURRENT_TIME); 197 // Four font dirs are created. 198 assertThat(mUpdatableFontFilesDir.list()).hasLength(4); 199 assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedTimeMillis()) 200 .isNotEqualTo(expectedModifiedDate); 201 202 UpdatableFontDir dir = new UpdatableFontDir( 203 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 204 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 205 dir.loadFontFileMap(); 206 assertThat(dir.getPostScriptMap()).containsKey("foo"); 207 assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(3); 208 assertThat(dir.getPostScriptMap()).containsKey("bar"); 209 assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(4); 210 // Outdated font dir should be deleted. 211 assertThat(mUpdatableFontFilesDir.list()).hasLength(2); 212 assertNamedFamilyExists(dir.getSystemFontConfig(), "foobar"); 213 assertThat(dir.getFontFamilyMap()).containsKey("foobar"); 214 FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar"); 215 assertThat(foobar.getFontList()).hasSize(2); 216 assertThat(foobar.getFontList().get(0).getFile()) 217 .isEqualTo(dir.getPostScriptMap().get("foo")); 218 assertThat(foobar.getFontList().get(1).getFile()) 219 .isEqualTo(dir.getPostScriptMap().get("bar")); 220 } 221 222 @Test construct_empty()223 public void construct_empty() { 224 UpdatableFontDir dir = new UpdatableFontDir( 225 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 226 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 227 dir.loadFontFileMap(); 228 assertThat(dir.getPostScriptMap()).isEmpty(); 229 assertThat(dir.getFontFamilyMap()).isEmpty(); 230 } 231 232 @Test construct_missingFsverity()233 public void construct_missingFsverity() throws Exception { 234 UpdatableFontDir dirForPreparation = new UpdatableFontDir( 235 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 236 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 237 dirForPreparation.loadFontFileMap(); 238 dirForPreparation.update(Arrays.asList( 239 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE), 240 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE), 241 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE), 242 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE), 243 newAddFontFamilyRequest("<family name='foobar'>" 244 + " <font>foo.ttf</font>" 245 + " <font>bar.ttf</font>" 246 + "</family>"))); 247 // Four font dirs are created. 248 assertThat(mUpdatableFontFilesDir.list()).hasLength(4); 249 250 mFakeFsverityUtil.remove( 251 dirForPreparation.getPostScriptMap().get("foo").getAbsolutePath()); 252 UpdatableFontDir dir = new UpdatableFontDir( 253 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 254 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 255 dir.loadFontFileMap(); 256 assertThat(dir.getPostScriptMap()).isEmpty(); 257 // All font dirs (including dir for "bar.ttf") should be deleted. 258 assertThat(mUpdatableFontFilesDir.list()).hasLength(0); 259 assertThat(dir.getFontFamilyMap()).isEmpty(); 260 } 261 262 @Test construct_fontNameMismatch()263 public void construct_fontNameMismatch() throws Exception { 264 UpdatableFontDir dirForPreparation = new UpdatableFontDir( 265 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 266 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 267 dirForPreparation.loadFontFileMap(); 268 dirForPreparation.update(Arrays.asList( 269 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE), 270 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE), 271 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE), 272 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE), 273 newAddFontFamilyRequest("<family name='foobar'>" 274 + " <font>foo.ttf</font>" 275 + " <font>bar.ttf</font>" 276 + "</family>"))); 277 // Four font dirs are created. 278 assertThat(mUpdatableFontFilesDir.list()).hasLength(4); 279 280 // Overwrite "foo.ttf" with wrong contents. 281 FileUtils.stringToFile(dirForPreparation.getPostScriptMap().get("foo"), "bar,4"); 282 283 UpdatableFontDir dir = new UpdatableFontDir( 284 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 285 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 286 dir.loadFontFileMap(); 287 assertThat(dir.getPostScriptMap()).isEmpty(); 288 // All font dirs (including dir for "bar.ttf") should be deleted. 289 assertThat(mUpdatableFontFilesDir.list()).hasLength(0); 290 assertThat(dir.getFontFamilyMap()).isEmpty(); 291 } 292 293 @Test construct_olderThanPreinstalledFont()294 public void construct_olderThanPreinstalledFont() throws Exception { 295 Function<Map<String, File>, FontConfig> configSupplier = (map) -> { 296 FontConfig.Font fooFont = new FontConfig.Font( 297 new File(mPreinstalledFontDirs.get(0), "foo.ttf"), null, "foo", 298 new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null); 299 FontConfig.Font barFont = new FontConfig.Font( 300 new File(mPreinstalledFontDirs.get(1), "bar.ttf"), null, "bar", 301 new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, null); 302 303 FontConfig.FontFamily family = new FontConfig.FontFamily( 304 Arrays.asList(fooFont, barFont), "sans-serif", null, 305 FontConfig.FontFamily.VARIANT_DEFAULT); 306 return new FontConfig(Collections.singletonList(family), 307 Collections.emptyList(), 0, 1); 308 }; 309 310 UpdatableFontDir dirForPreparation = new UpdatableFontDir( 311 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 312 mConfigFile, mCurrentTimeSupplier, configSupplier); 313 dirForPreparation.loadFontFileMap(); 314 dirForPreparation.update(Arrays.asList( 315 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE), 316 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE), 317 newFontUpdateRequest("foo.ttf,3,foo", GOOD_SIGNATURE), 318 newFontUpdateRequest("bar.ttf,4,bar", GOOD_SIGNATURE), 319 newAddFontFamilyRequest("<family name='foobar'>" 320 + " <font>foo.ttf</font>" 321 + " <font>bar.ttf</font>" 322 + "</family>"))); 323 // Four font dirs are created. 324 assertThat(mUpdatableFontFilesDir.list()).hasLength(4); 325 326 // Add preinstalled fonts. 327 FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "foo.ttf"), "foo,5,foo"); 328 FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,1,bar"); 329 FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,2,bar"); 330 UpdatableFontDir dir = new UpdatableFontDir( 331 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 332 mConfigFile, mCurrentTimeSupplier, configSupplier); 333 dir.loadFontFileMap(); 334 // For foo.ttf, preinstalled font (revision 5) should be used. 335 assertThat(dir.getPostScriptMap()).doesNotContainKey("foo"); 336 // For bar.ttf, updated font (revision 4) should be used. 337 assertThat(dir.getPostScriptMap()).containsKey("bar"); 338 assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(4); 339 // Outdated font dir should be deleted. 340 // We don't delete bar.ttf in this case, because it's normal that OTA updates preinstalled 341 // fonts. 342 assertThat(mUpdatableFontFilesDir.list()).hasLength(1); 343 // Font family depending on obsoleted font should be removed. 344 assertThat(dir.getFontFamilyMap()).isEmpty(); 345 } 346 347 @Test construct_failedToLoadConfig()348 public void construct_failedToLoadConfig() throws Exception { 349 UpdatableFontDir dir = new UpdatableFontDir( 350 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 351 new File("/dev/null"), mCurrentTimeSupplier, mConfigSupplier); 352 dir.loadFontFileMap(); 353 assertThat(dir.getPostScriptMap()).isEmpty(); 354 assertThat(dir.getFontFamilyMap()).isEmpty(); 355 } 356 357 @Test construct_afterBatchFailure()358 public void construct_afterBatchFailure() throws Exception { 359 UpdatableFontDir dirForPreparation = new UpdatableFontDir( 360 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 361 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 362 dirForPreparation.loadFontFileMap(); 363 dirForPreparation.update(Arrays.asList( 364 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE), 365 newAddFontFamilyRequest("<family name='foobar'>" 366 + " <font>foo.ttf</font>" 367 + "</family>"))); 368 try { 369 dirForPreparation.update(Arrays.asList( 370 newFontUpdateRequest("foo.ttf,2,foo", GOOD_SIGNATURE), 371 newFontUpdateRequest("bar.ttf,2,bar", "Invalid signature"), 372 newAddFontFamilyRequest("<family name='foobar'>" 373 + " <font>foo.ttf</font>" 374 + " <font>bar.ttf</font>" 375 + "</family>"))); 376 fail("Batch update with invalid signature should fail"); 377 } catch (FontManagerService.SystemFontException e) { 378 // Expected 379 } 380 381 UpdatableFontDir dir = new UpdatableFontDir( 382 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 383 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 384 dir.loadFontFileMap(); 385 // The state should be rolled back as a whole if one of the update requests fail. 386 assertThat(dir.getPostScriptMap()).containsKey("foo"); 387 assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1); 388 assertThat(dir.getFontFamilyMap()).containsKey("foobar"); 389 FontConfig.FontFamily foobar = dir.getFontFamilyMap().get("foobar"); 390 assertThat(foobar.getFontList()).hasSize(1); 391 assertThat(foobar.getFontList().get(0).getFile()) 392 .isEqualTo(dir.getPostScriptMap().get("foo")); 393 } 394 395 @Test loadFontFileMap_twice()396 public void loadFontFileMap_twice() throws Exception { 397 UpdatableFontDir dir = new UpdatableFontDir( 398 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 399 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 400 dir.loadFontFileMap(); 401 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 402 GOOD_SIGNATURE))); 403 assertThat(dir.getPostScriptMap()).containsKey("test"); 404 File fontFile = dir.getPostScriptMap().get("test"); 405 dir.loadFontFileMap(); 406 assertThat(dir.getPostScriptMap().get("test")).isEqualTo(fontFile); 407 } 408 409 @Test installFontFile()410 public void installFontFile() throws Exception { 411 UpdatableFontDir dir = new UpdatableFontDir( 412 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 413 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 414 dir.loadFontFileMap(); 415 416 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 417 GOOD_SIGNATURE))); 418 assertThat(dir.getPostScriptMap()).containsKey("test"); 419 assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1); 420 File fontFile = dir.getPostScriptMap().get("test"); 421 assertThat(Os.stat(fontFile.getAbsolutePath()).st_mode & 0777).isEqualTo(0644); 422 File fontDir = fontFile.getParentFile(); 423 assertThat(Os.stat(fontDir.getAbsolutePath()).st_mode & 0777).isEqualTo(0711); 424 } 425 426 @Test installFontFile_upgrade()427 public void installFontFile_upgrade() throws Exception { 428 UpdatableFontDir dir = new UpdatableFontDir( 429 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 430 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 431 dir.loadFontFileMap(); 432 433 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 434 GOOD_SIGNATURE))); 435 Map<String, File> mapBeforeUpgrade = dir.getPostScriptMap(); 436 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test", 437 GOOD_SIGNATURE))); 438 assertThat(dir.getPostScriptMap()).containsKey("test"); 439 assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2); 440 assertThat(mapBeforeUpgrade).containsKey("test"); 441 assertWithMessage("Older fonts should not be deleted until next loadFontFileMap") 442 .that(mParser.getRevision(mapBeforeUpgrade.get("test"))).isEqualTo(1); 443 // Check that updatedFontDirs is pruned. 444 assertWithMessage("config.updatedFontDirs should only list latest active dirs") 445 .that(readConfig(mConfigFile).updatedFontDirs) 446 .containsExactly(dir.getPostScriptMap().get("test").getParentFile().getName()); 447 } 448 449 @Test installFontFile_systemFontHasPSNameDifferentFromFileName()450 public void installFontFile_systemFontHasPSNameDifferentFromFileName() throws Exception { 451 452 // Setup the environment that the system installed font file named "foo.ttf" has PostScript 453 // name "bar". 454 File file = new File(mPreinstalledFontDirs.get(0), "foo.ttf"); 455 FileUtils.stringToFile(file, "foo.ttf,1,bar"); 456 UpdatableFontDir dir = new UpdatableFontDir( 457 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 458 mConfigFile, mCurrentTimeSupplier, (map) -> { 459 FontConfig.Font font = new FontConfig.Font( 460 file, null, "bar", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 461 0, null, null); 462 FontConfig.FontFamily family = new FontConfig.FontFamily( 463 Collections.singletonList(font), "sans-serif", null, 464 FontConfig.FontFamily.VARIANT_DEFAULT); 465 return new FontConfig(Collections.singletonList(family), 466 Collections.emptyList(), 0, 1); 467 }); 468 dir.loadFontFileMap(); 469 470 dir.update(Collections.singletonList(newFontUpdateRequest("bar.ttf,2,bar", 471 GOOD_SIGNATURE))); 472 assertThat(dir.getPostScriptMap()).containsKey("bar"); 473 assertThat(dir.getPostScriptMap().size()).isEqualTo(1); 474 assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2); 475 File fontFile = dir.getPostScriptMap().get("bar"); 476 assertThat(Os.stat(fontFile.getAbsolutePath()).st_mode & 0777).isEqualTo(0644); 477 File fontDir = fontFile.getParentFile(); 478 assertThat(Os.stat(fontDir.getAbsolutePath()).st_mode & 0777).isEqualTo(0711); 479 } 480 481 @Test installFontFile_sameVersion()482 public void installFontFile_sameVersion() throws Exception { 483 UpdatableFontDir dir = new UpdatableFontDir( 484 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 485 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 486 dir.loadFontFileMap(); 487 488 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 489 GOOD_SIGNATURE))); 490 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 491 GOOD_SIGNATURE))); 492 assertThat(dir.getPostScriptMap()).containsKey("test"); 493 assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1); 494 } 495 496 @Test installFontFile_downgrade()497 public void installFontFile_downgrade() throws Exception { 498 UpdatableFontDir dir = new UpdatableFontDir( 499 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 500 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 501 dir.loadFontFileMap(); 502 503 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test", 504 GOOD_SIGNATURE))); 505 try { 506 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 507 GOOD_SIGNATURE))); 508 fail("Expect SystemFontException"); 509 } catch (FontManagerService.SystemFontException e) { 510 assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING); 511 } 512 assertThat(dir.getPostScriptMap()).containsKey("test"); 513 assertWithMessage("Font should not be downgraded to an older revision") 514 .that(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2); 515 // Check that updatedFontDirs is not updated. 516 assertWithMessage("config.updatedFontDirs should only list latest active dirs") 517 .that(readConfig(mConfigFile).updatedFontDirs) 518 .containsExactly(dir.getPostScriptMap().get("test").getParentFile().getName()); 519 } 520 521 @Test installFontFile_multiple()522 public void installFontFile_multiple() throws Exception { 523 UpdatableFontDir dir = new UpdatableFontDir( 524 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 525 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 526 dir.loadFontFileMap(); 527 528 dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo", 529 GOOD_SIGNATURE))); 530 dir.update(Collections.singletonList(newFontUpdateRequest("bar.ttf,2,bar", 531 GOOD_SIGNATURE))); 532 assertThat(dir.getPostScriptMap()).containsKey("foo"); 533 assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1); 534 assertThat(dir.getPostScriptMap()).containsKey("bar"); 535 assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2); 536 } 537 538 @Test installFontFile_batch()539 public void installFontFile_batch() throws Exception { 540 UpdatableFontDir dir = new UpdatableFontDir( 541 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 542 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 543 dir.loadFontFileMap(); 544 545 dir.update(Arrays.asList( 546 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE), 547 newFontUpdateRequest("bar.ttf,2,bar", GOOD_SIGNATURE))); 548 assertThat(dir.getPostScriptMap()).containsKey("foo"); 549 assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1); 550 assertThat(dir.getPostScriptMap()).containsKey("bar"); 551 assertThat(mParser.getRevision(dir.getPostScriptMap().get("bar"))).isEqualTo(2); 552 } 553 554 @Test installFontFile_invalidSignature()555 public void installFontFile_invalidSignature() throws Exception { 556 UpdatableFontDir dir = new UpdatableFontDir( 557 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 558 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 559 dir.loadFontFileMap(); 560 561 try { 562 dir.update( 563 Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 564 "Invalid signature"))); 565 fail("Expect SystemFontException"); 566 } catch (FontManagerService.SystemFontException e) { 567 assertThat(e.getErrorCode()) 568 .isEqualTo(FontManager.RESULT_ERROR_VERIFICATION_FAILURE); 569 } 570 assertThat(dir.getPostScriptMap()).isEmpty(); 571 } 572 573 @Test installFontFile_preinstalled_upgrade()574 public void installFontFile_preinstalled_upgrade() throws Exception { 575 FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"), 576 "test.ttf,1,test"); 577 UpdatableFontDir dir = new UpdatableFontDir( 578 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 579 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 580 dir.loadFontFileMap(); 581 582 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,2,test", 583 GOOD_SIGNATURE))); 584 assertThat(dir.getPostScriptMap()).containsKey("test"); 585 assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(2); 586 } 587 588 @Test installFontFile_preinstalled_sameVersion()589 public void installFontFile_preinstalled_sameVersion() throws Exception { 590 FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"), 591 "test.ttf,1,test"); 592 UpdatableFontDir dir = new UpdatableFontDir( 593 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 594 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 595 dir.loadFontFileMap(); 596 597 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 598 GOOD_SIGNATURE))); 599 assertThat(dir.getPostScriptMap()).containsKey("test"); 600 assertThat(mParser.getRevision(dir.getPostScriptMap().get("test"))).isEqualTo(1); 601 } 602 603 @Test installFontFile_preinstalled_downgrade()604 public void installFontFile_preinstalled_downgrade() throws Exception { 605 File file = new File(mPreinstalledFontDirs.get(0), "test.ttf"); 606 FileUtils.stringToFile(file, "test.ttf,2,test"); 607 UpdatableFontDir dir = new UpdatableFontDir( 608 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 609 mConfigFile, mCurrentTimeSupplier, (map) -> { 610 FontConfig.Font font = new FontConfig.Font( 611 file, null, "test", new FontStyle(400, FontStyle.FONT_SLANT_UPRIGHT), 0, null, 612 null); 613 FontConfig.FontFamily family = new FontConfig.FontFamily( 614 Collections.singletonList(font), "sans-serif", null, 615 FontConfig.FontFamily.VARIANT_DEFAULT); 616 return new FontConfig(Collections.singletonList(family), Collections.emptyList(), 0, 1); 617 }); 618 dir.loadFontFileMap(); 619 620 try { 621 dir.update(Collections.singletonList(newFontUpdateRequest("test.ttf,1,test", 622 GOOD_SIGNATURE))); 623 fail("Expect SystemFontException"); 624 } catch (FontManagerService.SystemFontException e) { 625 assertThat(e.getErrorCode()).isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING); 626 } 627 assertThat(dir.getPostScriptMap()).isEmpty(); 628 } 629 630 @Test installFontFile_failedToWriteConfigXml()631 public void installFontFile_failedToWriteConfigXml() throws Exception { 632 long expectedModifiedDate = 1234567890; 633 FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"), 634 "test.ttf,1,test"); 635 636 File readonlyDir = new File(mCacheDir, "readonly"); 637 assertThat(readonlyDir.mkdir()).isTrue(); 638 File readonlyFile = new File(readonlyDir, "readonly_config.xml"); 639 640 PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); 641 config.lastModifiedMillis = expectedModifiedDate; 642 writeConfig(config, readonlyFile); 643 644 assertThat(readonlyDir.setWritable(false, false)).isTrue(); 645 try { 646 UpdatableFontDir dir = new UpdatableFontDir( 647 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 648 readonlyFile, mCurrentTimeSupplier, mConfigSupplier); 649 dir.loadFontFileMap(); 650 651 try { 652 dir.update( 653 Collections.singletonList(newFontUpdateRequest("test.ttf,2,test", 654 GOOD_SIGNATURE))); 655 } catch (FontManagerService.SystemFontException e) { 656 assertThat(e.getErrorCode()) 657 .isEqualTo(FontManager.RESULT_ERROR_FAILED_UPDATE_CONFIG); 658 } 659 assertThat(dir.getSystemFontConfig().getLastModifiedTimeMillis()) 660 .isEqualTo(expectedModifiedDate); 661 assertThat(dir.getPostScriptMap()).isEmpty(); 662 } finally { 663 assertThat(readonlyDir.setWritable(true, true)).isTrue(); 664 } 665 } 666 667 @Test installFontFile_failedToParsePostScript()668 public void installFontFile_failedToParsePostScript() throws Exception { 669 UpdatableFontDir dir = new UpdatableFontDir( 670 mUpdatableFontFilesDir, 671 new UpdatableFontDir.FontFileParser() { 672 673 @Override 674 public String getPostScriptName(File file) throws IOException { 675 return null; 676 } 677 678 @Override 679 public String buildFontFileName(File file) throws IOException { 680 return null; 681 } 682 683 @Override 684 public long getRevision(File file) throws IOException { 685 return 0; 686 } 687 688 @Override 689 public void tryToCreateTypeface(File file) throws IOException { 690 } 691 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 692 dir.loadFontFileMap(); 693 694 try { 695 dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo", 696 GOOD_SIGNATURE))); 697 fail("Expect SystemFontException"); 698 } catch (FontManagerService.SystemFontException e) { 699 assertThat(e.getErrorCode()) 700 .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_NAME); 701 } 702 assertThat(dir.getPostScriptMap()).isEmpty(); 703 } 704 705 @Test installFontFile_failedToParsePostScriptName_invalidFont()706 public void installFontFile_failedToParsePostScriptName_invalidFont() throws Exception { 707 UpdatableFontDir dir = new UpdatableFontDir( 708 mUpdatableFontFilesDir, 709 new UpdatableFontDir.FontFileParser() { 710 @Override 711 public String getPostScriptName(File file) throws IOException { 712 throw new IOException(); 713 } 714 715 @Override 716 public String buildFontFileName(File file) throws IOException { 717 throw new IOException(); 718 } 719 720 @Override 721 public long getRevision(File file) throws IOException { 722 return 0; 723 } 724 725 @Override 726 public void tryToCreateTypeface(File file) throws IOException { 727 } 728 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 729 dir.loadFontFileMap(); 730 731 try { 732 dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo", 733 GOOD_SIGNATURE))); 734 fail("Expect SystemFontException"); 735 } catch (FontManagerService.SystemFontException e) { 736 assertThat(e.getErrorCode()) 737 .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_FILE); 738 } 739 assertThat(dir.getPostScriptMap()).isEmpty(); 740 } 741 742 @Test installFontFile_failedToCreateTypeface()743 public void installFontFile_failedToCreateTypeface() throws Exception { 744 UpdatableFontDir dir = new UpdatableFontDir( 745 mUpdatableFontFilesDir, 746 new UpdatableFontDir.FontFileParser() { 747 @Override 748 public String getPostScriptName(File file) throws IOException { 749 return mParser.getPostScriptName(file); 750 } 751 752 @Override 753 public String buildFontFileName(File file) throws IOException { 754 return mParser.buildFontFileName(file); 755 } 756 757 @Override 758 public long getRevision(File file) throws IOException { 759 return mParser.getRevision(file); 760 } 761 762 @Override 763 public void tryToCreateTypeface(File file) throws IOException { 764 throw new IOException(); 765 } 766 }, mFakeFsverityUtil, mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 767 dir.loadFontFileMap(); 768 769 try { 770 dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo", 771 GOOD_SIGNATURE))); 772 fail("Expect SystemFontException"); 773 } catch (FontManagerService.SystemFontException e) { 774 assertThat(e.getErrorCode()) 775 .isEqualTo(FontManager.RESULT_ERROR_INVALID_FONT_FILE); 776 } 777 assertThat(dir.getPostScriptMap()).isEmpty(); 778 } 779 780 @Test installFontFile_renameToPsNameFailure()781 public void installFontFile_renameToPsNameFailure() throws Exception { 782 UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() { 783 784 @Override 785 public boolean hasFsverity(String path) { 786 return mFakeFsverityUtil.hasFsverity(path); 787 } 788 789 @Override 790 public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException { 791 mFakeFsverityUtil.setUpFsverity(path, pkcs7Signature); 792 } 793 794 @Override 795 public boolean rename(File src, File dest) { 796 return false; 797 } 798 }; 799 UpdatableFontDir dir = new UpdatableFontDir( 800 mUpdatableFontFilesDir, mParser, fakeFsverityUtil, 801 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 802 dir.loadFontFileMap(); 803 804 try { 805 dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo", 806 GOOD_SIGNATURE))); 807 fail("Expect SystemFontException"); 808 } catch (FontManagerService.SystemFontException e) { 809 assertThat(e.getErrorCode()) 810 .isEqualTo(FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE); 811 } 812 assertThat(dir.getPostScriptMap()).isEmpty(); 813 } 814 815 @Test installFontFile_batchFailure()816 public void installFontFile_batchFailure() throws Exception { 817 UpdatableFontDir dir = new UpdatableFontDir( 818 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 819 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 820 dir.loadFontFileMap(); 821 822 dir.update(Collections.singletonList(newFontUpdateRequest("foo.ttf,1,foo", 823 GOOD_SIGNATURE))); 824 try { 825 dir.update(Arrays.asList( 826 newFontUpdateRequest("foo.ttf,2,foo", GOOD_SIGNATURE), 827 newFontUpdateRequest("bar.ttf,2,bar", "Invalid signature"))); 828 fail("Batch update with invalid signature should fail"); 829 } catch (FontManagerService.SystemFontException e) { 830 // Expected 831 } 832 // The state should be rolled back as a whole if one of the update requests fail. 833 assertThat(dir.getPostScriptMap()).containsKey("foo"); 834 assertThat(mParser.getRevision(dir.getPostScriptMap().get("foo"))).isEqualTo(1); 835 } 836 837 @Test addFontFamily()838 public void addFontFamily() throws Exception { 839 UpdatableFontDir dir = new UpdatableFontDir( 840 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 841 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 842 dir.loadFontFileMap(); 843 844 dir.update(Arrays.asList( 845 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE), 846 newAddFontFamilyRequest("<family name='test'>" 847 + " <font>test.ttf</font>" 848 + "</family>"))); 849 assertThat(dir.getPostScriptMap()).containsKey("test"); 850 assertThat(dir.getFontFamilyMap()).containsKey("test"); 851 FontConfig.FontFamily test = dir.getFontFamilyMap().get("test"); 852 assertThat(test.getFontList()).hasSize(1); 853 assertThat(test.getFontList().get(0).getFile()) 854 .isEqualTo(dir.getPostScriptMap().get("test")); 855 } 856 857 @Test addFontFamily_noName()858 public void addFontFamily_noName() throws Exception { 859 UpdatableFontDir dir = new UpdatableFontDir( 860 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 861 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 862 dir.loadFontFileMap(); 863 864 List<FontUpdateRequest> requests = Arrays.asList( 865 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE), 866 newAddFontFamilyRequest("<family lang='en'>" 867 + " <font>test.ttf</font>" 868 + "</family>")); 869 try { 870 dir.update(requests); 871 fail("Expect NullPointerException"); 872 } catch (NullPointerException e) { 873 // Expect 874 } 875 } 876 877 @Test addFontFamily_fontNotAvailable()878 public void addFontFamily_fontNotAvailable() throws Exception { 879 UpdatableFontDir dir = new UpdatableFontDir( 880 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 881 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 882 dir.loadFontFileMap(); 883 884 try { 885 dir.update(Arrays.asList(newAddFontFamilyRequest("<family name='test'>" 886 + " <font>test.ttf</font>" 887 + "</family>"))); 888 fail("Expect SystemFontException"); 889 } catch (FontManagerService.SystemFontException e) { 890 assertThat(e.getErrorCode()) 891 .isEqualTo(FontManager.RESULT_ERROR_FONT_NOT_FOUND); 892 } 893 } 894 895 @Test getSystemFontConfig()896 public void getSystemFontConfig() throws Exception { 897 UpdatableFontDir dir = new UpdatableFontDir( 898 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 899 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 900 dir.loadFontFileMap(); 901 // We assume we have monospace. 902 assertNamedFamilyExists(dir.getSystemFontConfig(), "monospace"); 903 904 dir.update(Arrays.asList( 905 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE), 906 // Updating an existing font family. 907 newAddFontFamilyRequest("<family name='monospace'>" 908 + " <font>test.ttf</font>" 909 + "</family>"), 910 // Adding a new font family. 911 newAddFontFamilyRequest("<family name='test'>" 912 + " <font>test.ttf</font>" 913 + "</family>"))); 914 FontConfig fontConfig = dir.getSystemFontConfig(); 915 assertNamedFamilyExists(fontConfig, "monospace"); 916 FontConfig.FontFamily monospace = getLastFamily(fontConfig, "monospace"); 917 assertThat(monospace.getFontList()).hasSize(1); 918 assertThat(monospace.getFontList().get(0).getFile()) 919 .isEqualTo(dir.getPostScriptMap().get("test")); 920 assertNamedFamilyExists(fontConfig, "test"); 921 assertThat(getLastFamily(fontConfig, "test").getFontList()) 922 .isEqualTo(monospace.getFontList()); 923 } 924 925 @Test getSystemFontConfig_preserveFirstFontFamily()926 public void getSystemFontConfig_preserveFirstFontFamily() throws Exception { 927 UpdatableFontDir dir = new UpdatableFontDir( 928 mUpdatableFontFilesDir, mParser, mFakeFsverityUtil, 929 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 930 dir.loadFontFileMap(); 931 assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty(); 932 FontConfig.FontFamily firstFontFamily = dir.getSystemFontConfig().getFontFamilies().get(0); 933 assertThat(firstFontFamily.getName()).isNotEmpty(); 934 935 dir.update(Arrays.asList( 936 newFontUpdateRequest("test.ttf,1,test", GOOD_SIGNATURE), 937 newAddFontFamilyRequest("<family name='" + firstFontFamily.getName() + "'>" 938 + " <font>test.ttf</font>" 939 + "</family>"))); 940 FontConfig fontConfig = dir.getSystemFontConfig(); 941 assertThat(dir.getSystemFontConfig().getFontFamilies()).isNotEmpty(); 942 assertThat(fontConfig.getFontFamilies().get(0)).isEqualTo(firstFontFamily); 943 FontConfig.FontFamily updated = getLastFamily(fontConfig, firstFontFamily.getName()); 944 assertThat(updated.getFontList()).hasSize(1); 945 assertThat(updated.getFontList().get(0).getFile()) 946 .isEqualTo(dir.getPostScriptMap().get("test")); 947 assertThat(updated).isNotEqualTo(firstFontFamily); 948 } 949 950 @Test deleteAllFiles()951 public void deleteAllFiles() throws Exception { 952 FakeFontFileParser parser = new FakeFontFileParser(); 953 FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); 954 UpdatableFontDir dirForPreparation = new UpdatableFontDir( 955 mUpdatableFontFilesDir, parser, fakeFsverityUtil, 956 mConfigFile, mCurrentTimeSupplier, mConfigSupplier); 957 dirForPreparation.loadFontFileMap(); 958 dirForPreparation.update(Collections.singletonList( 959 newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE))); 960 assertThat(mConfigFile.exists()).isTrue(); 961 assertThat(mUpdatableFontFilesDir.list()).hasLength(1); 962 963 UpdatableFontDir.deleteAllFiles(mUpdatableFontFilesDir, mConfigFile); 964 assertThat(mConfigFile.exists()).isFalse(); 965 assertThat(mUpdatableFontFilesDir.list()).hasLength(0); 966 } 967 newFontUpdateRequest(String content, String signature)968 private FontUpdateRequest newFontUpdateRequest(String content, String signature) 969 throws Exception { 970 File file = File.createTempFile("font", "ttf", mCacheDir); 971 FileUtils.stringToFile(file, content); 972 return new FontUpdateRequest( 973 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY), 974 signature.getBytes()); 975 } 976 newAddFontFamilyRequest(String xml)977 private static FontUpdateRequest newAddFontFamilyRequest(String xml) throws Exception { 978 XmlPullParser mParser = Xml.newPullParser(); 979 ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); 980 mParser.setInput(is, "UTF-8"); 981 mParser.nextTag(); 982 983 FontConfig.FontFamily fontFamily = FontListParser.readFamily(mParser, "", null, true); 984 List<FontUpdateRequest.Font> fonts = new ArrayList<>(); 985 for (FontConfig.Font font : fontFamily.getFontList()) { 986 String name = font.getFile().getName(); 987 String psName = name.substring(0, name.length() - 4); // drop suffix 988 FontUpdateRequest.Font updateFont = new FontUpdateRequest.Font( 989 psName, font.getStyle(), font.getTtcIndex(), font.getFontVariationSettings()); 990 fonts.add(updateFont); 991 } 992 FontUpdateRequest.Family family = new FontUpdateRequest.Family(fontFamily.getName(), fonts); 993 return new FontUpdateRequest(family); 994 } 995 readConfig(File file)996 private static PersistentSystemFontConfig.Config readConfig(File file) throws Exception { 997 PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); 998 try (InputStream is = new FileInputStream(file)) { 999 PersistentSystemFontConfig.loadFromXml(is, config); 1000 } 1001 return config; 1002 } 1003 writeConfig(PersistentSystemFontConfig.Config config, File file)1004 private static void writeConfig(PersistentSystemFontConfig.Config config, 1005 File file) throws IOException { 1006 try (FileOutputStream fos = new FileOutputStream(file)) { 1007 PersistentSystemFontConfig.writeToXml(fos, config); 1008 } 1009 } 1010 1011 // Returns the last family with the given name, which will be used for creating Typeface. getLastFamily(FontConfig fontConfig, String familyName)1012 private static FontConfig.FontFamily getLastFamily(FontConfig fontConfig, String familyName) { 1013 List<FontConfig.FontFamily> fontFamilies = fontConfig.getFontFamilies(); 1014 for (int i = fontFamilies.size() - 1; i >= 0; i--) { 1015 if (familyName.equals(fontFamilies.get(i).getName())) { 1016 return fontFamilies.get(i); 1017 } 1018 } 1019 return null; 1020 } 1021 assertNamedFamilyExists(FontConfig fontConfig, String familyName)1022 private static void assertNamedFamilyExists(FontConfig fontConfig, String familyName) { 1023 assertThat(fontConfig.getFontFamilies().stream() 1024 .map(FontConfig.FontFamily::getName) 1025 .collect(Collectors.toSet())).contains(familyName); 1026 } 1027 } 1028