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