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.updatablesystemfont; 18 19 import static android.graphics.fonts.FontStyle.FONT_SLANT_UPRIGHT; 20 import static android.graphics.fonts.FontStyle.FONT_WEIGHT_BOLD; 21 import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL; 22 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 23 24 import static com.google.common.truth.Truth.assertThat; 25 26 import static org.junit.Assert.assertThrows; 27 import static org.junit.Assume.assumeTrue; 28 29 import static java.util.concurrent.TimeUnit.SECONDS; 30 31 import android.app.UiAutomation; 32 import android.content.Context; 33 import android.graphics.fonts.FontFamilyUpdateRequest; 34 import android.graphics.fonts.FontFileUpdateRequest; 35 import android.graphics.fonts.FontManager; 36 import android.graphics.fonts.FontStyle; 37 import android.os.ParcelFileDescriptor; 38 import android.platform.test.annotations.RootPermissionTest; 39 import android.security.FileIntegrityManager; 40 import android.text.FontConfig; 41 import android.util.Log; 42 import android.util.Pair; 43 44 import androidx.annotation.Nullable; 45 import androidx.test.ext.junit.runners.AndroidJUnit4; 46 import androidx.test.platform.app.InstrumentationRegistry; 47 import androidx.test.uiautomator.By; 48 import androidx.test.uiautomator.UiDevice; 49 import androidx.test.uiautomator.Until; 50 51 import com.android.compatibility.common.util.StreamUtil; 52 import com.android.compatibility.common.util.SystemUtil; 53 54 import org.junit.After; 55 import org.junit.Before; 56 import org.junit.Test; 57 import org.junit.runner.RunWith; 58 59 import java.io.File; 60 import java.io.FileInputStream; 61 import java.io.FileOutputStream; 62 import java.io.IOException; 63 import java.io.InputStream; 64 import java.io.OutputStream; 65 import java.nio.file.Files; 66 import java.nio.file.Paths; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.Collections; 70 import java.util.List; 71 import java.util.regex.Pattern; 72 import java.util.stream.Stream; 73 74 /** 75 * Tests if fonts can be updated by {@link FontManager} API. 76 */ 77 @RootPermissionTest 78 @RunWith(AndroidJUnit4.class) 79 public class UpdatableSystemFontTest { 80 81 private static final String TAG = "UpdatableSystemFontTest"; 82 private static final String SYSTEM_FONTS_DIR = "/system/fonts/"; 83 private static final String DATA_FONTS_DIR = "/data/fonts/files/"; 84 private static final String CERT_PATH = "/data/local/tmp/UpdatableSystemFontTestCert.der"; 85 86 private static final String NOTO_COLOR_EMOJI_POSTSCRIPT_NAME = "NotoColorEmoji"; 87 private static final String NOTO_COLOR_EMOJI_TTF = 88 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmoji.ttf"; 89 private static final String NOTO_COLOR_EMOJI_SIG = 90 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmoji.sig"; 91 // A font with revision == 0. 92 private static final String TEST_NOTO_COLOR_EMOJI_V0_TTF = 93 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmojiV0.ttf"; 94 private static final String TEST_NOTO_COLOR_EMOJI_V0_SIG = 95 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmojiV0.sig"; 96 // A font with revision == original + 1 97 private static final String TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF = 98 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmojiVPlus1.ttf"; 99 private static final String TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG = 100 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmojiVPlus1.sig"; 101 // A font with revision == original + 2 102 private static final String TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF = 103 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmojiVPlus2.ttf"; 104 private static final String TEST_NOTO_COLOR_EMOJI_VPLUS2_SIG = 105 "/data/local/tmp/UpdatableSystemFontTest_NotoColorEmojiVPlus2.sig"; 106 107 private static final String NOTO_SERIF_REGULAR_POSTSCRIPT_NAME = "NotoSerif"; 108 private static final String NOTO_SERIF_REGULAR_TTF = 109 "/data/local/tmp/UpdatableSystemFontTest_NotoSerif-Regular.ttf"; 110 private static final String NOTO_SERIF_REGULAR_SIG = 111 "/data/local/tmp/UpdatableSystemFontTest_NotoSerif-Regular.sig"; 112 113 private static final String NOTO_SERIF_BOLD_POSTSCRIPT_NAME = "NotoSerif-Bold"; 114 private static final String NOTO_SERIF_BOLD_TTF = 115 "/data/local/tmp/UpdatableSystemFontTest_NotoSerif-Bold.ttf"; 116 private static final String NOTO_SERIF_BOLD_SIG = 117 "/data/local/tmp/UpdatableSystemFontTest_NotoSerif-Bold.sig"; 118 119 private static final String EMOJI_RENDERING_TEST_APP_ID = "com.android.emojirenderingtestapp"; 120 private static final String EMOJI_RENDERING_TEST_ACTIVITY = 121 EMOJI_RENDERING_TEST_APP_ID + "/.EmojiRenderingTestActivity"; 122 // This should be the same as the one in EmojiRenderingTestActivity. 123 private static final String TEST_NOTO_SERIF = "test-noto-serif"; 124 private static final long ACTIVITY_TIMEOUT_MILLIS = SECONDS.toMillis(10); 125 126 private static final String GET_AVAILABLE_FONTS_TEST_ACTIVITY = 127 EMOJI_RENDERING_TEST_APP_ID + "/.GetAvailableFontsTestActivity"; 128 129 private static final Pattern PATTERN_FONT_FILES = Pattern.compile("\\.(ttf|otf|ttc|otc)$"); 130 private static final Pattern PATTERN_TMP_FILES = Pattern.compile("^/data/local/tmp/"); 131 private static final Pattern PATTERN_DATA_FONT_FILES = Pattern.compile("^/data/fonts/files/"); 132 private static final Pattern PATTERN_SYSTEM_FONT_FILES = 133 Pattern.compile("^/(system|product)/fonts/"); 134 135 private String mKeyId; 136 private FontManager mFontManager; 137 private UiDevice mUiDevice; 138 139 @Before setUp()140 public void setUp() throws Exception { 141 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 142 // Run tests only if updatable system font is enabled. 143 FileIntegrityManager fim = context.getSystemService(FileIntegrityManager.class); 144 assumeTrue(fim != null); 145 assumeTrue(fim.isApkVeritySupported()); 146 mKeyId = insertCert(CERT_PATH); 147 mFontManager = context.getSystemService(FontManager.class); 148 expectCommandToSucceed("cmd font clear"); 149 mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 150 } 151 152 @After tearDown()153 public void tearDown() throws Exception { 154 // Ignore errors because this may fail if updatable system font is not enabled. 155 runShellCommand("cmd font clear", null); 156 if (mKeyId != null) { 157 expectCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity"); 158 } 159 } 160 161 @Test updateFont()162 public void updateFont() throws Exception { 163 FontConfig oldFontConfig = 164 SystemUtil.callWithShellPermissionIdentity(mFontManager::getFontConfig); 165 assertThat(updateFontFile( 166 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)) 167 .isEqualTo(FontManager.RESULT_SUCCESS); 168 // Check that font config is updated. 169 String fontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 170 assertThat(fontPath).startsWith(DATA_FONTS_DIR); 171 FontConfig newFontConfig = 172 SystemUtil.callWithShellPermissionIdentity(mFontManager::getFontConfig); 173 assertThat(newFontConfig.getConfigVersion()) 174 .isGreaterThan(oldFontConfig.getConfigVersion()); 175 assertThat(newFontConfig.getLastModifiedTimeMillis()) 176 .isGreaterThan(oldFontConfig.getLastModifiedTimeMillis()); 177 // The updated font should be readable and unmodifiable. 178 expectCommandToSucceed("dd status=none if=" + fontPath + " of=/dev/null"); 179 expectCommandToFail("dd status=none if=" + CERT_PATH + " of=" + fontPath); 180 } 181 182 @Test updateFont_twice()183 public void updateFont_twice() throws Exception { 184 assertThat(updateFontFile( 185 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)) 186 .isEqualTo(FontManager.RESULT_SUCCESS); 187 String fontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 188 assertThat(updateFontFile( 189 TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_SIG)) 190 .isEqualTo(FontManager.RESULT_SUCCESS); 191 String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 192 assertThat(fontPath2).startsWith(DATA_FONTS_DIR); 193 assertThat(fontPath2).isNotEqualTo(fontPath); 194 // The new file should be readable. 195 expectCommandToSucceed("dd status=none if=" + fontPath2 + " of=/dev/null"); 196 // The old file should be still readable. 197 expectCommandToSucceed("dd status=none if=" + fontPath + " of=/dev/null"); 198 } 199 200 @Test updateFont_allowSameVersion()201 public void updateFont_allowSameVersion() throws Exception { 202 // Update original font to the same version 203 assertThat(updateFontFile( 204 NOTO_COLOR_EMOJI_TTF, NOTO_COLOR_EMOJI_SIG)) 205 .isEqualTo(FontManager.RESULT_SUCCESS); 206 String fontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 207 assertThat(updateFontFile( 208 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)) 209 .isEqualTo(FontManager.RESULT_SUCCESS); 210 String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 211 // Update updated font to the same version 212 assertThat(updateFontFile( 213 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)) 214 .isEqualTo(FontManager.RESULT_SUCCESS); 215 String fontPath3 = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 216 assertThat(fontPath).startsWith(DATA_FONTS_DIR); 217 assertThat(fontPath2).isNotEqualTo(fontPath); 218 assertThat(fontPath2).startsWith(DATA_FONTS_DIR); 219 assertThat(fontPath3).startsWith(DATA_FONTS_DIR); 220 assertThat(fontPath3).isNotEqualTo(fontPath); 221 } 222 223 @Test updateFont_invalidCert()224 public void updateFont_invalidCert() throws Exception { 225 assertThat(updateFontFile( 226 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_SIG)) 227 .isEqualTo(FontManager.RESULT_ERROR_VERIFICATION_FAILURE); 228 } 229 230 @Test updateFont_downgradeFromSystem()231 public void updateFont_downgradeFromSystem() throws Exception { 232 assertThat(updateFontFile( 233 TEST_NOTO_COLOR_EMOJI_V0_TTF, TEST_NOTO_COLOR_EMOJI_V0_SIG)) 234 .isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING); 235 } 236 237 @Test updateFont_downgradeFromData()238 public void updateFont_downgradeFromData() throws Exception { 239 assertThat(updateFontFile( 240 TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_SIG)) 241 .isEqualTo(FontManager.RESULT_SUCCESS); 242 assertThat(updateFontFile( 243 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)) 244 .isEqualTo(FontManager.RESULT_ERROR_DOWNGRADING); 245 } 246 247 @Test updateFontFamily()248 public void updateFontFamily() throws Exception { 249 assertThat(updateNotoSerifAs("serif")).isEqualTo(FontManager.RESULT_SUCCESS); 250 final FontConfig.NamedFamilyList namedFamilyList = findFontFamilyOrThrow("serif"); 251 assertThat(namedFamilyList.getFamilies().size()).isEqualTo(1); 252 final FontConfig.FontFamily family = namedFamilyList.getFamilies().get(0); 253 254 assertThat(family.getFontList()).hasSize(2); 255 assertThat(family.getFontList().get(0).getPostScriptName()) 256 .isEqualTo(NOTO_SERIF_REGULAR_POSTSCRIPT_NAME); 257 assertThat(family.getFontList().get(0).getFile().getAbsolutePath()) 258 .startsWith(DATA_FONTS_DIR); 259 assertThat(family.getFontList().get(0).getStyle().getWeight()) 260 .isEqualTo(FONT_WEIGHT_NORMAL); 261 assertThat(family.getFontList().get(1).getPostScriptName()) 262 .isEqualTo(NOTO_SERIF_BOLD_POSTSCRIPT_NAME); 263 assertThat(family.getFontList().get(1).getFile().getAbsolutePath()) 264 .startsWith(DATA_FONTS_DIR); 265 assertThat(family.getFontList().get(1).getStyle().getWeight()).isEqualTo(FONT_WEIGHT_BOLD); 266 } 267 268 @Test updateFontFamily_asNewFont()269 public void updateFontFamily_asNewFont() throws Exception { 270 assertThat(updateNotoSerifAs("UpdatableSystemFontTest-serif")) 271 .isEqualTo(FontManager.RESULT_SUCCESS); 272 final FontConfig.NamedFamilyList namedFamilyList = 273 findFontFamilyOrThrow("UpdatableSystemFontTest-serif"); 274 assertThat(namedFamilyList.getFamilies().size()).isEqualTo(1); 275 final FontConfig.FontFamily family = namedFamilyList.getFamilies().get(0); 276 assertThat(family.getFontList()).hasSize(2); 277 assertThat(family.getFontList().get(0).getPostScriptName()) 278 .isEqualTo(NOTO_SERIF_REGULAR_POSTSCRIPT_NAME); 279 assertThat(family.getFontList().get(1).getPostScriptName()) 280 .isEqualTo(NOTO_SERIF_BOLD_POSTSCRIPT_NAME); 281 } 282 283 @Test launchApp()284 public void launchApp() throws Exception { 285 String fontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 286 assertThat(fontPath).startsWith(SYSTEM_FONTS_DIR); 287 startActivity(EMOJI_RENDERING_TEST_APP_ID, EMOJI_RENDERING_TEST_ACTIVITY); 288 SystemUtil.eventually( 289 () -> assertThat(isFileOpenedBy(fontPath, EMOJI_RENDERING_TEST_APP_ID)).isTrue(), 290 ACTIVITY_TIMEOUT_MILLIS); 291 } 292 293 @Test launchApp_afterUpdateFont()294 public void launchApp_afterUpdateFont() throws Exception { 295 String originalFontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 296 assertThat(originalFontPath).startsWith(SYSTEM_FONTS_DIR); 297 assertThat(updateFontFile( 298 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)) 299 .isEqualTo(FontManager.RESULT_SUCCESS); 300 String updatedFontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 301 assertThat(updatedFontPath).startsWith(DATA_FONTS_DIR); 302 updateNotoSerifAs(TEST_NOTO_SERIF); 303 String notoSerifPath = getFontPath(NOTO_SERIF_REGULAR_POSTSCRIPT_NAME); 304 startActivity(EMOJI_RENDERING_TEST_APP_ID, EMOJI_RENDERING_TEST_ACTIVITY); 305 // The original font should NOT be opened by the app. 306 SystemUtil.eventually(() -> { 307 assertThat(isFileOpenedBy(updatedFontPath, EMOJI_RENDERING_TEST_APP_ID)).isTrue(); 308 assertThat(isFileOpenedBy(originalFontPath, EMOJI_RENDERING_TEST_APP_ID)).isFalse(); 309 assertThat(isFileOpenedBy(notoSerifPath, EMOJI_RENDERING_TEST_APP_ID)).isTrue(); 310 }, ACTIVITY_TIMEOUT_MILLIS); 311 } 312 313 @Test reboot()314 public void reboot() throws Exception { 315 expectCommandToSucceed(String.format("cmd font update %s %s", 316 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)); 317 String fontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 318 assertThat(fontPath).startsWith(DATA_FONTS_DIR); 319 320 // Emulate reboot by 'cmd font restart'. 321 expectCommandToSucceed("cmd font restart"); 322 String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 323 assertThat(fontPathAfterReboot).isEqualTo(fontPath); 324 } 325 326 @Test fdLeakTest()327 public void fdLeakTest() throws Exception { 328 long originalOpenFontCount = 329 countMatch(getOpenFiles("system_server"), PATTERN_FONT_FILES); 330 Pattern patternEmojiVPlus1 = 331 Pattern.compile(Pattern.quote(TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF)); 332 for (int i = 0; i < 10; i++) { 333 assertThat(updateFontFile( 334 TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)) 335 .isEqualTo(FontManager.RESULT_SUCCESS); 336 List<String> openFiles = getOpenFiles("system_server"); 337 for (Pattern p : Arrays.asList(PATTERN_FONT_FILES, PATTERN_SYSTEM_FONT_FILES, 338 PATTERN_DATA_FONT_FILES, PATTERN_TMP_FILES)) { 339 Log.i(TAG, String.format("num of %s: %d", p, countMatch(openFiles, p))); 340 } 341 // system_server should not keep /data/fonts files open. 342 assertThat(countMatch(openFiles, PATTERN_DATA_FONT_FILES)).isEqualTo(0); 343 // system_server should not keep passed FD open. 344 assertThat(countMatch(openFiles, patternEmojiVPlus1)).isEqualTo(0); 345 // The number of open font FD should not increase. 346 assertThat(countMatch(openFiles, PATTERN_FONT_FILES)) 347 .isAtMost(originalOpenFontCount); 348 } 349 } 350 351 @Test fdLeakTest_withoutPermission()352 public void fdLeakTest_withoutPermission() throws Exception { 353 Pattern patternEmojiVPlus1 = 354 Pattern.compile(Pattern.quote(TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF)); 355 byte[] signature = Files.readAllBytes(Paths.get(TEST_NOTO_COLOR_EMOJI_VPLUS1_SIG)); 356 try (ParcelFileDescriptor fd = ParcelFileDescriptor.open( 357 new File(TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF), MODE_READ_ONLY)) { 358 assertThrows(SecurityException.class, 359 () -> updateFontFileWithoutPermission(fd, signature, 0)); 360 } 361 List<String> openFiles = getOpenFiles("system_server"); 362 assertThat(countMatch(openFiles, patternEmojiVPlus1)).isEqualTo(0); 363 } 364 365 @Test getAvailableFonts()366 public void getAvailableFonts() throws Exception { 367 String fontPath = getFontPath(NOTO_COLOR_EMOJI_POSTSCRIPT_NAME); 368 startActivity(EMOJI_RENDERING_TEST_APP_ID, GET_AVAILABLE_FONTS_TEST_ACTIVITY); 369 // GET_AVAILABLE_FONTS_TEST_ACTIVITY shows the NotoColorEmoji path it got. 370 mUiDevice.wait( 371 Until.findObject(By.pkg(EMOJI_RENDERING_TEST_APP_ID).text(fontPath)), 372 ACTIVITY_TIMEOUT_MILLIS); 373 // The font file should not be opened just by querying the path using 374 // SystemFont.getAvailableFonts(). 375 assertThat(isFileOpenedBy(fontPath, EMOJI_RENDERING_TEST_APP_ID)).isFalse(); 376 } 377 insertCert(String certPath)378 private static String insertCert(String certPath) throws Exception { 379 Pair<String, String> result; 380 try (InputStream is = new FileInputStream(certPath)) { 381 result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is); 382 } 383 // /data/local/tmp is not readable by system server. Copy a cert file to /data/fonts 384 final String copiedCert = "/data/fonts/debug_cert.der"; 385 runShellCommand("cp " + certPath + " " + copiedCert, null); 386 runShellCommand("cmd font install-debug-cert " + copiedCert, null); 387 // Assert that there are no errors. 388 assertThat(result.second).isEmpty(); 389 String keyId = result.first.trim(); 390 assertThat(keyId).matches("^\\d+$"); 391 return keyId; 392 } 393 updateFontFile(String fontPath, String signaturePath)394 private int updateFontFile(String fontPath, String signaturePath) throws IOException { 395 byte[] signature = Files.readAllBytes(Paths.get(signaturePath)); 396 try (ParcelFileDescriptor fd = 397 ParcelFileDescriptor.open(new File(fontPath), MODE_READ_ONLY)) { 398 return SystemUtil.runWithShellPermissionIdentity(() -> { 399 int configVersion = mFontManager.getFontConfig().getConfigVersion(); 400 return updateFontFileWithoutPermission(fd, signature, configVersion); 401 }); 402 } 403 } 404 updateFontFileWithoutPermission(ParcelFileDescriptor fd, byte[] signature, int configVersion)405 private int updateFontFileWithoutPermission(ParcelFileDescriptor fd, byte[] signature, 406 int configVersion) { 407 return mFontManager.updateFontFamily( 408 new FontFamilyUpdateRequest.Builder() 409 .addFontFileUpdateRequest(new FontFileUpdateRequest(fd, signature)) 410 .build(), 411 configVersion); 412 } 413 updateNotoSerifAs(String familyName)414 private int updateNotoSerifAs(String familyName) throws IOException { 415 List<FontFamilyUpdateRequest.Font> fonts = Arrays.asList( 416 new FontFamilyUpdateRequest.Font.Builder(NOTO_SERIF_REGULAR_POSTSCRIPT_NAME, 417 new FontStyle(FONT_WEIGHT_NORMAL, FONT_SLANT_UPRIGHT)).build(), 418 new FontFamilyUpdateRequest.Font.Builder(NOTO_SERIF_BOLD_POSTSCRIPT_NAME, 419 new FontStyle(FONT_WEIGHT_BOLD, FONT_SLANT_UPRIGHT)).build()); 420 FontFamilyUpdateRequest.FontFamily fontFamily = 421 new FontFamilyUpdateRequest.FontFamily.Builder(familyName, fonts).build(); 422 byte[] regularSig = Files.readAllBytes(Paths.get(NOTO_SERIF_REGULAR_SIG)); 423 byte[] boldSig = Files.readAllBytes(Paths.get(NOTO_SERIF_BOLD_SIG)); 424 try (ParcelFileDescriptor regularFd = ParcelFileDescriptor.open( 425 new File(NOTO_SERIF_REGULAR_TTF), MODE_READ_ONLY); 426 ParcelFileDescriptor boldFd = ParcelFileDescriptor.open( 427 new File(NOTO_SERIF_BOLD_TTF), MODE_READ_ONLY)) { 428 return SystemUtil.runWithShellPermissionIdentity(() -> { 429 FontConfig fontConfig = mFontManager.getFontConfig(); 430 return mFontManager.updateFontFamily(new FontFamilyUpdateRequest.Builder() 431 .addFontFileUpdateRequest( 432 new FontFileUpdateRequest(regularFd, regularSig)) 433 .addFontFileUpdateRequest( 434 new FontFileUpdateRequest(boldFd, boldSig)) 435 .addFontFamily(fontFamily) 436 .build(), fontConfig.getConfigVersion()); 437 }); 438 } 439 } 440 441 private String getFontPath(String psName) { 442 FontConfig fontConfig = 443 SystemUtil.runWithShellPermissionIdentity(mFontManager::getFontConfig); 444 final List<FontConfig.FontFamily> namedFamilies = fontConfig.getNamedFamilyLists().stream() 445 .flatMap(namedFamily -> namedFamily.getFamilies().stream()).toList(); 446 447 return Stream.concat(fontConfig.getFontFamilies().stream(), namedFamilies.stream()) 448 .flatMap(family -> family.getFontList().stream()) 449 .filter(font -> { 450 Log.e("Debug", "PsName = " + font.getPostScriptName()); 451 return psName.equals(font.getPostScriptName()); 452 }) 453 // Return the last match, because the latter family takes precedence if two families 454 // have the same name. 455 .reduce((first, second) -> second) 456 .orElseThrow(() -> new AssertionError("Font not found: " + psName)) 457 .getFile() 458 .getAbsolutePath(); 459 } 460 461 private FontConfig.NamedFamilyList findFontFamilyOrThrow(String familyName) { 462 FontConfig fontConfig = 463 SystemUtil.runWithShellPermissionIdentity(mFontManager::getFontConfig); 464 return fontConfig.getNamedFamilyLists().stream() 465 .filter(family -> familyName.equals(family.getName())) 466 // Return the last match, because the latter family takes precedence if two families 467 // have the same name. 468 .reduce((first, second) -> second) 469 .orElseThrow(() -> new AssertionError("Family not found: " + familyName)); 470 } 471 472 private static void startActivity(String appId, String activityId) throws Exception { 473 expectCommandToSucceed("am force-stop " + appId); 474 expectCommandToSucceed("am start-activity -n " + activityId); 475 } 476 477 private static String expectCommandToSucceed(String cmd) throws IOException { 478 Pair<String, String> result = runShellCommand(cmd, null); 479 // UiAutomation.runShellCommand() does not return exit code. 480 // Assume that the command fails if stderr is not empty. 481 assertThat(result.second.trim()).isEmpty(); 482 return result.first; 483 } 484 485 private static void expectCommandToFail(String cmd) throws IOException { 486 Pair<String, String> result = runShellCommand(cmd, null); 487 // UiAutomation.runShellCommand() does not return exit code. 488 // Assume that the command fails if stderr is not empty. 489 assertThat(result.second.trim()).isNotEmpty(); 490 } 491 492 /** Runs a command and returns (stdout, stderr). */ 493 private static Pair<String, String> runShellCommand(String cmd, @Nullable InputStream input) 494 throws IOException { 495 Log.i(TAG, "runShellCommand: " + cmd); 496 UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 497 ParcelFileDescriptor[] rwe = automation.executeShellCommandRwe(cmd); 498 // executeShellCommandRwe returns [stdout, stdin, stderr]. 499 try (ParcelFileDescriptor outFd = rwe[0]; 500 ParcelFileDescriptor inFd = rwe[1]; 501 ParcelFileDescriptor errFd = rwe[2]) { 502 if (input != null) { 503 try (OutputStream os = new FileOutputStream(inFd.getFileDescriptor())) { 504 StreamUtil.copyStreams(input, os); 505 } 506 } 507 // We have to close stdin before reading stdout and stderr. 508 // It's safe to close ParcelFileDescriptor multiple times. 509 inFd.close(); 510 String stdout; 511 try (InputStream is = new FileInputStream(outFd.getFileDescriptor())) { 512 stdout = StreamUtil.readInputStream(is); 513 } 514 Log.i(TAG, "stdout = " + stdout); 515 String stderr; 516 try (InputStream is = new FileInputStream(errFd.getFileDescriptor())) { 517 stderr = StreamUtil.readInputStream(is); 518 } 519 Log.i(TAG, "stderr = " + stderr); 520 return new Pair<>(stdout, stderr); 521 } 522 } 523 524 private static boolean isFileOpenedBy(String path, String appId) throws Exception { 525 String pid = pidOf(appId); 526 if (pid.isEmpty()) { 527 return false; 528 } 529 String cmd = String.format("lsof -t -p %s %s", pid, path); 530 return !expectCommandToSucceed(cmd).trim().isEmpty(); 531 } 532 533 private static List<String> getOpenFiles(String appId) throws Exception { 534 String pid = pidOf(appId); 535 if (pid.isEmpty()) { 536 return Collections.emptyList(); 537 } 538 String cmd = String.format("lsof -p %s", pid); 539 String out = expectCommandToSucceed(cmd); 540 List<String> paths = new ArrayList<>(); 541 boolean first = true; 542 for (String line : out.split("\n")) { 543 // Skip the header. 544 if (first) { 545 first = false; 546 continue; 547 } 548 String[] records = line.split(" "); 549 if (records.length > 0) { 550 paths.add(records[records.length - 1]); 551 } 552 } 553 return paths; 554 } 555 556 private static String pidOf(String appId) throws Exception { 557 return expectCommandToSucceed("pidof " + appId).trim(); 558 } 559 560 private static long countMatch(List<String> paths, Pattern pattern) { 561 // Note: asPredicate() returns true for partial matching. 562 return paths.stream() 563 .filter(pattern.asPredicate()) 564 .count(); 565 } 566 } 567